Skip to content

Commit 100fb7f

Browse files
committed
feat: finish with polyfill substitutions
Allow callback to replace polyfill with alternative implementation add tests for callbacks update readme to reflect changes, document callback, update footnotes for cypto and fs modules
1 parent 9a54ad0 commit 100fb7f

9 files changed

Lines changed: 164 additions & 54 deletions

File tree

polyfills/constants.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ export var UV_FS_COPYFILE_FICLONE = 2;
173173
export var COPYFILE_FICLONE = 2;
174174
export var UV_FS_COPYFILE_FICLONE_FORCE = 4;
175175
export var COPYFILE_FICLONE_FORCE = 4;
176-
export var OPENSSL_VERSION_NUMBER = 805306528;
176+
export var OPENSSL_VERSION_NUMBER = 805306496;
177177
export var SSL_OP_ALL = 2147485776;
178178
export var SSL_OP_ALLOW_NO_DHE_KEX = 1024;
179179
export var SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION = 262144;

readme.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ rollup({
5454
- `sourceMap: boolean`: True to get source maps, false otherwise.
5555

5656
- `prefixExternals: boolean;`: If a polyfill is skipped using `onPolyfill` callback, include prefix `node:` on the module name, see `onPolyfill`
57-
- `onPolyfill: (module: string) => boolean;`: Default, allow all. Allow project to opt-out of one or more polyfills, skipped modules are preserved as `import "module"` or `import "node:module"`
57+
- `onPolyfill: (module: string, implementation: string | undefined) => boolean | string;`: Default, allow all. Allow project to opt-out or replace one or more polyfills, returning false sets modules to resolve as external (e.g. `import "module"` or `import "node:module"`), or return an alternative polyfill implementation as a string value, undefined `implementation` indicates that an empty shim is being used.
5858

5959
## Node.js Builtin Support Table
6060

@@ -90,9 +90,9 @@ The following modules include ES6 specific version which allow you to do named i
9090
- readline∆
9191
- repl∆
9292
- tls∆
93-
- fs˚
94-
- crypto˚
95-
- perf_hooks˚ - **New:* just an empty shim for now, but would love help building a true polyfill!*
93+
- fs
94+
- crypto
95+
- perf_hooks - **New:* just an empty shim for now, but would love help building a true polyfill!*
9696

9797

9898
† the http and https modules are actually the same and don't differentiate based on protocol
@@ -101,8 +101,8 @@ The following modules include ES6 specific version which allow you to do named i
101101

102102
§ vm does not have all corner cases and has less of them in a web worker
103103

104-
∆ not shimmed, just returns mock
104+
∆ not shimmed, just returns mock (module, with no functions)
105105

106-
˚ shimmed, but too complex to polyfill fully. Avoid if at all possible. Some bugs and partial support expected.
106+
~~˚ shimmed, but too complex to polyfill fully. Avoid if at all possible. Some bugs and partial support expected. ~~
107107

108108
Not all included modules rollup equally, streams (and by extension anything that requires it like http) are a mess of circular references that are pretty much impossible to tree-shake out, similarly url methods are actually a shortcut to a url object so those methods don't tree shake out very well, punycode, path, querystring, events, util, and process tree shake very well especially if you do named imports.

scripts/collect-polyfills.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ async function collect(depName) {
1111
for (const f of allFiles) {
1212
allFilesContents[f] = fs.readFileSync(path.join(__dirname, '../polyfills', f), 'utf8');
1313
}
14-
fs.writeFileSync(path.join(__dirname, '../src/polyfills.ts'), `export default ${JSON.stringify(allFilesContents)}`);
14+
fs.writeFileSync(path.join(__dirname, '../src/polyfills.ts'), `type Polyfills = Record<string, string>;\nexport default ${JSON.stringify(allFilesContents)} as Polyfills;`);
1515
}
1616

1717
collect();

src/has-module.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
2+
export type OnPolyfill = (module: string, implementation: string | undefined) => boolean | string
3+
4+
export const createHasModule = (modules: Map<string, string>, polyfills: Record<string, string>, onPolyfill: OnPolyfill | undefined ) => {
5+
if (onPolyfill === undefined) {
6+
return (module: string) => modules.has(module)
7+
}
8+
9+
const cachedResult = new Map<string, boolean>();
10+
return (module: string) => {
11+
const emptyPath = polyfills['empty.js'];
12+
13+
if (modules.has(module)) {
14+
// some special cases, matching injected modules and underscored modules
15+
// are likely to need to be implemented as part of a previously added polyfill
16+
if (module === 'buffer' || module === 'process' || module ==='global' || module.startsWith("_")) {
17+
return true;
18+
}
19+
20+
const implementation = modules.get(module);
21+
22+
// call the callback only once per module
23+
if (cachedResult.has(module)) {
24+
return cachedResult.get(module)!;
25+
} else {
26+
let result = onPolyfill(module, implementation === emptyPath ? undefined : implementation);
27+
28+
if (result === undefined || result === false) {
29+
cachedResult.set(module, false);
30+
return false;
31+
} else if (typeof result === 'string') {
32+
// callback wants to replace the polyfill with a different implementation
33+
if (result === '') {
34+
modules.set(module, emptyPath);
35+
} else {
36+
modules.set(module, result);
37+
}
38+
result = true
39+
}
40+
41+
cachedResult.set(module, result);
42+
return result;
43+
}
44+
}
45+
}
46+
};

src/index.ts

Lines changed: 14 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
// @ts-ignore
22
import type { Plugin } from "rollup";
3-
import inject, { Injectment, RollupInjectOptions } from "@rollup/plugin-inject";
3+
import inject, { type RollupInjectOptions } from "@rollup/plugin-inject";
44
import { getModules } from "./modules";
55
import { posix, resolve } from "path";
66
import { randomBytes } from "crypto";
77
import POLYFILLS from './polyfills';
88
import { isBuiltin } from "module";
9+
import { createHasModule, type OnPolyfill } from "./has-module";
910

1011
// Node import paths use POSIX separators
1112
const { dirname, relative, join } = posix;
@@ -19,36 +20,22 @@ export interface NodePolyfillsOptions {
1920
include?: Array<string | RegExp> | string | RegExp | null;
2021
exclude?: Array<string | RegExp> | string | RegExp | null;
2122

23+
/**
24+
* @deprecated, crypto flag behavior is not implemented
25+
*/
26+
crypto?: boolean;
27+
2228
// If a polyfill is skipped, should the external module name be prefixed with `node:`?
2329
prefixExternals?: boolean; // default: false
2430

25-
// filtering of which polyfills are applied, others remain external
26-
onPolyfill?: (module: string) => boolean; // default: () => true
31+
// allow filtering or substitution of polyfills that are applied, returning false will result in dependency remaining external
32+
// if defaultPolyfill is undefined, the polyfill returns an empty module
33+
onPolyfill?: OnPolyfill; // default: () => true
2734
}
2835

2936
export default function (opts: NodePolyfillsOptions = {}): Plugin {
3037
const mods = getModules();
31-
const onPolyfill = opts.onPolyfill ?? (() => true);
32-
const onPolyfillCache = new Map<string, boolean>();
33-
const hasModule = (module: string) => {
34-
if (mods.has(module)) {
35-
// some special cases, matching injected modules and underscored modules
36-
// are likely to need to be implemented as part of a previously added polyfill
37-
if (module === 'buffer' || module === 'process' || module ==='global' || module.startsWith("_")) {
38-
return true;
39-
}
40-
41-
// cache the result of shouldPolyfill, to make it easier for the user to
42-
// keep track of what was actually polyfilled
43-
if (onPolyfillCache.has(module)) {
44-
return onPolyfillCache.get(module)!;
45-
} else {
46-
const result = onPolyfill(module);
47-
onPolyfillCache.set(module, result);
48-
return result;
49-
}
50-
}
51-
};
38+
const hasModule = createHasModule(mods, POLYFILLS, opts.onPolyfill);
5239

5340
const injectPlugin = inject({
5441
include: opts.include === undefined ? ['node_modules/**/*.js'] : opts.include,
@@ -67,7 +54,7 @@ export default function (opts: NodePolyfillsOptions = {}): Plugin {
6754
return {
6855
name: "polyfill-node",
6956
resolveId(importee: string, importer?: string) {
70-
// Fixes commonjs compatability: https://github.com/FredKSchott/rollup-plugin-polyfill-node/pull/42
57+
// Fixes commonjs compatibility: https://github.com/FredKSchott/rollup-plugin-polyfill-node/pull/42
7158
if (importee[0] == '\0' && /\?commonjs-\w+$/.test(importee)) {
7259
importee = importee.slice(1).replace(/\?commonjs-\w+$/, '');
7360
}
@@ -103,7 +90,7 @@ export default function (opts: NodePolyfillsOptions = {}): Plugin {
10390
if (importee.startsWith(PREFIX)) {
10491
importee = importee.substr(PREFIX_LENGTH);
10592
}
106-
if (hasModule(importee) || (POLYFILLS as any)[importee.replace('.js', '') + '.js']) {
93+
if (hasModule(importee) || POLYFILLS[importee.replace('.js', '') + '.js']) {
10794
return { id: PREFIX + importee.replace('.js', '') + '.js', moduleSideEffects: false };
10895
}
10996
return null;
@@ -114,7 +101,7 @@ export default function (opts: NodePolyfillsOptions = {}): Plugin {
114101
}
115102
if (id.startsWith(PREFIX)) {
116103
const importee = id.substr(PREFIX_LENGTH).replace('.js', '');
117-
return mods.get(importee) || (POLYFILLS as any)[importee + '.js'];
104+
return mods.get(importee) || POLYFILLS[importee + '.js'];
118105
}
119106

120107
},

src/modules.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import POLYFILLS from './polyfills';
2-
const EMPTY_PATH = POLYFILLS['empty.js'];
2+
export const EMPTY_PATH = POLYFILLS['empty.js'];
33

44
export function getModules() {
55
const libs = new Map<string, string>();

src/polyfills.ts

Lines changed: 2 additions & 1 deletion
Large diffs are not rendered by default.

test/examples/alt-assert.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import assert from 'assert';
2+
3+
assert(false, 'custom polyfill assert should be invoked, as capitalized message');

test/index.js

Lines changed: 90 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const rollup = require('rollup');
33
const nodePolyfills = require('..');
44
const os = require('os');
55
const constants = require('constants');
6+
const assert = require('assert')
67
const debug = require('debug')('builtins:test');
78
const files = [
89
'events.js',
@@ -18,9 +19,23 @@ const files = [
1819
'string-decoder.js',
1920
'zlib.js',
2021
'domain.js',
21-
'crypto.js'
2222
];
2323

24+
const runCode = (code, done) => {
25+
const script = new vm.Script(code);
26+
const context = vm.createContext({
27+
done: done,
28+
setTimeout: setTimeout,
29+
clearTimeout: clearTimeout,
30+
console: console,
31+
_constants: constants,
32+
_osEndianness: os.endianness()
33+
});
34+
context.self = context;
35+
36+
return script.runInContext(context);
37+
}
38+
2439
describe('rollup-plugin-node-polyfills', function() {
2540

2641
this.timeout(5000);
@@ -31,25 +46,18 @@ describe('rollup-plugin-node-polyfills', function() {
3146
input: 'test/examples/' + file,
3247
plugins: [
3348
nodePolyfills({
34-
include: null
49+
include: null,
50+
onPolyfill: function (module) {
51+
return true;
52+
}
3553
})
3654
]
3755
})
3856
.then(bundle => bundle.generate({format: 'cjs'}))
3957
.then(generated => {
4058
const code = generated.output[0].code;
4159
debug(code);
42-
const script = new vm.Script(code);
43-
const context = vm.createContext({
44-
done: done,
45-
setTimeout: setTimeout,
46-
clearTimeout: clearTimeout,
47-
console: console,
48-
_constants: constants,
49-
_osEndianness: os.endianness()
50-
});
51-
context.self = context;
52-
script.runInContext(context);
60+
return runCode(code, done);
5361
})
5462
.catch(done)
5563
});
@@ -61,6 +69,7 @@ describe('rollup-plugin-node-polyfills', function() {
6169
plugins: [
6270
nodePolyfills({
6371
include: null,
72+
// this flag has no effect
6473
crypto: true
6574
})
6675
]
@@ -81,9 +90,12 @@ describe('rollup-plugin-node-polyfills', function() {
8190
plugins: [
8291
nodePolyfills({
8392
include: null,
84-
onPolyfill: function (module) {
85-
// exclude the util module
86-
return false;
93+
onPolyfill: function (module, implementation) {
94+
if (module === 'util') {
95+
// exclude the util module
96+
return false
97+
}
98+
return true;
8799
}
88100
})
89101
]
@@ -94,6 +106,67 @@ describe('rollup-plugin-node-polyfills', function() {
94106
} else {
95107
done(new Error('util module was not excluded'));
96108
}
97-
})
109+
}).catch(done);
98110
});
111+
112+
it('can replace a polyfill', function(done) {
113+
rollup.rollup({
114+
input: 'test/examples/alt-assert.js',
115+
plugins: [
116+
nodePolyfills({
117+
include: null,
118+
onPolyfill: function (module, implementation) {
119+
if (module === 'assert') {
120+
assert(implementation !== undefined, 'assert implementation should be defined')
121+
// replace the assert module with a custom one
122+
// must be a properly formatted as cjs formatted code
123+
124+
// use a partial mock implementation of assert
125+
return `
126+
function assert(value, message) {
127+
// custom assert implementation, upper-cases the message
128+
// call the 'callback' below with the upper-cased message
129+
if (!value) done(message.toUpperCase());
130+
}
131+
export default assert;
132+
`;
133+
}
134+
return true;
135+
}
136+
})
137+
]
138+
}).then(bundle => bundle.generate({format: 'cjs'}))
139+
.then(generated => {
140+
const code = generated.output[0].code;
141+
142+
const callback = (assertMsg) => {
143+
assert.equal(assertMsg, 'CUSTOM POLYFILL ASSERT SHOULD BE INVOKED, AS CAPITALIZED MESSAGE')
144+
done()
145+
}
146+
return runCode(code, callback);
147+
}).catch(done);
148+
});
149+
150+
it('can note an empty polyfill implementation', function(done) {
151+
rollup.rollup({
152+
input: 'test/examples/crypto.js',
153+
plugins: [
154+
nodePolyfills({
155+
include: null,
156+
onPolyfill: function (module, implementation) {
157+
if (module === 'crypto') {
158+
// crypto currently is a no-op polyfill, with an empty implementation
159+
assert(implementation === undefined, 'crypto implementation should be undefined')
160+
}
161+
return true;
162+
}
163+
})
164+
]
165+
}).then(bundle => bundle.generate({format: 'cjs'}))
166+
.then(generated => {
167+
const code = generated.output[0].code;
168+
return runCode(code, done);
169+
}).catch(done);
170+
});
171+
99172
})

0 commit comments

Comments
 (0)