Skip to content

Instantly share code, notes, and snippets.

@Nemo157
Last active August 29, 2015 14:06
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Nemo157/f20064a282ee620f3877 to your computer and use it in GitHub Desktop.
Save Nemo157/f20064a282ee620f3877 to your computer and use it in GitHub Desktop.
Proposed TypeScript Require Resolution Semantics

Proposed TypeScript Require Resolution Semantics

These resolution semantics are taken directly from the node API documentation and changed to be appropriate for the TypeScript compiler. Some parts of the resolution strategy are defined in the TypeScript Language Specification and therefore cannot be changed in the compiler implementation. Luckily these largely agree between the node and TypeScript definitions, to make the TypeScript compiler follow these semantics will just require changing the host specific external module resolution.

If the import declaration specifies a top-level external module name and the program contains no AmbientExternalModuleDeclaration (§12.1.6) with a string literal that specifies that exact name, the name is resolved in a host dependent manner (for example by considering the name relative to a module name space root). If a matching module cannot be found an error occurs.

TypeScript Language Specification §11.2.1

Resolution as node Documents It

File Modules

TypeScript will attempt to load the required filename with the added extension of .ts then .d.ts.

A module prefixed with '/' is an absolute path to the file. For example, require('/home/marco/foo') will load the file at /home/marco/foo.ts.

A module prefixed with './' or '../' is relative to the file calling require(). That is, circle.ts must be in the same directory as foo.ts for require('./circle') to find it.

Without a leading '/', './' or '../' to indicate a file, the module is loaded from a node_modules or typings folder.

Loading From node_modules and typings Folders

If the module identifier passed to require() does not begin with '/', '../', or './', then TypeScript starts at the parent directory of the current module, adds /node_modules, and attempts to load the module from that location.

If it is not found there then it instead trys adding /typings, and attempts to load the module from that location.

If it is not found there, then it moves to the parent directory and repeats the two load attempts, and so on, until the root of the file system or a directory called node_modules or typings is reached.

For example, if the file at '/home/ry/node_modules/projects/foo.ts' called require('bar'), then TypeScript would look in the following locations, in this order (each of these locations would expand to multiple files as specified in Folders as Modules):

  • /home/ry/node_modules/projects/node_modules/bar
  • /home/ry/node_modules/projects/typings/bar
  • /home/ry/node_modules/node_modules/bar
  • /home/ry/node_modules/typings/bar
  • /home/ry/node_modules/bar

This allows programs to localize their dependencies, so that they do not clash.

Folders as Modules

It is convenient to organize programs and libraries into self-contained directories, and then provide a single entry point to that library. There are three ways in which a folder may be passed to require() as an argument.

The first is to create a package.json file in the root of the folder, which specifies a main module or definition file. An example package.json file might look like this:

{
  "name" : "some-library",
  "typescript": {
    "main": "./src/some-library.ts",
    "definition": "./dist/some-library.d.ts"
  }
}

If this was in a folder at ./some-library, then require('./some-library') would attempt to load ./some-library/src/some-library.ts.

If there is no typescript.main value in the package.json then TypeScript will fallback to loading the file specified by typescript.definition, e.g. if the example did not have typescript.main then require('./some-library') would attempt to load ./some-library/dist/some-library.d.ts.

This is the extent of TypeScripts awareness of package.json files.

If there is no package.json file present in the directory, or neither of typescript.main and typescript.definition are set, then TypeScript will attempt to load an index.ts or index.d.ts file out of that directory. For example, if there was no package.json file in the above example, then require('./some-library') would attempt to load:

  • ./some-library/index.ts
  • ./some-library/index.d.ts

Resolution in TypeScript Language Specification Terms

This extends TypeScript Language Specification §11.2.1. Specifically this extends the final bullet point of the section, defining a host dependent manner in which to resolve top-level external module names where there is no AmbientExternalModuleDeclaration that matches. This also adds in an extension to allow external modules to have multiple file paths associated with them, allowing for Folders as Modules.

Host Dependent Top-Level External Module Resolution

  • Starting at the parent directory of the module requiring the external module the following steps will be followed:
    • The current directory has '/node_modules' appended to it followed by the external module name, this is attempted to be resolved as an (absolute) relative external module name. If resolved this is returned as the resolution of the module.
    • The current directory has '/typings' appended to it followed by the external module name, this is attempted to be resolved as an (absolute) relative external module name. If resolved this is returned as the resolution of the module.
    • If the current directory is the root of the file system or is a directory called 'node_modules' or 'typings' then the module is not found and an error occurs.
    • The current directory is changed to the parent of the current directory.

For example, if the file at '/home/ry/node_modules/projects/foo.ts' called require('bar'), then TypeScript would attempt to resolve the following relative external module names, in this order (each of these locations would expand to multiple files as specified in Extended File Paths):

  • /home/ry/node_modules/projects/node_modules/bar
  • /home/ry/node_modules/projects/typings/bar
  • /home/ry/node_modules/node_modules/bar
  • /home/ry/node_modules/typings/bar
  • /home/ry/node_modules/bar

This allows programs to localize their dependencies, so that they do not clash.

Extended File Paths

In addition to associating the module's source file path without extension there are multiple special case paths that must be associated with source files to ensure the Folders as Modules section above works. They are:

  • If the file is named 'index.ts' and there is no 'package.json' next to the file it also gains its parent directory's path as a file path.
  • If the file is named 'index.ts' and there is a 'package.json' next to the file, but that 'package.json' does not contain either of typescript.main or typescript.definition fields then the file also gains its parent directory's path as a file path.
  • If the file is named 'index.d.ts' and there is no 'package.json' or 'index.ts' next to the file it also gains its parent directory's path as a file path.
  • If the file is named 'index.d.ts' and there is no 'index.ts' and there is a 'package.json' next to the file, but that 'package.json' does not contain either of typescript.main or typescript.definition fields then the file also gains its parent directory's path as a file path.
  • If at some point in the file system hierarchy above the file there is a 'package.json' that contains a typescript.main which references the file when resolved relative to the 'package.json' then the file also gains the 'package.json's parent directory's path as a file path.
  • If at some point in the file system hierarchy above the file there is a 'package.json' that contains a typescript.definition which references the file when resolved relative to the 'package.json' and does not contain a typescript.main then the file also gains the 'package.json's parent directory's path as a file path.

Examples

The following directory tree (plus package.json contents)

/
├── some-lib
│   └── index.ts
├── other-lib
│   ├── package.json
│   │       { typescript: { main: 'main.ts' } }
│   └── main.ts
└── weird-lib
    ├── package.json
    │       { typescript: { main: './dist/bundle.ts' } }
    └── dist
        └── bundle.ts

will have the following mapping of files to file paths

{
"/some-lib/index.ts": [ "/some-lib/index", "/some-lib" ],
"/other-lib/main.ts": [ "/other-lib/main", "/other-lib" ],
"/weird-lib/dist/bundle.ts": [ "/weird-lib/dist/bundle", "/weird-lib" ]
}

Psuedo-Javascript of Require Resolution

function require(X) { // from module at path Y
  if (X.begins_with_any('./', '/', '../')) {
    return load_as_file(Y + X)
      || load_as_directory(Y + X);
  } else {
    return load_node_modules_or_typings(X, dirname(Y))
      || throw "Not found";
  }
}

function load_as_file(X) {
  if (file.exists(X)) {
    return load(X);
  } else if (file.exists(X + '.ts')) {
    return load(X + '.ts');
  } else if (file.exists(X + '.d.ts')) {
    return load(X + '.d.ts');
  }
}

function load_as_directory(X) {
  if (file.exists(X + '/package.json')) {
    var pkg = load(X + '/package.json');
    if (pkg.typescript) {
      if (pkg.typescript.main) {
        return load_as_file(X + pkg.typescript.main);
      } else if (pkg.typescript.definition) {
        return load_as_file(X + pkg.typescript.definition)
      }
    }
  }
  return load_as_file(X + '/index');
}

function load_node_modules_or_typings(X, start) {
  var result;
  node_modules_paths(start).any(function (dir) {
    return (result = (load_as_file(dir + X) || load_as_directory(dir + X)) )
 } return result;
}

function node_modules_paths(start) {
  var parts = path.split(start);
  var dirs = [];
  parts.reverse().any(function (part) {
    if (part === "node_modules" || part === "typings") {
      return true;
    }
    dirs.push(dir + "/node_modules");
    dirs.push(dir + "/typings");
  });
  return dirs;
}
@wokim
Copy link

wokim commented Dec 10, 2014

👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment