Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] Support source field in package.json to enable babel on symlinked modules #1101

Merged
merged 3 commits into from
May 1, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 49 additions & 14 deletions src/Resolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)};
}

Expand Down Expand Up @@ -213,16 +215,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 === './') {
Expand Down Expand Up @@ -269,16 +285,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) {
Expand All @@ -295,7 +312,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];
Expand Down Expand Up @@ -325,6 +342,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;
Expand Down
24 changes: 17 additions & 7 deletions src/transforms/babel.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -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)) {
Expand All @@ -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);
}

Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;
}

Expand Down
1 change: 1 addition & 0 deletions src/utils/fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": "../.eslintrc.json",
"parserOptions": {
"sourceType": "module"
}
}
4 changes: 4 additions & 0 deletions test/integration/babel-node-modules-source-unlinked/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import Foo from 'foo';

export {Foo};
export class Bar {}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "parcel-test-browser-browserslist",
"browserslist": ["last 2 Chrome versions", "IE >= 11"]
}
6 changes: 6 additions & 0 deletions test/integration/babel-node-modules-source/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": "../.eslintrc.json",
"parserOptions": {
"sourceType": "module"
}
}
4 changes: 4 additions & 0 deletions test/integration/babel-node-modules-source/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import Foo from 'foo';

export {Foo};
export class Bar {}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions test/integration/babel-node-modules-source/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "parcel-test-browser-browserslist",
"browserslist": ["last 2 Chrome versions", "IE >= 11"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default class Foo {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "foo",
"source": true
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Empty file.
1 change: 1 addition & 0 deletions test/integration/resolver/node_modules/source

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions test/integration/resolver/node_modules/source-alias

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions test/integration/resolver/node_modules/source-alias-glob

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Empty file.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "source",
"main": "lib/test.js",
"source": {
"./lib/*": "./src/$1"
}
}
Empty file.
Empty file.
Empty file.
6 changes: 6 additions & 0 deletions test/integration/resolver/packages/source-alias/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "source-alias",
"source": {
"./dist": "./source"
}
}
Empty file.
Empty file.
5 changes: 5 additions & 0 deletions test/integration/resolver/packages/source/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "source",
"main": "dist.js",
"source": "source.js"
}
Empty file.
18 changes: 18 additions & 0 deletions test/javascript.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down
74 changes: 74 additions & 0 deletions test/resolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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;
Expand Down