Skip to content
This repository has been archived by the owner on Aug 4, 2021. It is now read-only.

Commit

Permalink
Handle module side effects (#219)
Browse files Browse the repository at this point in the history
* Handle module side-effects specified as an array of patterns

* Refine code ordering and variable names

* Speed up builtins lookup by converting them to a Set

* Fix minimum npm version in Travis

* Do not ignore scripts, add Node 12

* Or ignore scripts again

* Also cache mutated package.json information

* Use new dependencies and require at least node 1.11.0
  • Loading branch information
lukastaegert committed May 15, 2019
1 parent ae49cb0 commit f5e57a3
Show file tree
Hide file tree
Showing 22 changed files with 337 additions and 283 deletions.
4 changes: 4 additions & 0 deletions .travis.yml
Expand Up @@ -3,6 +3,10 @@ node_js:
- "6"
- "8"
- "10"
- "12"
env:
global:
- BUILD_TIMEOUT=10000
install: npm ci --ignore-scripts
before_install:
- if [[ $TRAVIS_NODE_VERSION -lt 8 ]]; then npm install --global npm@5; fi
358 changes: 131 additions & 227 deletions package-lock.json

Large diffs are not rendered by default.

13 changes: 9 additions & 4 deletions package.json
Expand Up @@ -4,14 +4,15 @@
"version": "4.2.4",
"devDependencies": {
"@babel/core": "7.4.4",
"@babel/register": "^7.4.4",
"@babel/preset-env": "^7.4.4",
"@babel/register": "^7.4.4",
"es5-ext": "^0.10.50",
"eslint": "^5.16.0",
"mocha": "^6.1.4",
"rollup": "^1.11.3",
"rollup": "^1.12.0",
"rollup-plugin-babel": "^4.3.2",
"rollup-plugin-commonjs": "^9.3.4",
"rollup-plugin-commonjs": "^10.0.0",
"rollup-plugin-json": "^4.0.0",
"string-capitalize": "^1.0.1",
"typescript": "^3.4.5"
},
Expand All @@ -36,7 +37,11 @@
"@types/resolve": "0.0.8",
"builtin-modules": "^3.1.0",
"is-module": "^1.0.0",
"resolve": "^1.10.1"
"resolve": "^1.10.1",
"rollup-pluginutils": "^2.7.0"
},
"peerDependencies": {
"rollup": ">=1.11.0"
},
"repository": "rollup/rollup-plugin-node-resolve",
"keywords": [
Expand Down
21 changes: 13 additions & 8 deletions rollup.config.js
@@ -1,15 +1,20 @@
import babel from 'rollup-plugin-babel';
import json from 'rollup-plugin-json';

export default {
input: 'src/index.js',
plugins: [ babel({
presets: [['@babel/preset-env', {
targets: {
node: 6
}
}]]
}) ],
external: [ 'path', 'fs', 'builtin-modules', 'resolve', 'browser-resolve', 'is-module' ],
plugins: [
json(),
babel({
presets: [['@babel/preset-env', {
targets: {
node: 6
}
}]
]
})
],
external: [ 'path', 'fs', 'builtin-modules', 'resolve', 'browser-resolve', 'is-module', 'rollup-pluginutils' ],
output: [
{ file: 'dist/rollup-plugin-node-resolve.cjs.js', format: 'cjs' },
{ file: 'dist/rollup-plugin-node-resolve.es.js', format: 'es' }
Expand Down
136 changes: 92 additions & 44 deletions src/index.js
@@ -1,16 +1,22 @@
import {dirname, extname, join, normalize, resolve, sep} from 'path';
import builtins from 'builtin-modules';
import builtinList from 'builtin-modules';
import resolveId from 'resolve';
import isModule from 'is-module';
import fs from 'fs';
import {createFilter} from 'rollup-pluginutils';
import {peerDependencies} from '../package.json';

const builtins = builtinList.reduce((set, id) => set.add(id), new Set());

const ES6_BROWSER_EMPTY = resolve( __dirname, '../src/empty.js' );
// It is important that .mjs occur before .js so that Rollup will interpret npm modules
// which deploy both ESM .mjs and CommonJS .js files as ESM.
const DEFAULT_EXTS = [ '.mjs', '.js', '.json', '.node' ];

const readFileAsync = file => new Promise((fulfil, reject) => fs.readFile(file, (err, contents) => err ? reject(err) : fulfil(contents)));

const statAsync = file => new Promise((fulfil, reject) => fs.stat(file, (err, contents) => err ? reject(err) : fulfil(contents)));

const cache = fn => {
const cache = new Map();
const wrapped = (param, done) => {
Expand All @@ -25,12 +31,16 @@ const cache = fn => {
wrapped.clear = () => cache.clear();
return wrapped;
};

const ignoreENOENT = err => {
if (err.code === 'ENOENT') return false;
throw err;
};

const readFileCached = cache(readFileAsync);

const isDirCached = cache(file => statAsync(file).then(stat => stat.isDirectory(), ignoreENOENT));

const isFileCached = cache(file => statAsync(file).then(stat => stat.isFile(), ignoreENOENT));

function getMainFields (options) {
Expand Down Expand Up @@ -63,6 +73,8 @@ function getMainFields (options) {
return mainFields;
}

const alwaysNull = () => null;

const resolveIdAsync = (file, opts) => new Promise((fulfil, reject) => resolveId(file, opts, (err, contents) => err ? reject(err) : fulfil(contents)));

export default function nodeResolve ( options = {} ) {
Expand All @@ -85,13 +97,79 @@ export default function nodeResolve ( options = {} ) {
throw new Error( 'options.skip is no longer supported — you should use the main Rollup `external` option instead' );
}

const extensions = options.extensions || DEFAULT_EXTS;
const packageInfoCache = new Map();

function getCachedPackageInfo (pkg, pkgPath) {
if (packageInfoCache.has(pkgPath)) {
return packageInfoCache.get(pkgPath);
}
const pkgRoot = dirname( pkgPath );

let overriddenMain = false;
for ( let i = 0; i < mainFields.length; i++ ) {
const field = mainFields[i];
if ( typeof pkg[ field ] === 'string' ) {
pkg[ 'main' ] = pkg[ field ];
overriddenMain = true;
break;
}
}

const packageInfo = {
cachedPkg: pkg,
hasModuleSideEffects: alwaysNull,
hasPackageEntry: overriddenMain !== false || mainFields.indexOf( 'main' ) !== -1,
packageBrowserField: useBrowserOverrides && typeof pkg[ 'browser' ] === 'object' &&
Object.keys(pkg[ 'browser' ]).reduce((browser, key) => {
let resolved = pkg[ 'browser' ][ key ];
if (resolved && resolved[0] === '.') {
resolved = resolve( pkgRoot, resolved );
}
browser[ key ] = resolved;
if ( key[0] === '.' ) {
const absoluteKey = resolve( pkgRoot, key );
browser[ absoluteKey ] = resolved;
if ( !extname(key) ) {
extensions.reduce( ( browser, ext ) => {
browser[ absoluteKey + ext ] = browser[ key ];
return browser;
}, browser );
}
}
return browser;
}, {})
};

const packageSideEffects = pkg['sideEffects'];
if (typeof packageSideEffects === 'boolean') {
packageInfo.hasModuleSideEffects = () => packageSideEffects;
} else if (Array.isArray(packageSideEffects)) {
const filter = createFilter(packageSideEffects, null, {resolve: pkgRoot});
packageInfo.hasModuleSideEffects = id => !filter(id);
}

packageInfoCache.set(pkgPath, packageInfo);
return packageInfo;
}

let preserveSymlinks;

return {
name: 'node-resolve',

options ( options ) {
preserveSymlinks = options.preserveSymlinks;
const [major, minor] = this.meta.rollupVersion.split('.').map(Number);
const minVersion = peerDependencies.rollup.slice(2);
const [minMajor, minMinor] = minVersion.split('.').map(Number);
if (major < minMajor || (major === minMajor && minor < minMinor)) {
this.error(
`Insufficient Rollup version: "rollup-plugin-node-resolve" requires at least rollup@${minVersion} but found rollup@${
this.meta.rollupVersion
}.`
);
}
},

generateBundle () {
Expand Down Expand Up @@ -134,48 +212,17 @@ export default function nodeResolve ( options = {} ) {

if (only && !only.some(pattern => pattern.test(id))) return null;

let disregardResult = false;
let hasModuleSideEffects = alwaysNull;
let hasPackageEntry = true;
let packageBrowserField = false;
const extensions = options.extensions || DEFAULT_EXTS;

const resolveOptions = {
basedir,
packageFilter ( pkg, pkgPath ) {
const pkgRoot = dirname( pkgPath );
if (useBrowserOverrides && typeof pkg[ 'browser' ] === 'object') {
packageBrowserField = Object.keys(pkg[ 'browser' ]).reduce((browser, key) => {
let resolved = pkg[ 'browser' ][ key ];
if (resolved && resolved[0] === '.') {
resolved = resolve( pkgRoot, pkg[ 'browser' ][ key ] );
}
browser[ key ] = resolved;
if ( key[0] === '.' ) {
const absoluteKey = resolve( pkgRoot, key );
browser[ absoluteKey ] = resolved;
if ( !extname(key) ) {
extensions.reduce( ( browser, ext ) => {
browser[ absoluteKey + ext ] = browser[ key ];
return browser;
}, browser );
}
}
return browser;
}, {});
}

let overriddenMain = false;
for ( let i = 0; i < mainFields.length; i++ ) {
const field = mainFields[i];
if ( typeof pkg[ field ] === 'string' ) {
pkg[ 'main' ] = pkg[ field ];
overriddenMain = true;
break;
}
}
if ( overriddenMain === false && mainFields.indexOf( 'main' ) === -1 ) {
disregardResult = true;
}
return pkg;
let cachedPkg;
({cachedPkg, hasModuleSideEffects, hasPackageEntry, packageBrowserField} =
getCachedPackageInfo(pkg, pkgPath));
return cachedPkg;
},
readFile: readFileCached,
isFile: isFileCached,
Expand All @@ -192,7 +239,7 @@ export default function nodeResolve ( options = {} ) {
Object.assign( resolveOptions, customResolveOptions )
)
.then(resolved => {
if ( resolved && useBrowserOverrides && packageBrowserField ) {
if ( resolved && packageBrowserField ) {
if ( packageBrowserField.hasOwnProperty(resolved) ) {
if (!packageBrowserField[resolved]) {
browserMapCache[resolved] = packageBrowserField;
Expand All @@ -203,14 +250,14 @@ export default function nodeResolve ( options = {} ) {
browserMapCache[resolved] = packageBrowserField;
}

if ( !disregardResult ) {
if ( hasPackageEntry ) {
if ( !preserveSymlinks && resolved && fs.existsSync( resolved ) ) {
resolved = fs.realpathSync( resolved );
}

if ( ~builtins.indexOf( resolved ) ) {
if ( builtins.has( resolved ) ) {
return null;
} else if ( ~builtins.indexOf( importee ) && preferBuiltins ) {
} else if ( builtins.has( importee ) && preferBuiltins ) {
if ( !isPreferBuiltinsSet ) {
this.warn(
`preferring built-in module '${importee}' over local alternative ` +
Expand All @@ -225,9 +272,10 @@ export default function nodeResolve ( options = {} ) {
}

if ( resolved && options.modulesOnly ) {
return readFileAsync( resolved, 'utf-8').then(code => isModule( code ) ? resolved : null);
return readFileAsync( resolved, 'utf-8')
.then(code => isModule( code ) ? {id: resolved, moduleSideEffects: hasModuleSideEffects(resolved)} : null);
} else {
return resolved;
return {id: resolved, moduleSideEffects: hasModuleSideEffects(resolved)};
}
})
.catch(() => null);
Expand Down
4 changes: 4 additions & 0 deletions test/node_modules/side-effects-array/dep1.js

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

4 changes: 4 additions & 0 deletions test/node_modules/side-effects-array/dep2.js

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

4 changes: 4 additions & 0 deletions test/node_modules/side-effects-array/dep3-free.js

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

8 changes: 8 additions & 0 deletions test/node_modules/side-effects-array/index.js

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

4 changes: 4 additions & 0 deletions test/node_modules/side-effects-array/nested/dep4.js

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

4 changes: 4 additions & 0 deletions test/node_modules/side-effects-array/nested/dep5-free.js

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

7 changes: 7 additions & 0 deletions test/node_modules/side-effects-array/package.json

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

4 changes: 4 additions & 0 deletions test/node_modules/side-effects-false/dep1.js

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

4 changes: 4 additions & 0 deletions test/node_modules/side-effects-false/dep2.js

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

5 changes: 5 additions & 0 deletions test/node_modules/side-effects-false/index.js

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

4 changes: 4 additions & 0 deletions test/node_modules/side-effects-false/package.json

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

4 changes: 4 additions & 0 deletions test/node_modules/side-effects-true/dep1.js

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

4 changes: 4 additions & 0 deletions test/node_modules/side-effects-true/dep2.js

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

5 changes: 5 additions & 0 deletions test/node_modules/side-effects-true/index.js

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

4 changes: 4 additions & 0 deletions test/node_modules/side-effects-true/package.json

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

3 changes: 3 additions & 0 deletions test/samples/side-effects/main.js
@@ -0,0 +1,3 @@
export {value1 as falseValue} from 'side-effects-false';
export {value1 as trueValue} from 'side-effects-true';
import 'side-effects-array';

0 comments on commit f5e57a3

Please sign in to comment.