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

Handle module side effects #219

Merged
merged 8 commits into from May 15, 2019
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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';