From cf604ffa85da0c86423d9a35f483b662f0cc381c Mon Sep 17 00:00:00 2001 From: Brian Terlson Date: Tue, 19 Nov 2019 13:01:13 -0800 Subject: [PATCH 1/5] Add getPackageInfoForId API --- src/index.js | 58 +++++++++++++++++++++++----- test/test.js | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 153 insertions(+), 10 deletions(-) diff --git a/src/index.js b/src/index.js index 4495ea0..17d08cc 100644 --- a/src/index.js +++ b/src/index.js @@ -2,7 +2,7 @@ import {dirname, extname, join, normalize, resolve, sep} from 'path'; import builtinList from 'builtin-modules'; import resolveId from 'resolve'; import isModule from 'is-module'; -import fs from 'fs'; +import fs, { realpathSync } from 'fs'; import {createFilter} from 'rollup-pluginutils'; import {peerDependencies} from '../package.json'; @@ -129,6 +129,7 @@ export default function nodeResolve ( options = {} ) { const extensions = options.extensions || DEFAULT_EXTS; const packageInfoCache = new Map(); + const idToPackageInfo = new Map(); const shouldDedupe = typeof dedupe === 'function' ? dedupe @@ -140,17 +141,39 @@ export default function nodeResolve ( options = {} ) { } const pkgRoot = dirname( pkgPath ); + const packageInfo = { + // copy as we are about to munge the `main` field of `pkg`. + packageJson: Object.assign({}, pkg), + + // resolve doesn't handle preserveSymlinks for us in this callback + packageJsonPath: preserveSymlinks ? pkgPath : realpathSync(pkgPath), + + // directory containing the package.json + root: preserveSymlinks ? pkgPath : realpathSync(pkgRoot), + + // which main field was used during resolution of this module (main, module, or browser) + resolvedMainField: 'main', + + // whether the browser map was used to resolve the entry point to this module + browserMappedMain: false, + + // the entry point of the module with respect to the selected main field and any + // relevant browser mappings. + resolvedEntryPoint: '' + }; + let overriddenMain = false; for ( let i = 0; i < mainFields.length; i++ ) { const field = mainFields[i]; if ( typeof pkg[ field ] === 'string' ) { pkg[ 'main' ] = pkg[ field ]; + packageInfo.resolvedMainField = field; overriddenMain = true; break; } } - const packageInfo = { + const internalPackageInfo = { cachedPkg: pkg, hasModuleSideEffects: alwaysNull, hasPackageEntry: overriddenMain !== false || mainFields.indexOf( 'main' ) !== -1, @@ -172,18 +195,28 @@ export default function nodeResolve ( options = {} ) { } } return browser; - }, {}) + }, {}), + packageInfo }; + const browserMap = internalPackageInfo.packageBrowserField; + if (useBrowserOverrides && typeof pkg['browser'] === 'object' && browserMap.hasOwnProperty(pkg.main)) { + packageInfo.resolvedEntryPoint = browserMap[pkg.main]; + packageInfo.browserMappedMain = true; + } else { + packageInfo.resolvedEntryPoint = resolve(pkgRoot, pkg.main); + packageInfo.browserMappedMain = false; + } + const packageSideEffects = pkg['sideEffects']; if (typeof packageSideEffects === 'boolean') { - packageInfo.hasModuleSideEffects = () => packageSideEffects; + internalPackageInfo.hasModuleSideEffects = () => packageSideEffects; } else if (Array.isArray(packageSideEffects)) { - packageInfo.hasModuleSideEffects = createFilter(packageSideEffects, null, {resolve: pkgRoot}); + internalPackageInfo.hasModuleSideEffects = createFilter(packageSideEffects, null, {resolve: pkgRoot}); } - packageInfoCache.set(pkgPath, packageInfo); - return packageInfo; + packageInfoCache.set(pkgPath, internalPackageInfo); + return internalPackageInfo; } let preserveSymlinks; @@ -253,13 +286,15 @@ export default function nodeResolve ( options = {} ) { let hasModuleSideEffects = alwaysNull; let hasPackageEntry = true; let packageBrowserField = false; + let packageInfo = undefined; const resolveOptions = { basedir, packageFilter ( pkg, pkgPath ) { let cachedPkg; - ({cachedPkg, hasModuleSideEffects, hasPackageEntry, packageBrowserField} = + ({packageInfo, cachedPkg, hasModuleSideEffects, hasPackageEntry, packageBrowserField} = getCachedPackageInfo(pkg, pkgPath)); + return cachedPkg; }, readFile: readFileCached, @@ -297,7 +332,6 @@ export default function nodeResolve ( options = {} ) { } importSpecifierList.push(importee); - return resolveImportSpecifiers( importSpecifierList, Object.assign(resolveOptions, customResolveOptions) @@ -321,6 +355,8 @@ export default function nodeResolve ( options = {} ) { return resolved; }) .then(resolved => { + idToPackageInfo.set(resolved, packageInfo); + if ( hasPackageEntry ) { if (builtins.has(resolved) && preferBuiltins && isPreferBuiltinsSet) { return null; @@ -354,5 +390,9 @@ export default function nodeResolve ( options = {} ) { } return null; }, + + getPackageInfoForId (id) { + return idToPackageInfo.get(id); + } }; } diff --git a/test/test.js b/test/test.js index c4610f2..5fc34f4 100644 --- a/test/test.js +++ b/test/test.js @@ -910,6 +910,7 @@ describe( 'rollup-plugin-node-resolve', function () { }); }); + it('handles package side-effects', () => rollup.rollup({ input: 'samples/side-effects/main.js', @@ -926,5 +927,107 @@ describe( 'rollup-plugin-node-resolve', function () { 'array-index' ]); delete global.sideEffects; - })); + }) + ); + + describe.only('getPackageInfoForId', () => { + it('populates info for main', () => { + const resolve = nodeResolve({ + mainFields: ['main'] + }); + + let entriesInfo; + + return rollup.rollup({ + input: 'samples/prefer-main/main.js', + plugins: [ + resolve, + { + transform (code, id) { + if (!id.match(/main-entry.js$/)) return; + entriesInfo = resolve.getPackageInfoForId(id); + return code; + } + } + ] + }).then(() => { + const entriesPkgJsonPath = path.resolve(__dirname, './node_modules/entries/package.json'); + const root = path.dirname(entriesPkgJsonPath); + assert.equal(entriesInfo.browserMappedMain, false); + assert.equal(entriesInfo.resolvedMainField, 'main'); + assert.deepEqual(entriesInfo.packageJson, require(entriesPkgJsonPath)); + assert.equal(entriesInfo.packageJsonPath, entriesPkgJsonPath); + assert.equal(entriesInfo.root, root); + assert.equal(entriesInfo.resolvedEntryPoint, path.resolve(root, './main-entry.js')); + }); + }); + + it('populates info for module', () => { + const resolve = nodeResolve({ + mainFields: ['module'] + }); + + let entriesInfo; + + return rollup.rollup({ + input: 'samples/prefer-main/main.js', + plugins: [ + resolve, + { + transform (code, id) { + if (!id.match(/module-entry.js$/)) return; + entriesInfo = resolve.getPackageInfoForId(id); + return code; + } + } + ] + }).then(() => { + const entriesPkgJsonPath = path.resolve(__dirname, './node_modules/entries/package.json'); + const root = path.dirname(entriesPkgJsonPath); + assert.equal(entriesInfo.browserMappedMain, false); + assert.equal(entriesInfo.resolvedMainField, 'module'); + assert.deepEqual(entriesInfo.packageJson, require(entriesPkgJsonPath)); + assert.equal(entriesInfo.packageJsonPath, entriesPkgJsonPath); + assert.equal(entriesInfo.root, root); + assert.equal(entriesInfo.resolvedEntryPoint, path.resolve(root, './module-entry.js')); + }); + }); + + it.only('populates info for browser', () => { + const resolve = nodeResolve({ + mainFields: ['browser'] + }); + + const entriesInfoMap = new Map(); + + return rollup.rollup({ + input: 'samples/browser-object/main.js', + plugins: [ + resolve, + { + transform (code, id) { + if (!id.match(/isomorphic-object/)) return; + entriesInfoMap.set(id, resolve.getPackageInfoForId(id)); + return code; + } + } + ] + }).then(() => { + const entriesPkgJsonPath = path.resolve(__dirname, './node_modules/isomorphic-object/package.json'); + const root = path.dirname(entriesPkgJsonPath); + const expectedPkgJson = require(entriesPkgJsonPath); + + for (const entriesInfo of entriesInfoMap.values()) { + assert.equal(entriesInfo.browserMappedMain, true); + assert.equal(entriesInfo.resolvedMainField, 'main'); + assert.deepEqual(entriesInfo.packageJson, expectedPkgJson); + assert.equal(entriesInfo.packageJsonPath, entriesPkgJsonPath); + assert.equal(entriesInfo.root, root); + assert.equal(entriesInfo.resolvedEntryPoint, path.resolve(root, './browser.js')); + } + + }); + }); + + }); }); From 67fcf5efb6963576928ff399dc1971b2548c8552 Mon Sep 17 00:00:00 2001 From: Brian Terlson Date: Wed, 20 Nov 2019 11:36:21 -0800 Subject: [PATCH 2/5] fix up tests --- test/test.js | 48 +++++++++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/test/test.js b/test/test.js index 5fc34f4..68b170e 100644 --- a/test/test.js +++ b/test/test.js @@ -930,7 +930,7 @@ describe( 'rollup-plugin-node-resolve', function () { }) ); - describe.only('getPackageInfoForId', () => { + describe('getPackageInfoForId', () => { it('populates info for main', () => { const resolve = nodeResolve({ mainFields: ['main'] @@ -953,12 +953,14 @@ describe( 'rollup-plugin-node-resolve', function () { }).then(() => { const entriesPkgJsonPath = path.resolve(__dirname, './node_modules/entries/package.json'); const root = path.dirname(entriesPkgJsonPath); - assert.equal(entriesInfo.browserMappedMain, false); - assert.equal(entriesInfo.resolvedMainField, 'main'); - assert.deepEqual(entriesInfo.packageJson, require(entriesPkgJsonPath)); - assert.equal(entriesInfo.packageJsonPath, entriesPkgJsonPath); - assert.equal(entriesInfo.root, root); - assert.equal(entriesInfo.resolvedEntryPoint, path.resolve(root, './main-entry.js')); + assert.deepStrictEqual(entriesInfo, { + browserMappedMain: false, + resolvedMainField: 'main', + packageJson: require(entriesPkgJsonPath), + packageJsonPath: entriesPkgJsonPath, + root, + resolvedEntryPoint: path.resolve(root, './main-entry.js') + }); }); }); @@ -984,16 +986,19 @@ describe( 'rollup-plugin-node-resolve', function () { }).then(() => { const entriesPkgJsonPath = path.resolve(__dirname, './node_modules/entries/package.json'); const root = path.dirname(entriesPkgJsonPath); - assert.equal(entriesInfo.browserMappedMain, false); - assert.equal(entriesInfo.resolvedMainField, 'module'); - assert.deepEqual(entriesInfo.packageJson, require(entriesPkgJsonPath)); - assert.equal(entriesInfo.packageJsonPath, entriesPkgJsonPath); - assert.equal(entriesInfo.root, root); - assert.equal(entriesInfo.resolvedEntryPoint, path.resolve(root, './module-entry.js')); + + assert.deepStrictEqual(entriesInfo, { + browserMappedMain: false, + resolvedMainField: 'module', + packageJson: require(entriesPkgJsonPath), + packageJsonPath: entriesPkgJsonPath, + root, + resolvedEntryPoint: path.resolve(root, './module-entry.js') + }); }); }); - it.only('populates info for browser', () => { + it('populates info for browser', () => { const resolve = nodeResolve({ mainFields: ['browser'] }); @@ -1018,14 +1023,15 @@ describe( 'rollup-plugin-node-resolve', function () { const expectedPkgJson = require(entriesPkgJsonPath); for (const entriesInfo of entriesInfoMap.values()) { - assert.equal(entriesInfo.browserMappedMain, true); - assert.equal(entriesInfo.resolvedMainField, 'main'); - assert.deepEqual(entriesInfo.packageJson, expectedPkgJson); - assert.equal(entriesInfo.packageJsonPath, entriesPkgJsonPath); - assert.equal(entriesInfo.root, root); - assert.equal(entriesInfo.resolvedEntryPoint, path.resolve(root, './browser.js')); + assert.deepStrictEqual(entriesInfo, { + browserMappedMain: true, + resolvedMainField: 'main', + packageJson: expectedPkgJson, + packageJsonPath: entriesPkgJsonPath, + root, + resolvedEntryPoint: path.resolve(root, './browser.js') + }); } - }); }); From 33b84ec500637619f0bf243005a8450d518e413d Mon Sep 17 00:00:00 2001 From: Brian Terlson Date: Wed, 20 Nov 2019 12:35:41 -0800 Subject: [PATCH 3/5] Take a stab at docs --- README.md | 77 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/README.md b/README.md index d0e0123..5e0f013 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,83 @@ export default ({ }) ``` +## Additional Plugin APIs + +In addition to the standard hooks used by Rollup, this plugin exposes additional functionality useful for other plugins. + +## getPackageInfoForId (moduleId: string) => PackageInfo + +Returns an object with metadata about the package containing the specified module. PackageInfo has the following fields: + +* **packageJson**: The package.json file for the package +* **packageJsonPath**: The path to the package.json file +* **root**: The root directory of the package +* **resolvedMainField**: Which main field was used during resolution (see the mainFields option) +* **browserMappedMain**: Whether the browser map was used to resolve the module's entry point +* **resolvedEntrypoint**: The resolved entry point to the module with respect to the mainFields configuration and browser mappings. + +This object is populated during the `resolve` hook, so plugins should only depend on this information being present in hooks that run after `resolve`. + + +## Usage from Other Plugins + +`getPackageInfoForId` is exposed as a method on the plugin object along side the other hooks expected of a Rollup plugin. + +```js +import resolve from 'rollup-plugin-node-resolve'; +const resolve = resolve(); + +export default ({ + input: ..., + plugins: [ + resolve(), + // custom plugin + { + transform(code, id) { + // get package info for this module id + const info = resolve.getPackageInfoForId(id); + + // if it's the buffer shim, return nothing. + if (info.packageJson.name === 'buffer') { + return ''; + } + + return code; + } + } + ], + output: ... +}) +``` + +If you're writing a standalone plugin, you can get access to the plugin object by pulling it out of the config provided to the `buildStart` hook: + +```js + +export default function { + let nodeResolvePlugin; + + function getPackageInfoForId(id) { + // user config isn't using this plugin + if (!nodeResolvePlugin) return; + + // user config has an older version without this API + if (!nodeResolvePlugin.getPackageInfoForId) return; + + return nodeResolvePlugin.getPackageInfoForId(id); + } + + return { + buildStart (options) { + nodeResolvePlugin = options.plugins && options.plugins.filter(p => p.name === 'node-resolve')[0]; + }, + transform (code, id) { + const info = getPackageInfoForId(id); + // ... + } + } +} +``` ## License From 50d5ead7975550d5a98b059b90fc0893a5f965eb Mon Sep 17 00:00:00 2001 From: Brian Terlson Date: Wed, 20 Nov 2019 12:41:44 -0800 Subject: [PATCH 4/5] handle missing main entries --- src/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index 17d08cc..c41a4e6 100644 --- a/src/index.js +++ b/src/index.js @@ -204,7 +204,8 @@ export default function nodeResolve ( options = {} ) { packageInfo.resolvedEntryPoint = browserMap[pkg.main]; packageInfo.browserMappedMain = true; } else { - packageInfo.resolvedEntryPoint = resolve(pkgRoot, pkg.main); + // index.node is technically a valid default entrypoint as well... + packageInfo.resolvedEntryPoint = resolve(pkgRoot, pkg.main || 'index.js'); packageInfo.browserMappedMain = false; } From 30a214010939c7ad0791dfa375f17a41e12cbd41 Mon Sep 17 00:00:00 2001 From: Brian Terlson Date: Wed, 20 Nov 2019 12:59:41 -0800 Subject: [PATCH 5/5] clean up realpathing --- src/index.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/index.js b/src/index.js index c41a4e6..9ce67bc 100644 --- a/src/index.js +++ b/src/index.js @@ -139,17 +139,23 @@ export default function nodeResolve ( options = {} ) { if (packageInfoCache.has(pkgPath)) { return packageInfoCache.get(pkgPath); } + + // browserify/resolve doesn't realpath paths returned in its packageFilter callback + if (!preserveSymlinks) { + pkgPath = realpathSync(pkgPath); + } + const pkgRoot = dirname( pkgPath ); const packageInfo = { // copy as we are about to munge the `main` field of `pkg`. packageJson: Object.assign({}, pkg), - // resolve doesn't handle preserveSymlinks for us in this callback - packageJsonPath: preserveSymlinks ? pkgPath : realpathSync(pkgPath), + // path to package.json file + packageJsonPath: pkgPath, // directory containing the package.json - root: preserveSymlinks ? pkgPath : realpathSync(pkgRoot), + root: pkgRoot, // which main field was used during resolution of this module (main, module, or browser) resolvedMainField: 'main',