Skip to content
This repository was archived by the owner on Aug 4, 2021. It is now read-only.
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
77 changes: 77 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe .find instead of . filter?
As for the first check, I hope at some point in the future we can rework Rollup to only pass some kind of "normalized" options here so the check would become unnecessary, but alas, not yet.

},
transform (code, id) {
const info = getPackageInfoForId(id);
// ...
}
}
}
```

## License

Expand Down
65 changes: 56 additions & 9 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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
Expand All @@ -138,19 +139,47 @@ 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),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would definitely be gold-plating, but an approach that would avoid the Object.assign would be to change the code to not override main but instead e.g. take it from the packageInfo. I admit I did not look at the usages of main, though. What do you think? It would be cleaner IMO.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

overriding main is done mainly (tee hee) for controlling browserify/resolve's behavior. It doesn't handle e.g. different main fields like module, so support for that added by munging the package json before resolve sees it. Since the munging is somewhat complex, doing it up front and caching the result seems good to me.


// path to package.json file
packageJsonPath: pkgPath,

// directory containing the package.json
root: 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,
Expand All @@ -172,18 +201,29 @@ 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 {
// index.node is technically a valid default entrypoint as well...
packageInfo.resolvedEntryPoint = resolve(pkgRoot, pkg.main || 'index.js');
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;
Expand Down Expand Up @@ -253,13 +293,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,
Expand Down Expand Up @@ -297,7 +339,6 @@ export default function nodeResolve ( options = {} ) {
}

importSpecifierList.push(importee);

return resolveImportSpecifiers(
importSpecifierList,
Object.assign(resolveOptions, customResolveOptions)
Expand All @@ -321,6 +362,8 @@ export default function nodeResolve ( options = {} ) {
return resolved;
})
.then(resolved => {
idToPackageInfo.set(resolved, packageInfo);

if ( hasPackageEntry ) {
if (builtins.has(resolved) && preferBuiltins && isPreferBuiltinsSet) {
return null;
Expand Down Expand Up @@ -354,5 +397,9 @@ export default function nodeResolve ( options = {} ) {
}
return null;
},

getPackageInfoForId (id) {
return idToPackageInfo.get(id);
}
};
}
111 changes: 110 additions & 1 deletion test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -910,6 +910,7 @@ describe( 'rollup-plugin-node-resolve', function () {
});
});


it('handles package side-effects', () =>
rollup.rollup({
input: 'samples/side-effects/main.js',
Expand All @@ -926,5 +927,113 @@ describe( 'rollup-plugin-node-resolve', function () {
'array-index'
]);
delete global.sideEffects;
}));
})
);

describe('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.deepStrictEqual(entriesInfo, {
browserMappedMain: false,
resolvedMainField: 'main',
packageJson: require(entriesPkgJsonPath),
packageJsonPath: entriesPkgJsonPath,
root,
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.deepStrictEqual(entriesInfo, {
browserMappedMain: false,
resolvedMainField: 'module',
packageJson: require(entriesPkgJsonPath),
packageJsonPath: entriesPkgJsonPath,
root,
resolvedEntryPoint: path.resolve(root, './module-entry.js')
});
});
});

it('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.deepStrictEqual(entriesInfo, {
browserMappedMain: true,
resolvedMainField: 'main',
packageJson: expectedPkgJson,
packageJsonPath: entriesPkgJsonPath,
root,
resolvedEntryPoint: path.resolve(root, './browser.js')
});
}
});
});

});
});