Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support empty fallbacks #146

Merged
merged 1 commit into from
Aug 19, 2023
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
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Polyfills nodejs builtin modules and globals for the browser.
- Full TypeScript & JavaScript support
- Supports `node:` protocol
- Optionally injects globals
- Optionally provides empty fallbacks

## Install

Expand Down Expand Up @@ -65,6 +66,21 @@ build({
});
```

Optionally provide empty fallbacks for any unpolyfilled or unconfigured modules:

```ts
import { nodeModulesPolyfillPlugin } from 'esbuild-plugins-node-modules-polyfill';
import { build } from 'esbuild';
build({
plugins: [nodeModulesPolyfillPlugin({
fallback: 'empty',
modules: {
crypto: true,
}
})],
});
```

Optionally inject globals when detected:

```ts
Expand Down
39 changes: 25 additions & 14 deletions src/lib/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type esbuild from 'esbuild';
const NAME = 'node-modules-polyfills';

export interface NodePolyfillsOptions {
fallback?: 'empty' | 'none';
Copy link
Contributor Author

@markdalgleish markdalgleish Aug 18, 2023

Choose a reason for hiding this comment

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

I went with this API so that it's more future proof, as opposed to emptyFallback: true or something similar. This way we can introduce different fallback behaviour in the future if needed without it being a breaking change, e.g. fallback: 'error' or fallback: (args) => { /* some custom logic... */ }. It also nicely mirrors the modules config option, making it clear that it's providing the same result.

globals?: {
Buffer?: boolean;
process?: boolean;
Expand Down Expand Up @@ -53,7 +54,7 @@ const loader = async (args: esbuild.OnLoadArgs): Promise<esbuild.OnLoadResult> =
};

export const nodeModulesPolyfillPlugin = (options: NodePolyfillsOptions = {}): Plugin => {
const { globals = {}, modules: modulesOption = builtinModules, namespace = NAME, name = NAME } = options;
const { globals = {}, modules: modulesOption = builtinModules, fallback, namespace = NAME, name = NAME } = options;
if (namespace.endsWith('commonjs')) {
throw new Error(`namespace ${namespace} must not end with commonjs`);
}
Expand Down Expand Up @@ -101,32 +102,42 @@ export const nodeModulesPolyfillPlugin = (options: NodePolyfillsOptions = {}): P

onLoad({ filter: /.*/, namespace }, loader);
onLoad({ filter: /.*/, namespace: commonjsNamespace }, loader);
const filter = new RegExp(
`^(?:node:)?(?:${Object.keys(modules)
.filter((moduleName) => builtinModules.includes(moduleName))
.map(escapeRegex)
.join('|')})$`,
);

// If we are using empty fallbacks, we need to handle all builtin modules so that we can replace their contents,
// otherwise we only need to handle the modules that are configured (which is everything by default)
const bundledModules =
fallback === 'empty'
? builtinModules
: Object.keys(modules).filter((moduleName) => builtinModules.includes(moduleName));

const filter = new RegExp(`^(?:node:)?(?:${bundledModules.map(escapeRegex).join('|')})$`);

const resolver = async (args: OnResolveArgs): Promise<OnResolveResult | undefined> => {
const moduleName = normalizeNodeBuiltinPath(args.path);

const emptyResult = {
namespace: emptyNamespace,
path: args.path,
sideEffects: false,
};

const fallbackResult =
fallback === 'empty'
? emptyResult // Stub it out with an empty module
: undefined; // Opt out of resolving it entirely so esbuild/other plugins can handle it

if (!modules[moduleName]) {
return;
return fallbackResult;
}

if (modules[moduleName] === 'empty') {
return {
namespace: emptyNamespace,
path: args.path,
sideEffects: false,
};
return emptyResult;
}

const polyfill = await getCachedPolyfillPath(moduleName).catch(() => null);

if (!polyfill) {
return;
return fallbackResult;
}

const ignoreRequire = args.namespace === commonjsNamespace;
Expand Down
5 changes: 5 additions & 0 deletions tests/fixtures/input/fallbackPolyfilled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import * as constants from 'node:constants';
import * as util from 'node:util';

console.log(typeof constants);
console.log(typeof util);
4 changes: 4 additions & 0 deletions tests/fixtures/input/fallbackUnpolyfilled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// This is not polyfilled: https://github.com/jspm/jspm-core/tree/main/nodelibs/browser
import * as traceEvents from 'node:trace_events';

console.log(typeof traceEvents);
Loading