From d517132890318586c0ccd45905dc66bf52425844 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Tue, 1 May 2018 07:39:23 -0700 Subject: [PATCH] Support source field in package.json to enable babel on symlinked modules (#1101) --- src/Resolver.js | 63 ++++++++++++---- src/transforms/babel.js | 24 ++++-- src/utils/fs.js | 1 + .../.eslintrc.json | 6 ++ .../index.js | 4 + .../node_modules/foo/index.js | 1 + .../node_modules/foo/package.json | 4 + .../package.json | 4 + .../babel-node-modules-source/.eslintrc.json | 6 ++ .../babel-node-modules-source/index.js | 4 + .../node_modules/foo | 1 + .../babel-node-modules-source/package.json | 4 + .../packages/foo/index.js | 1 + .../packages/foo/package.json | 4 + .../package-alias-glob/package.json | 6 ++ .../package-alias-glob/src/test.js | 0 test/integration/resolver/node_modules/source | 1 + .../resolver/node_modules/source-alias | 1 + .../resolver/node_modules/source-alias-glob | 1 + .../node_modules/source-not-symlinked/dist.js | 0 .../source-not-symlinked/package.json | 5 ++ .../source-not-symlinked/source.js | 0 .../packages/source-alias-glob/package.json | 7 ++ .../packages/source-alias-glob/src/test.js | 0 .../resolver/packages/source-alias/dist.js | 0 .../resolver/packages/source-alias/other.js | 0 .../packages/source-alias/package.json | 6 ++ .../resolver/packages/source-alias/source.js | 0 .../resolver/packages/source/dist.js | 0 .../resolver/packages/source/package.json | 5 ++ .../resolver/packages/source/source.js | 0 test/javascript.js | 18 +++++ test/resolver.js | 74 +++++++++++++++++++ 33 files changed, 230 insertions(+), 21 deletions(-) create mode 100644 test/integration/babel-node-modules-source-unlinked/.eslintrc.json create mode 100644 test/integration/babel-node-modules-source-unlinked/index.js create mode 100644 test/integration/babel-node-modules-source-unlinked/node_modules/foo/index.js create mode 100644 test/integration/babel-node-modules-source-unlinked/node_modules/foo/package.json create mode 100644 test/integration/babel-node-modules-source-unlinked/package.json create mode 100644 test/integration/babel-node-modules-source/.eslintrc.json create mode 100644 test/integration/babel-node-modules-source/index.js create mode 120000 test/integration/babel-node-modules-source/node_modules/foo create mode 100644 test/integration/babel-node-modules-source/package.json create mode 100644 test/integration/babel-node-modules-source/packages/foo/index.js create mode 100644 test/integration/babel-node-modules-source/packages/foo/package.json create mode 100644 test/integration/resolver/node_modules/package-alias-glob/package.json create mode 100644 test/integration/resolver/node_modules/package-alias-glob/src/test.js create mode 120000 test/integration/resolver/node_modules/source create mode 120000 test/integration/resolver/node_modules/source-alias create mode 120000 test/integration/resolver/node_modules/source-alias-glob create mode 100644 test/integration/resolver/node_modules/source-not-symlinked/dist.js create mode 100644 test/integration/resolver/node_modules/source-not-symlinked/package.json create mode 100644 test/integration/resolver/node_modules/source-not-symlinked/source.js create mode 100644 test/integration/resolver/packages/source-alias-glob/package.json create mode 100644 test/integration/resolver/packages/source-alias-glob/src/test.js create mode 100644 test/integration/resolver/packages/source-alias/dist.js create mode 100644 test/integration/resolver/packages/source-alias/other.js create mode 100644 test/integration/resolver/packages/source-alias/package.json create mode 100644 test/integration/resolver/packages/source-alias/source.js create mode 100644 test/integration/resolver/packages/source/dist.js create mode 100644 test/integration/resolver/packages/source/package.json create mode 100644 test/integration/resolver/packages/source/source.js diff --git a/src/Resolver.js b/src/Resolver.js index cdcad3a9aba..ec802a557ef 100644 --- a/src/Resolver.js +++ b/src/Resolver.js @@ -2,8 +2,10 @@ const builtins = require('./builtins'); const path = require('path'); const glob = require('glob'); const fs = require('./utils/fs'); +const micromatch = require('micromatch'); const EMPTY_SHIM = require.resolve('./builtins/_empty'); +const GLOB_RE = /[*+{}]/; /** * This resolver implements a modified version of the node_modules resolution algorithm: @@ -36,7 +38,7 @@ class Resolver { } // Check if this is a glob - if (/[*+{}]/.test(filename) && glob.hasMagic(filename)) { + if (GLOB_RE.test(filename) && glob.hasMagic(filename)) { return {path: path.resolve(path.dirname(parent), filename)}; } @@ -251,16 +253,30 @@ class Resolver { pkg.pkgfile = file; pkg.pkgdir = dir; + // If the package has a `source` field, check if it is behind a symlink. + // If so, we treat the module as source code rather than a pre-compiled module. + if (pkg.source) { + let realpath = await fs.realpath(file); + if (realpath === file) { + delete pkg.source; + } + } + this.packageCache.set(file, pkg); return pkg; } getPackageMain(pkg) { // libraries like d3.js specifies node.js specific files in the "main" which breaks the build - // we use the "module" or "jsnext:main" field to get the full dependency tree if available - let main = [pkg.module, pkg['jsnext:main'], pkg.browser, pkg.main].find( - entry => typeof entry === 'string' - ); + // we use the "module" or "jsnext:main" field to get the full dependency tree if available. + // If this is a linked module with a `source` field, use that as the entry point. + let main = [ + pkg.source, + pkg.module, + pkg['jsnext:main'], + pkg.browser, + pkg.main + ].find(entry => typeof entry === 'string'); // Default to index file if no main field find if (!main || main === '.' || main === './') { @@ -307,16 +323,17 @@ class Resolver { } resolvePackageAliases(filename, pkg) { - // Resolve aliases in the package.alias and package.browser fields. - if (pkg) { - return ( - this.getAlias(filename, pkg.pkgdir, pkg.alias) || - this.getAlias(filename, pkg.pkgdir, pkg.browser) || - filename - ); + if (!pkg) { + return filename; } - return filename; + // Resolve aliases in the package.source, package.alias, and package.browser fields. + return ( + this.getAlias(filename, pkg.pkgdir, pkg.source) || + this.getAlias(filename, pkg.pkgdir, pkg.alias) || + this.getAlias(filename, pkg.pkgdir, pkg.browser) || + filename + ); } getAlias(filename, dir, aliases) { @@ -333,7 +350,7 @@ class Resolver { filename = './' + filename; } - alias = aliases[filename]; + alias = this.lookupAlias(aliases, filename); } else { // It is a node_module. First try the entire filename as a key. alias = aliases[filename]; @@ -363,6 +380,24 @@ class Resolver { return alias; } + lookupAlias(aliases, filename) { + // First, try looking up the exact filename + let alias = aliases[filename]; + if (alias != null) { + return alias; + } + + // Otherwise, try replacing glob keys + for (let key in aliases) { + if (GLOB_RE.test(key)) { + let re = micromatch.makeRe(key, {capture: true}); + if (re.test(filename)) { + return filename.replace(re, aliases[key]); + } + } + } + } + async findPackage(dir) { // Find the nearest package.json file within the current node_modules folder let root = path.parse(dir).root; diff --git a/src/transforms/babel.js b/src/transforms/babel.js index 90cc6caa1c6..2f432af8027 100644 --- a/src/transforms/babel.js +++ b/src/transforms/babel.js @@ -89,9 +89,18 @@ async function getBabelConfig(asset) { return asset.babelConfig; } - let babelrc = await getBabelRc(asset); - let envConfig = await getEnvConfig(asset, !!babelrc); - let jsxConfig = getJSXConfig(asset, !!babelrc); + // Consider the module source code rather than precompiled if the resolver + // used the `source` field, or it is not in node_modules. + let isSource = + !!(asset.package && asset.package.source) || + !asset.name.includes(NODE_MODULES); + + // Try to resolve a .babelrc file. If one is found, consider the module source code. + let babelrc = await getBabelRc(asset, isSource); + isSource = isSource || !!babelrc; + + let envConfig = await getEnvConfig(asset, isSource); + let jsxConfig = getJSXConfig(asset, isSource); // Merge the babel-preset-env config and the babelrc if needed if (babelrc && !shouldIgnoreBabelrc(asset.name, babelrc)) { @@ -162,8 +171,9 @@ function getPluginName(p) { * Finds a .babelrc for an asset. By default, .babelrc files inside node_modules are not used. * However, there are some exceptions: * - if `browserify.transforms` includes "babelify" in package.json (for legacy module compat) + * - the `source` field in package.json is used by the resolver */ -async function getBabelRc(asset) { +async function getBabelRc(asset, isSource) { // Support legacy browserify packages let browserify = asset.package && asset.package.browserify; if (browserify && Array.isArray(browserify.transform)) { @@ -182,7 +192,7 @@ async function getBabelRc(asset) { } // If this asset is not in node_modules, always use the .babelrc - if (!asset.name.includes(NODE_MODULES)) { + if (isSource) { return await findBabelRc(asset); } @@ -224,7 +234,7 @@ async function getEnvConfig(asset, isSourceModule) { // If this is the app module, the source and target will be the same, so just compile everything. // Otherwise, load the source engines and generate a babel-present-env config. - if (asset.name.includes(NODE_MODULES) && !isSourceModule) { + if (!isSourceModule) { let sourceEngines = await getTargetEngines(asset, false); let sourceEnv = (await getEnvPlugins(sourceEngines, false)) || targetEnv; @@ -264,7 +274,7 @@ async function getEnvPlugins(targets, useBuiltIns = false) { */ function getJSXConfig(asset, isSourceModule) { // Don't enable JSX in node_modules - if (asset.name.includes(NODE_MODULES) && !isSourceModule) { + if (!isSourceModule) { return null; } diff --git a/src/utils/fs.js b/src/utils/fs.js index 1622e52425a..a301f0eadce 100644 --- a/src/utils/fs.js +++ b/src/utils/fs.js @@ -7,6 +7,7 @@ exports.writeFile = promisify(fs.writeFile); exports.stat = promisify(fs.stat); exports.readdir = promisify(fs.readdir); exports.unlink = promisify(fs.unlink); +exports.realpath = promisify(fs.realpath); exports.exists = function(filename) { return new Promise(resolve => { diff --git a/test/integration/babel-node-modules-source-unlinked/.eslintrc.json b/test/integration/babel-node-modules-source-unlinked/.eslintrc.json new file mode 100644 index 00000000000..f89e231fa93 --- /dev/null +++ b/test/integration/babel-node-modules-source-unlinked/.eslintrc.json @@ -0,0 +1,6 @@ +{ + "extends": "../.eslintrc.json", + "parserOptions": { + "sourceType": "module" + } +} \ No newline at end of file diff --git a/test/integration/babel-node-modules-source-unlinked/index.js b/test/integration/babel-node-modules-source-unlinked/index.js new file mode 100644 index 00000000000..21b5030d8ab --- /dev/null +++ b/test/integration/babel-node-modules-source-unlinked/index.js @@ -0,0 +1,4 @@ +import Foo from 'foo'; + +export {Foo}; +export class Bar {} diff --git a/test/integration/babel-node-modules-source-unlinked/node_modules/foo/index.js b/test/integration/babel-node-modules-source-unlinked/node_modules/foo/index.js new file mode 100644 index 00000000000..7804111002d --- /dev/null +++ b/test/integration/babel-node-modules-source-unlinked/node_modules/foo/index.js @@ -0,0 +1 @@ +export default class Foo {} diff --git a/test/integration/babel-node-modules-source-unlinked/node_modules/foo/package.json b/test/integration/babel-node-modules-source-unlinked/node_modules/foo/package.json new file mode 100644 index 00000000000..ccd5bdb1ee5 --- /dev/null +++ b/test/integration/babel-node-modules-source-unlinked/node_modules/foo/package.json @@ -0,0 +1,4 @@ +{ + "name": "foo", + "source": true +} diff --git a/test/integration/babel-node-modules-source-unlinked/package.json b/test/integration/babel-node-modules-source-unlinked/package.json new file mode 100644 index 00000000000..64d9b8d7f5e --- /dev/null +++ b/test/integration/babel-node-modules-source-unlinked/package.json @@ -0,0 +1,4 @@ +{ + "name": "parcel-test-browser-browserslist", + "browserslist": ["last 2 Chrome versions", "IE >= 11"] +} diff --git a/test/integration/babel-node-modules-source/.eslintrc.json b/test/integration/babel-node-modules-source/.eslintrc.json new file mode 100644 index 00000000000..f89e231fa93 --- /dev/null +++ b/test/integration/babel-node-modules-source/.eslintrc.json @@ -0,0 +1,6 @@ +{ + "extends": "../.eslintrc.json", + "parserOptions": { + "sourceType": "module" + } +} \ No newline at end of file diff --git a/test/integration/babel-node-modules-source/index.js b/test/integration/babel-node-modules-source/index.js new file mode 100644 index 00000000000..21b5030d8ab --- /dev/null +++ b/test/integration/babel-node-modules-source/index.js @@ -0,0 +1,4 @@ +import Foo from 'foo'; + +export {Foo}; +export class Bar {} diff --git a/test/integration/babel-node-modules-source/node_modules/foo b/test/integration/babel-node-modules-source/node_modules/foo new file mode 120000 index 00000000000..61d898a3a60 --- /dev/null +++ b/test/integration/babel-node-modules-source/node_modules/foo @@ -0,0 +1 @@ +../packages/foo \ No newline at end of file diff --git a/test/integration/babel-node-modules-source/package.json b/test/integration/babel-node-modules-source/package.json new file mode 100644 index 00000000000..64d9b8d7f5e --- /dev/null +++ b/test/integration/babel-node-modules-source/package.json @@ -0,0 +1,4 @@ +{ + "name": "parcel-test-browser-browserslist", + "browserslist": ["last 2 Chrome versions", "IE >= 11"] +} diff --git a/test/integration/babel-node-modules-source/packages/foo/index.js b/test/integration/babel-node-modules-source/packages/foo/index.js new file mode 100644 index 00000000000..7804111002d --- /dev/null +++ b/test/integration/babel-node-modules-source/packages/foo/index.js @@ -0,0 +1 @@ +export default class Foo {} diff --git a/test/integration/babel-node-modules-source/packages/foo/package.json b/test/integration/babel-node-modules-source/packages/foo/package.json new file mode 100644 index 00000000000..ccd5bdb1ee5 --- /dev/null +++ b/test/integration/babel-node-modules-source/packages/foo/package.json @@ -0,0 +1,4 @@ +{ + "name": "foo", + "source": true +} diff --git a/test/integration/resolver/node_modules/package-alias-glob/package.json b/test/integration/resolver/node_modules/package-alias-glob/package.json new file mode 100644 index 00000000000..e931f12003f --- /dev/null +++ b/test/integration/resolver/node_modules/package-alias-glob/package.json @@ -0,0 +1,6 @@ +{ + "name": "package-alias-glob", + "alias": { + "./lib/*": "./src/$1" + } +} diff --git a/test/integration/resolver/node_modules/package-alias-glob/src/test.js b/test/integration/resolver/node_modules/package-alias-glob/src/test.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/integration/resolver/node_modules/source b/test/integration/resolver/node_modules/source new file mode 120000 index 00000000000..b069b24161e --- /dev/null +++ b/test/integration/resolver/node_modules/source @@ -0,0 +1 @@ +../packages/source \ No newline at end of file diff --git a/test/integration/resolver/node_modules/source-alias b/test/integration/resolver/node_modules/source-alias new file mode 120000 index 00000000000..ab07af6a104 --- /dev/null +++ b/test/integration/resolver/node_modules/source-alias @@ -0,0 +1 @@ +../packages/source-alias \ No newline at end of file diff --git a/test/integration/resolver/node_modules/source-alias-glob b/test/integration/resolver/node_modules/source-alias-glob new file mode 120000 index 00000000000..124948a55e2 --- /dev/null +++ b/test/integration/resolver/node_modules/source-alias-glob @@ -0,0 +1 @@ +../packages/source-alias-glob \ No newline at end of file diff --git a/test/integration/resolver/node_modules/source-not-symlinked/dist.js b/test/integration/resolver/node_modules/source-not-symlinked/dist.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/integration/resolver/node_modules/source-not-symlinked/package.json b/test/integration/resolver/node_modules/source-not-symlinked/package.json new file mode 100644 index 00000000000..21aa7122433 --- /dev/null +++ b/test/integration/resolver/node_modules/source-not-symlinked/package.json @@ -0,0 +1,5 @@ +{ + "name": "source-not-symlinked", + "main": "dist.js", + "source": "source.js" +} diff --git a/test/integration/resolver/node_modules/source-not-symlinked/source.js b/test/integration/resolver/node_modules/source-not-symlinked/source.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/integration/resolver/packages/source-alias-glob/package.json b/test/integration/resolver/packages/source-alias-glob/package.json new file mode 100644 index 00000000000..e49cd95fdfb --- /dev/null +++ b/test/integration/resolver/packages/source-alias-glob/package.json @@ -0,0 +1,7 @@ +{ + "name": "source", + "main": "lib/test.js", + "source": { + "./lib/*": "./src/$1" + } +} diff --git a/test/integration/resolver/packages/source-alias-glob/src/test.js b/test/integration/resolver/packages/source-alias-glob/src/test.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/integration/resolver/packages/source-alias/dist.js b/test/integration/resolver/packages/source-alias/dist.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/integration/resolver/packages/source-alias/other.js b/test/integration/resolver/packages/source-alias/other.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/integration/resolver/packages/source-alias/package.json b/test/integration/resolver/packages/source-alias/package.json new file mode 100644 index 00000000000..f8a82e5191d --- /dev/null +++ b/test/integration/resolver/packages/source-alias/package.json @@ -0,0 +1,6 @@ +{ + "name": "source-alias", + "source": { + "./dist": "./source" + } +} diff --git a/test/integration/resolver/packages/source-alias/source.js b/test/integration/resolver/packages/source-alias/source.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/integration/resolver/packages/source/dist.js b/test/integration/resolver/packages/source/dist.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/integration/resolver/packages/source/package.json b/test/integration/resolver/packages/source/package.json new file mode 100644 index 00000000000..39197dfefc7 --- /dev/null +++ b/test/integration/resolver/packages/source/package.json @@ -0,0 +1,5 @@ +{ + "name": "source", + "main": "dist.js", + "source": "source.js" +} diff --git a/test/integration/resolver/packages/source/source.js b/test/integration/resolver/packages/source/source.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/javascript.js b/test/javascript.js index c05d3a704a4..1ff6b3d029e 100644 --- a/test/javascript.js +++ b/test/javascript.js @@ -723,6 +723,24 @@ describe('javascript', function() { assert(!file.includes('class Bar {}')); }); + it('should compile node_modules when symlinked with a source field in package.json', async function() { + await bundle(__dirname + '/integration/babel-node-modules-source/index.js'); + + let file = fs.readFileSync(__dirname + '/dist/index.js', 'utf8'); + assert(!file.includes('class Foo {}')); + assert(!file.includes('class Bar {}')); + }); + + it('should not compile node_modules with a source field in package.json when not symlinked', async function() { + await bundle( + __dirname + '/integration/babel-node-modules-source-unlinked/index.js' + ); + + let file = fs.readFileSync(__dirname + '/dist/index.js', 'utf8'); + assert(file.includes('class Foo {}')); + assert(!file.includes('class Bar {}')); + }); + it('should support compiling JSX', async function() { await bundle(__dirname + '/integration/jsx/index.jsx'); diff --git a/test/resolver.js b/test/resolver.js index d4e6df7634d..5d36bd739d2 100644 --- a/test/resolver.js +++ b/test/resolver.js @@ -295,6 +295,24 @@ describe('resolver', function() { assert.equal(resolved.pkg.name, 'package-alias'); }); + it('should alias a glob using the package.alias field', async function() { + let resolved = await resolver.resolve( + './lib/test', + path.join(rootDir, 'node_modules', 'package-alias-glob', 'index.js') + ); + assert.equal( + resolved.path, + path.join( + rootDir, + 'node_modules', + 'package-alias-glob', + 'src', + 'test.js' + ) + ); + assert.equal(resolved.pkg.name, 'package-alias-glob'); + }); + it('should apply a module alias using the package.alias field in the root package', async function() { let resolved = await resolver.resolve( 'aliased', @@ -392,6 +410,62 @@ describe('resolver', function() { }); }); + describe('source field', function() { + it('should use the source field when symlinked', async function() { + let resolved = await resolver.resolve( + 'source', + path.join(rootDir, 'foo.js') + ); + assert.equal( + resolved.path, + path.join(rootDir, 'node_modules', 'source', 'source.js') + ); + assert(resolved.pkg.source); + }); + + it('should not use the source field when not symlinked', async function() { + let resolved = await resolver.resolve( + 'source-not-symlinked', + path.join(rootDir, 'foo.js') + ); + assert.equal( + resolved.path, + path.join(rootDir, 'node_modules', 'source-not-symlinked', 'dist.js') + ); + assert(!resolved.pkg.source); + }); + + it('should use the source field as an alias when symlinked', async function() { + let resolved = await resolver.resolve( + 'source-alias/dist', + path.join(rootDir, 'foo.js') + ); + assert.equal( + resolved.path, + path.join(rootDir, 'node_modules', 'source-alias', 'source.js') + ); + assert(resolved.pkg.source); + }); + + it('should use the source field as a glob alias when symlinked', async function() { + let resolved = await resolver.resolve( + 'source-alias-glob', + path.join(rootDir, 'foo.js') + ); + assert.equal( + resolved.path, + path.join( + rootDir, + 'node_modules', + 'source-alias-glob', + 'src', + 'test.js' + ) + ); + assert(resolved.pkg.source); + }); + }); + describe('error handling', function() { it('should throw when a relative path cannot be resolved', async function() { let threw = false;