diff --git a/index.js b/index.js index 8ba8d3f..b164453 100644 --- a/index.js +++ b/index.js @@ -2,7 +2,8 @@ const path = require('path'); const debug = require('debug')('cabinet'); - +const {createMatchPath} = require('tsconfig-paths'); +const fs = require('fs'); /* * most js resolver are lazy-loaded (only required when needed) * e.g. dont load requirejs when we only have commonjs modules to resolve @@ -44,7 +45,8 @@ const defaultLookups = { * @param {String} [options.nodeModulesConfig.entry] The new value for "main" in package json * @param {String} [options.webpackConfig] Path to the webpack config * @param {Object} [options.ast] A preparsed AST for the file identified by filename. - * @param {Object} [options.tsConfig] Path to a typescript config file + * @param {String|Object} [options.tsConfig] Path to a typescript configuration or an object representing a pre-parsed typescript config. + * @param {String} [options.tsConfigPath] A (virtual) Path to typescript config file when options.tsConfig is given as an object. Needed to calculate [Path Mapping](https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping). If not given when options.tsConfig is an object, Path Mapping is not considered. * @param {Boolean} [options.noTypeDefinitions] Whether to return '.d.ts' files or '.js' files for a dependency */ module.exports = function cabinet(options) { @@ -147,7 +149,6 @@ function getCompilerOptionsFromTsConfig(tsConfig) { if (!tsConfig) { debug('no tsconfig given, defaulting'); - } else if (typeof tsConfig === 'string') { debug('string tsconfig given, parsing'); @@ -183,7 +184,7 @@ function getCompilerOptionsFromTsConfig(tsConfig) { * @return {String} */ function jsLookup(options) { - const {dependency, filename, directory, config, webpackConfig, configPath, ast} = options; + const {dependency, filename, directory, config, webpackConfig, configPath, nodeModulesConfig, ast, tsConfig} = options; const type = module.exports._getJSType({ config: config, webpackConfig: webpackConfig, @@ -222,9 +223,13 @@ function jsLookup(options) { } } -function tsLookup({dependency, filename, tsConfig, noTypeDefinitions}) { +function tsLookup({dependency, filename, tsConfig, tsConfigPath, noTypeDefinitions}) { debug('performing a typescript lookup'); + if (typeof tsConfig === 'string') { + tsConfigPath = tsConfigPath || path.dirname(tsConfig); + } + let compilerOptions = getCompilerOptionsFromTsConfig(tsConfig); // Preserve for backcompat. Consider removing this as a breaking change. @@ -233,6 +238,7 @@ function tsLookup({dependency, filename, tsConfig, noTypeDefinitions}) { } const host = ts.createCompilerHost({}); + debug('with options: ', compilerOptions); const namedModule = ts.resolveModuleName(dependency, filename, compilerOptions, host); @@ -252,6 +258,49 @@ function tsLookup({dependency, filename, tsConfig, noTypeDefinitions}) { result = lookUpLocations.find(ts.sys.fileExists) || ''; } + if (!result && tsConfigPath && compilerOptions.baseUrl && compilerOptions.paths) { + const absoluteBaseUrl = path.join(path.dirname(tsConfigPath), compilerOptions.baseUrl); + // REF: https://github.com/dividab/tsconfig-paths#creatematchpath + const tsMatchPath = createMatchPath(absoluteBaseUrl, compilerOptions.paths); + const extensions = ['.ts', '.tsx', '.d.ts', '.js', '.jsx', '.json', '.node']; + // REF: https://github.com/dividab/tsconfig-paths#creatematchpath + const resolvedTsAliasPath = tsMatchPath(dependency, undefined, undefined, extensions); // Get absolute path by ts path mapping. `undefined` if non-existent + if (resolvedTsAliasPath) { + const stat = (() => { + try { + // fs.statSync throws an error if path is non-existent + return fs.statSync(resolvedTsAliasPath); + } catch (error) { + return undefined; + } + })(); + if (stat) { + if (stat.isDirectory()) { + // When directory is imported, index file is resolved + for (const ext of extensions) { + const filename = path.join(resolvedTsAliasPath, 'index' + ext); + if (fs.existsSync(filename)) { + result = filename; + break; + } + } + } else { + // if the path is complete filename + result = resolvedTsAliasPath; + } + } else { + // For cases a file extension is omitted when being imported + for (const ext of extensions) { + const filenameWithExt = resolvedTsAliasPath + ext; + if (fs.existsSync(filenameWithExt)) { + result = filenameWithExt; + break; + } + } + } + } + } + debug('result: ' + result); return result ? path.resolve(result) : ''; } @@ -259,6 +308,7 @@ function tsLookup({dependency, filename, tsConfig, noTypeDefinitions}) { function commonJSLookup(options) { const {filename, directory, nodeModulesConfig, tsConfig} = options; let {dependency} = options; + if (!resolve) { resolve = require('resolve'); } diff --git a/package-lock.json b/package-lock.json index 4dab8d3..1d0879e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,6 @@ "app-module-path": "^2.2.0", "commander": "^2.20.3", "debug": "^4.3.3", - "decomment": "^0.9.5", "enhanced-resolve": "^5.8.3", "is-relative-path": "^1.0.2", "module-definition": "^3.3.1", @@ -21,6 +20,7 @@ "resolve-dependency-path": "^2.0.0", "sass-lookup": "^3.0.0", "stylus-lookup": "^3.0.1", + "tsconfig-paths": "^3.10.1", "typescript": "^3.9.7" }, "bin": { @@ -92,6 +92,11 @@ "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", "dev": true }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=" + }, "node_modules/@ungap/promise-all-settled": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", @@ -848,30 +853,6 @@ "node": ">=0.10.0" } }, - "node_modules/decomment": { - "version": "0.9.5", - "resolved": "https://registry.npmjs.org/decomment/-/decomment-0.9.5.tgz", - "integrity": "sha512-h0TZ8t6Dp49duwyDHo3iw67mnh9/UpFiSSiOb5gDK1sqoXzrfX/SQxIUQd2R2QEiSnqib0KF2fnKnGfAhAs6lg==", - "dependencies": { - "esprima": "4.0.1" - }, - "engines": { - "node": ">=6.4", - "npm": ">=2.15" - } - }, - "node_modules/decomment/node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/deep-equal": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.0.4.tgz", @@ -2201,10 +2182,9 @@ } }, "node_modules/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" }, "node_modules/mkdirp": { "version": "0.5.5", @@ -3197,6 +3177,14 @@ "node": ">=8" } }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "engines": { + "node": ">=4" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -3339,6 +3327,28 @@ "integrity": "sha1-gUXkynyv9ArN48z5Rui4u3W0Fys=", "dev": true }, + "node_modules/tsconfig-paths": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", + "integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -3789,6 +3799,11 @@ "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", "dev": true }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=" + }, "@ungap/promise-all-settled": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", @@ -4415,21 +4430,6 @@ "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", "dev": true }, - "decomment": { - "version": "0.9.5", - "resolved": "https://registry.npmjs.org/decomment/-/decomment-0.9.5.tgz", - "integrity": "sha512-h0TZ8t6Dp49duwyDHo3iw67mnh9/UpFiSSiOb5gDK1sqoXzrfX/SQxIUQd2R2QEiSnqib0KF2fnKnGfAhAs6lg==", - "requires": { - "esprima": "4.0.1" - }, - "dependencies": { - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" - } - } - }, "deep-equal": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.0.4.tgz", @@ -5434,10 +5434,9 @@ } }, "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" }, "mkdirp": { "version": "0.5.5", @@ -6181,6 +6180,11 @@ "ansi-regex": "^5.0.1" } }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" + }, "strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -6279,6 +6283,27 @@ "integrity": "sha1-gUXkynyv9ArN48z5Rui4u3W0Fys=", "dev": true }, + "tsconfig-paths": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", + "integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==", + "requires": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "requires": { + "minimist": "^1.2.0" + } + } + } + }, "type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", diff --git a/package.json b/package.json index dbad0f2..56436fb 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "resolve-dependency-path": "^2.0.0", "sass-lookup": "^3.0.0", "stylus-lookup": "^3.0.1", + "tsconfig-paths": "^3.10.1", "typescript": "^3.9.7" } } diff --git a/readme.md b/readme.md index b4940c7..6c29766 100644 --- a/readme.md +++ b/readme.md @@ -7,21 +7,21 @@ ### Usage ```js - -var cabinet = require('filing-cabinet'); +var cabinet = require("filing-cabinet"); var result = cabinet({ - partial: 'somePartialPath', - directory: 'path/to/all/files', - filename: 'path/to/parent/file', + partial: "somePartialPath", + directory: "path/to/all/files", + filename: "path/to/parent/file", ast: {}, // an optional AST representation of `filename` // Only for JavaScript files - config: 'path/to/requirejs/config', - webpackConfig: 'path/to/webpack/config', + config: "path/to/requirejs/config", + webpackConfig: "path/to/webpack/config", nodeModulesConfig: { - entry: 'module' + entry: "module", }, - tsConfig: 'path/to/typescript/config' + tsConfig: "path/to/tsconfig.json" // or an object + tsConfigPath: "path/to/tsconfig.json", }); console.log(result); // /absolute/path/to/somePartialPath @@ -37,38 +37,39 @@ console.log(result); // /absolute/path/to/somePartialPath * `webpackConfig`: (optional) webpack config for resolving aliased JavaScript modules. If exporting multiple configurations, the first configuration is used. * `nodeModulesConfig`: (optional) config for resolving entry file for node_modules. This value overrides the `main` attribute in the package.json file; used in conjunction with the [packageFilter](https://github.com/browserify/resolve#resolveid-opts-cb) of the `resolve` package. * `tsConfig`: (optional) path to a typescript configuration. Could also be an object representing a pre-parsed typescript config. +* `tsConfigPath`: (optional) A (virtual) Path to typescript config file when `tsConfig` option is given as an object, not a string. Needed to calculate [Path Mapping](https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping). If not given when `tsConfig` is an object, **Path Mapping** is ignored. This is not need when `tsConfig` is given as string (path to the tsconfig file). * `noTypeDefinitions`: (optional) For typescript files, whether to prefer `*.js` over `*.d.ts`. ### Registered languages By default, filing-cabinet provides support for the following languages: -* JavaScript: CommonJS, AMD, ES6 -* TypeScript -* CSS Preprocessors: Sass (`.scss` and `.sass`), Stylus (`.styl`), and Less (`.less`) +- JavaScript: CommonJS, AMD, ES6 +- TypeScript +- CSS Preprocessors: Sass (`.scss` and `.sass`), Stylus (`.styl`), and Less (`.less`) You can register resolvers for new languages via `cabinet.register(extension, resolver)`. -* `extension`: the extension of the file that should use the custom resolver (ex: '.py', '.php') -* `resolver`: a function that accepts the following (ordered) arguments that were given to cabinet: - * `partial` - * `filename` - * `directory` - * `config` +- `extension`: the extension of the file that should use the custom resolver (ex: '.py', '.php') +- `resolver`: a function that accepts the following (ordered) arguments that were given to cabinet: + - `partial` + - `filename` + - `directory` + - `config` For examples of resolver implementations, take a look at the default language resolvers: -* [sass-lookup](https://github.com/mrjoelkemp/node-sass-lookup) -* [stylus-lookup](https://github.com/mrjoelkemp/node-stylus-lookup) -* [amdLookup](https://github.com/mrjoelkemp/node-module-lookup-amd) +- [sass-lookup](https://github.com/mrjoelkemp/node-sass-lookup) +- [stylus-lookup](https://github.com/mrjoelkemp/node-stylus-lookup) +- [amdLookup](https://github.com/mrjoelkemp/node-module-lookup-amd) If a given extension does not have a registered resolver, cabinet will use a generic file resolver which is basically `require('path').join` with a bit of extension defaulting logic. ### Shell script -* Requires a global install `npm install -g filing-cabinet` +- Requires a global install `npm install -g filing-cabinet` `filing-cabinet [options] ` -* See `filing-cabinet --help` for details on the options +- See `filing-cabinet --help` for details on the options diff --git a/test/root3/README.md b/test/root3/README.md new file mode 100644 index 0000000..4735536 --- /dev/null +++ b/test/root3/README.md @@ -0,0 +1,3 @@ +# root3 + +This is a simple example for typescript monorepo, using [path mapping](https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping). diff --git a/test/root3/packages/bar/index.ts b/test/root3/packages/bar/index.ts new file mode 100644 index 0000000..750f16d --- /dev/null +++ b/test/root3/packages/bar/index.ts @@ -0,0 +1,8 @@ +import { doubleNumbers } from "@monorepo/foo"; + +export const run = () => { + const value = doubleNumbers([1, 2, 3]); + return value; +}; + +console.log(run()); diff --git a/test/root3/packages/foo/hello.ts b/test/root3/packages/foo/hello.ts new file mode 100644 index 0000000..026083d --- /dev/null +++ b/test/root3/packages/foo/hello.ts @@ -0,0 +1,3 @@ +export const hello = (to: string) => { + console.log(`hello ${to}`) +} diff --git a/test/root3/packages/foo/index.ts b/test/root3/packages/foo/index.ts new file mode 100644 index 0000000..f844474 --- /dev/null +++ b/test/root3/packages/foo/index.ts @@ -0,0 +1,10 @@ +/* The module name can be just './hello'. +But '#foo/hello' is demonstration of "Path Mapping" of `tsconfig` +and "Subpath imports"(defined in package.json's `imports` field) of node.js */ +import { hello } from "#foo/hello"; +// import { hello } from './hello' => this will work, too +// import { hello } from '@monorepo/foo/hello' // => this will not work for tsc, without additional configuration on tsconfig.json + +hello("world"); + +export const doubleNumbers = (data: number[]) => data.map((i) => i * 2); diff --git a/test/root3/tsconfig.json b/test/root3/tsconfig.json new file mode 100644 index 0000000..010d505 --- /dev/null +++ b/test/root3/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "CommonJS", + "rootDir": ".", + "baseUrl": "packages", + "paths": { + "@monorepo/*": ["*"], + "#foo/*": ["foo/*"], + "#bar/*": ["bar/*"], + "#*": ["*"] + }, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "removeComments": true, + "composite": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "esModuleInterop": true, + "incremental": true, + "resolveJsonModule": true + } +} diff --git a/test/test.js b/test/test.js index f97bcd1..ffa770d 100644 --- a/test/test.js +++ b/test/test.js @@ -518,6 +518,30 @@ describe('filing-cabinet', function() { assert.equal(result, path.join(directory, 'foo.ts')); }); }); + describe(`when the typescript's path mapping is configured`, function() { + it('should resolve the path', function() { + const result = cabinet({ + partial: '#foo/hello', + filename: path.resolve(__dirname, 'root3', 'packages', 'foo', 'index.ts'), + directory: path.resolve(__dirname, 'root3'), + tsConfig: { + 'compilerOptions': { + 'rootDir': '.', + 'baseUrl': 'packages', + 'paths': { + '@monorepo/*': ['*'], + '#foo/*': ['foo/*'], + '#bar/*': ['bar/*'], + '#*': ['*'] + }, + }, + }, + tsConfigPath: path.resolve(__dirname, 'root3', 'tsconfig.json'), + }); + const expected = path.resolve(__dirname, 'root3', 'packages', 'foo', 'hello.ts'); + assert.equal(result, expected); + }); + }); }); describe('when not given a tsconfig', function() {