Created
April 27, 2023 13:54
-
-
Save oxc/790596f1fe663c24e18ef47252e5a537 to your computer and use it in GitHub Desktop.
esbuild plugin to mark only (some) top-level node_modules as external
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @typedef {Object} NodeModulesLayerPluginOptions | |
* @property {string[]} [layerModules] | |
*/ | |
const recursionFlag = Symbol.for('$NodeModulesLayerPlugin_recursion') | |
/** | |
* This is an esbuild plugin which marks all imports that are included in a NodeModulesLayer as external. | |
* | |
* It ensures that only the "main" version will be marked as external, and other versions that have been installed | |
* into nested node_modules folders will be included in the bundle. | |
* | |
* Consider the following example: | |
* | |
* We have a module "uuid" included as version 8 in our package.json, it is included in the node_modules layer. | |
* We have some module (e.g. "request") that has a dependency on the "uuid" module in version 3. | |
* | |
* Those two dependencies (and in fact the versions) are not compatible, so they get deployed by yarn install like this: | |
* ``` | |
* node_modules/ | |
* uuid/ (version 8) | |
* request/ | |
* index.js | |
* node_modules/ | |
* uuid/ (version 3) | |
* ``` | |
* | |
* If we were just to pass --external:uuid to esbuild, it would replace all imports of uuid with a require('uuid') call. | |
* This would work correctly for users of uuid 8, but imports within the request module would also get the version 8, | |
* instead of version 3 which they depend on. | |
* | |
* To solve this, this module marks only those imports as external, that resolve to a top-level node_modules folder. | |
* If the path an import resolves to contains a second node_modules folder, it is not marked as external. | |
* | |
* In the above example, this would mean that imports of 'uuid' in our own code would be marked as external, but imports | |
* of 'uuid' within the request module would be bundled into the output. | |
* | |
* @type (options: NodeModulesLayerPluginOptions) => import("esbuild").Plugin | |
*/ | |
export const nodeModulesLayerPlugin = ({ layerModules }) => ({ | |
name: 'node-modules-layer', | |
setup(build) { | |
build.onResolve({ filter: new RegExp(`^(${layerModules.join('|')})(/|$)`), namespace: 'file' }, async args => { | |
if (args.pluginData?.[recursionFlag]) { | |
return; | |
} | |
const { path, ...resolveArgs } = args; | |
const resolved = await build.resolve(path, { ...resolveArgs, pluginData: { | |
...args.pluginData, | |
[recursionFlag]: true | |
}}); | |
if (resolved.external) { | |
return resolved; | |
} | |
const nodeModulesOffset = resolved.path.indexOf('/node_modules/'); | |
if (nodeModulesOffset === -1) { | |
return resolved; | |
} | |
const isNestedNodeModules = resolved.path.includes('/node_modules/', nodeModulesOffset + 14); | |
if (isNestedNodeModules) { | |
// we don't want to mark nested node_modules as external, because then the top-level node_modules version | |
// would be used, which is different. There is a reason that the nested node_modules is there. | |
return { | |
...resolved, | |
warnings: [ | |
...resolved.warnings, | |
{ | |
text: `The import "${path}" resolves to a module nested inside another node_modules folder, and will not be marked as external.`, | |
pluginName: 'node-modules-layer', | |
} | |
], | |
}; | |
} | |
return { | |
path, | |
external: true, | |
} | |
}); | |
} | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment