Skip to content

Commit d517132

Browse files
authored
Support source field in package.json to enable babel on symlinked modules (#1101)
1 parent 2af3fe3 commit d517132

33 files changed

Lines changed: 230 additions & 21 deletions

File tree

src/Resolver.js

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ const builtins = require('./builtins');
22
const path = require('path');
33
const glob = require('glob');
44
const fs = require('./utils/fs');
5+
const micromatch = require('micromatch');
56

67
const EMPTY_SHIM = require.resolve('./builtins/_empty');
8+
const GLOB_RE = /[*+{}]/;
79

810
/**
911
* This resolver implements a modified version of the node_modules resolution algorithm:
@@ -36,7 +38,7 @@ class Resolver {
3638
}
3739

3840
// Check if this is a glob
39-
if (/[*+{}]/.test(filename) && glob.hasMagic(filename)) {
41+
if (GLOB_RE.test(filename) && glob.hasMagic(filename)) {
4042
return {path: path.resolve(path.dirname(parent), filename)};
4143
}
4244

@@ -251,16 +253,30 @@ class Resolver {
251253
pkg.pkgfile = file;
252254
pkg.pkgdir = dir;
253255

256+
// If the package has a `source` field, check if it is behind a symlink.
257+
// If so, we treat the module as source code rather than a pre-compiled module.
258+
if (pkg.source) {
259+
let realpath = await fs.realpath(file);
260+
if (realpath === file) {
261+
delete pkg.source;
262+
}
263+
}
264+
254265
this.packageCache.set(file, pkg);
255266
return pkg;
256267
}
257268

258269
getPackageMain(pkg) {
259270
// libraries like d3.js specifies node.js specific files in the "main" which breaks the build
260-
// we use the "module" or "jsnext:main" field to get the full dependency tree if available
261-
let main = [pkg.module, pkg['jsnext:main'], pkg.browser, pkg.main].find(
262-
entry => typeof entry === 'string'
263-
);
271+
// we use the "module" or "jsnext:main" field to get the full dependency tree if available.
272+
// If this is a linked module with a `source` field, use that as the entry point.
273+
let main = [
274+
pkg.source,
275+
pkg.module,
276+
pkg['jsnext:main'],
277+
pkg.browser,
278+
pkg.main
279+
].find(entry => typeof entry === 'string');
264280

265281
// Default to index file if no main field find
266282
if (!main || main === '.' || main === './') {
@@ -307,16 +323,17 @@ class Resolver {
307323
}
308324

309325
resolvePackageAliases(filename, pkg) {
310-
// Resolve aliases in the package.alias and package.browser fields.
311-
if (pkg) {
312-
return (
313-
this.getAlias(filename, pkg.pkgdir, pkg.alias) ||
314-
this.getAlias(filename, pkg.pkgdir, pkg.browser) ||
315-
filename
316-
);
326+
if (!pkg) {
327+
return filename;
317328
}
318329

319-
return filename;
330+
// Resolve aliases in the package.source, package.alias, and package.browser fields.
331+
return (
332+
this.getAlias(filename, pkg.pkgdir, pkg.source) ||
333+
this.getAlias(filename, pkg.pkgdir, pkg.alias) ||
334+
this.getAlias(filename, pkg.pkgdir, pkg.browser) ||
335+
filename
336+
);
320337
}
321338

322339
getAlias(filename, dir, aliases) {
@@ -333,7 +350,7 @@ class Resolver {
333350
filename = './' + filename;
334351
}
335352

336-
alias = aliases[filename];
353+
alias = this.lookupAlias(aliases, filename);
337354
} else {
338355
// It is a node_module. First try the entire filename as a key.
339356
alias = aliases[filename];
@@ -363,6 +380,24 @@ class Resolver {
363380
return alias;
364381
}
365382

383+
lookupAlias(aliases, filename) {
384+
// First, try looking up the exact filename
385+
let alias = aliases[filename];
386+
if (alias != null) {
387+
return alias;
388+
}
389+
390+
// Otherwise, try replacing glob keys
391+
for (let key in aliases) {
392+
if (GLOB_RE.test(key)) {
393+
let re = micromatch.makeRe(key, {capture: true});
394+
if (re.test(filename)) {
395+
return filename.replace(re, aliases[key]);
396+
}
397+
}
398+
}
399+
}
400+
366401
async findPackage(dir) {
367402
// Find the nearest package.json file within the current node_modules folder
368403
let root = path.parse(dir).root;

src/transforms/babel.js

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,18 @@ async function getBabelConfig(asset) {
8989
return asset.babelConfig;
9090
}
9191

92-
let babelrc = await getBabelRc(asset);
93-
let envConfig = await getEnvConfig(asset, !!babelrc);
94-
let jsxConfig = getJSXConfig(asset, !!babelrc);
92+
// Consider the module source code rather than precompiled if the resolver
93+
// used the `source` field, or it is not in node_modules.
94+
let isSource =
95+
!!(asset.package && asset.package.source) ||
96+
!asset.name.includes(NODE_MODULES);
97+
98+
// Try to resolve a .babelrc file. If one is found, consider the module source code.
99+
let babelrc = await getBabelRc(asset, isSource);
100+
isSource = isSource || !!babelrc;
101+
102+
let envConfig = await getEnvConfig(asset, isSource);
103+
let jsxConfig = getJSXConfig(asset, isSource);
95104

96105
// Merge the babel-preset-env config and the babelrc if needed
97106
if (babelrc && !shouldIgnoreBabelrc(asset.name, babelrc)) {
@@ -162,8 +171,9 @@ function getPluginName(p) {
162171
* Finds a .babelrc for an asset. By default, .babelrc files inside node_modules are not used.
163172
* However, there are some exceptions:
164173
* - if `browserify.transforms` includes "babelify" in package.json (for legacy module compat)
174+
* - the `source` field in package.json is used by the resolver
165175
*/
166-
async function getBabelRc(asset) {
176+
async function getBabelRc(asset, isSource) {
167177
// Support legacy browserify packages
168178
let browserify = asset.package && asset.package.browserify;
169179
if (browserify && Array.isArray(browserify.transform)) {
@@ -182,7 +192,7 @@ async function getBabelRc(asset) {
182192
}
183193

184194
// If this asset is not in node_modules, always use the .babelrc
185-
if (!asset.name.includes(NODE_MODULES)) {
195+
if (isSource) {
186196
return await findBabelRc(asset);
187197
}
188198

@@ -224,7 +234,7 @@ async function getEnvConfig(asset, isSourceModule) {
224234

225235
// If this is the app module, the source and target will be the same, so just compile everything.
226236
// Otherwise, load the source engines and generate a babel-present-env config.
227-
if (asset.name.includes(NODE_MODULES) && !isSourceModule) {
237+
if (!isSourceModule) {
228238
let sourceEngines = await getTargetEngines(asset, false);
229239
let sourceEnv = (await getEnvPlugins(sourceEngines, false)) || targetEnv;
230240

@@ -264,7 +274,7 @@ async function getEnvPlugins(targets, useBuiltIns = false) {
264274
*/
265275
function getJSXConfig(asset, isSourceModule) {
266276
// Don't enable JSX in node_modules
267-
if (asset.name.includes(NODE_MODULES) && !isSourceModule) {
277+
if (!isSourceModule) {
268278
return null;
269279
}
270280

src/utils/fs.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ exports.writeFile = promisify(fs.writeFile);
77
exports.stat = promisify(fs.stat);
88
exports.readdir = promisify(fs.readdir);
99
exports.unlink = promisify(fs.unlink);
10+
exports.realpath = promisify(fs.realpath);
1011

1112
exports.exists = function(filename) {
1213
return new Promise(resolve => {
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"extends": "../.eslintrc.json",
3+
"parserOptions": {
4+
"sourceType": "module"
5+
}
6+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import Foo from 'foo';
2+
3+
export {Foo};
4+
export class Bar {}

test/integration/babel-node-modules-source-unlinked/node_modules/foo/index.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/integration/babel-node-modules-source-unlinked/node_modules/foo/package.json

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"name": "parcel-test-browser-browserslist",
3+
"browserslist": ["last 2 Chrome versions", "IE >= 11"]
4+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"extends": "../.eslintrc.json",
3+
"parserOptions": {
4+
"sourceType": "module"
5+
}
6+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import Foo from 'foo';
2+
3+
export {Foo};
4+
export class Bar {}

0 commit comments

Comments
 (0)