Skip to content
Open
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
2 changes: 1 addition & 1 deletion polyfills/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ export var UV_FS_COPYFILE_FICLONE = 2;
export var COPYFILE_FICLONE = 2;
export var UV_FS_COPYFILE_FICLONE_FORCE = 4;
export var COPYFILE_FICLONE_FORCE = 4;
export var OPENSSL_VERSION_NUMBER = 805306528;
export var OPENSSL_VERSION_NUMBER = 805306496;
export var SSL_OP_ALL = 2147485776;
export var SSL_OP_ALLOW_NO_DHE_KEX = 1024;
export var SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION = 262144;
Expand Down
33 changes: 28 additions & 5 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,27 @@ rollup({
})
```


```js
import nodePolyfills from 'rollup-plugin-polyfill-node';
rollup({
entry: 'main.js',
plugins: [
nodePolyfills({
onPolyfill: (module) => {
const allowList = ['util']
const allow = allowList.includes(module)
if (allow === false) {
console.warn('module will be maintained as external import', module)
}
return allow
}
})
]
})
```


## Options

*All options are optional.*
Expand All @@ -32,6 +53,8 @@ rollup({
- `exclude: Array<string | RegExp> | string | RegExp | null;`: Exclude files from transformation.
- `sourceMap: boolean`: True to get source maps, false otherwise.

- `prefixExternals: boolean;`: If a polyfill is skipped using `onPolyfill` callback, include prefix `node:` on the module name, see `onPolyfill`
- `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.

## Node.js Builtin Support Table

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


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

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

∆ not shimmed, just returns mock
∆ not shimmed, just returns mock (module, with no functions)

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

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.
1 change: 1 addition & 0 deletions rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export default {
external: [
'path',
'crypto',
'module',
'@rollup/plugin-inject'
],
output: ['es','cjs'].map((format) => ({
Expand Down
2 changes: 1 addition & 1 deletion scripts/collect-polyfills.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ async function collect(depName) {
for (const f of allFiles) {
allFilesContents[f] = fs.readFileSync(path.join(__dirname, '../polyfills', f), 'utf8');
}
fs.writeFileSync(path.join(__dirname, '../src/polyfills.ts'), `export default ${JSON.stringify(allFilesContents)}`);
fs.writeFileSync(path.join(__dirname, '../src/polyfills.ts'), `type Polyfills = Record<string, string>;\nexport default ${JSON.stringify(allFilesContents)} as Polyfills;`);
}

collect();
46 changes: 46 additions & 0 deletions src/has-module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@

export type OnPolyfill = (module: string, implementation: string | undefined) => boolean | string

export const createHasModule = (modules: Map<string, string>, polyfills: Record<string, string>, onPolyfill: OnPolyfill | undefined ) => {
if (onPolyfill === undefined) {
return (module: string) => modules.has(module)
}

const cachedResult = new Map<string, boolean>();
return (module: string) => {
const emptyPath = polyfills['empty.js'];

if (modules.has(module)) {
// some special cases, matching injected modules and underscored modules
// are likely to need to be implemented as part of a previously added polyfill
if (module === 'buffer' || module === 'process' || module ==='global' || module.startsWith("_")) {
return true;
}

const implementation = modules.get(module);

// call the callback only once per module
if (cachedResult.has(module)) {
return cachedResult.get(module)!;
} else {
let result = onPolyfill(module, implementation === emptyPath ? undefined : implementation);

if (result === undefined || result === false) {
cachedResult.set(module, false);
return false;
} else if (typeof result === 'string') {
// callback wants to replace the polyfill with a different implementation
if (result === '') {
modules.set(module, emptyPath);
} else {
modules.set(module, result);
}
result = true
}

cachedResult.set(module, result);
return result;
}
}
}
};
37 changes: 33 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
// @ts-ignore
import type { Plugin } from "rollup";
import inject, { RollupInjectOptions } from "@rollup/plugin-inject";
import inject, { type RollupInjectOptions } from "@rollup/plugin-inject";
import { getModules } from "./modules";
import { posix, resolve } from "path";
import { randomBytes } from "crypto";
import POLYFILLS from './polyfills';
import { isBuiltin } from "module";
import { createHasModule, type OnPolyfill } from "./has-module";

// Node import paths use POSIX separators
const { dirname, relative, join } = posix;
Expand All @@ -17,10 +19,24 @@ export interface NodePolyfillsOptions {
sourceMap?: RollupInjectOptions['sourceMap'];
include?: Array<string | RegExp> | string | RegExp | null;
exclude?: Array<string | RegExp> | string | RegExp | null;

/**
* @deprecated, crypto flag behavior is not implemented
*/
crypto?: boolean;

// If a polyfill is skipped, should the external module name be prefixed with `node:`?
prefixExternals?: boolean; // default: false

// allow filtering or substitution of polyfills that are applied, returning false will result in dependency remaining external
// if defaultPolyfill is undefined, the polyfill returns an empty module
onPolyfill?: OnPolyfill; // default: () => true
}

export default function (opts: NodePolyfillsOptions = {}): Plugin {
const mods = getModules();
const hasModule = createHasModule(mods, POLYFILLS, opts.onPolyfill);

const injectPlugin = inject({
include: opts.include === undefined ? ['node_modules/**/*.js'] : opts.include,
exclude: opts.exclude,
Expand All @@ -38,7 +54,7 @@ export default function (opts: NodePolyfillsOptions = {}): Plugin {
return {
name: "polyfill-node",
resolveId(importee: string, importer?: string) {
// Fixes commonjs compatability: https://github.com/FredKSchott/rollup-plugin-polyfill-node/pull/42
// Fixes commonjs compatibility: https://github.com/FredKSchott/rollup-plugin-polyfill-node/pull/42
if (importee[0] == '\0' && /\?commonjs-\w+$/.test(importee)) {
importee = importee.slice(1).replace(/\?commonjs-\w+$/, '');
}
Expand All @@ -55,13 +71,26 @@ export default function (opts: NodePolyfillsOptions = {}): Plugin {
if (importee && importee.slice(-1) === "/") {
importee = importee.slice(0, -1);
}

if (isBuiltin(importee)) {
importee = importee.replace(/^node:/, "");
// if the module is known and not applying a polyfill, return it as an external
if (!hasModule(importee)) {
return {
id: opts.prefixExternals ? "node:" + importee : importee,
external: true,
moduleSideEffects: false,
};
}
}

if (importer && importer.startsWith(PREFIX) && importee.startsWith('.')) {
importee = PREFIX + join(importer.substr(PREFIX_LENGTH).replace('.js', ''), '..', importee) + '.js';
}
if (importee.startsWith(PREFIX)) {
importee = importee.substr(PREFIX_LENGTH);
}
if (mods.has(importee) || (POLYFILLS as any)[importee.replace('.js', '') + '.js']) {
if (hasModule(importee) || POLYFILLS[importee.replace('.js', '') + '.js']) {
return { id: PREFIX + importee.replace('.js', '') + '.js', moduleSideEffects: false };
}
return null;
Expand All @@ -72,7 +101,7 @@ export default function (opts: NodePolyfillsOptions = {}): Plugin {
}
if (id.startsWith(PREFIX)) {
const importee = id.substr(PREFIX_LENGTH).replace('.js', '');
return mods.get(importee) || (POLYFILLS as any)[importee + '.js'];
return mods.get(importee) || POLYFILLS[importee + '.js'];
}

},
Expand Down
2 changes: 1 addition & 1 deletion src/modules.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import POLYFILLS from './polyfills';
const EMPTY_PATH = POLYFILLS['empty.js'];
export const EMPTY_PATH = POLYFILLS['empty.js'];

export function getModules() {
const libs = new Map<string, string>();
Expand Down
3 changes: 2 additions & 1 deletion src/polyfills.ts

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions test/examples/alt-assert.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import assert from 'assert';

assert(false, 'custom polyfill assert should be invoked, as capitalized message');
3 changes: 3 additions & 0 deletions test/examples/filter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { inspect } from 'util'

console.log(inspect('test'))
121 changes: 108 additions & 13 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const rollup = require('rollup');
const nodePolyfills = require('..');
const os = require('os');
const constants = require('constants');
const assert = require('assert')
const debug = require('debug')('builtins:test');
const files = [
'events.js',
Expand All @@ -18,9 +19,23 @@ const files = [
'string-decoder.js',
'zlib.js',
'domain.js',
'crypto.js'
];

const runCode = (code, done) => {
const script = new vm.Script(code);
const context = vm.createContext({
done: done,
setTimeout: setTimeout,
clearTimeout: clearTimeout,
console: console,
_constants: constants,
_osEndianness: os.endianness()
});
context.self = context;

return script.runInContext(context);
}

describe('rollup-plugin-node-polyfills', function() {

this.timeout(5000);
Expand All @@ -31,25 +46,18 @@ describe('rollup-plugin-node-polyfills', function() {
input: 'test/examples/' + file,
plugins: [
nodePolyfills({
include: null
include: null,
onPolyfill: function (module) {
return true;
}
})
]
})
.then(bundle => bundle.generate({format: 'cjs'}))
.then(generated => {
const code = generated.output[0].code;
debug(code);
const script = new vm.Script(code);
const context = vm.createContext({
done: done,
setTimeout: setTimeout,
clearTimeout: clearTimeout,
console: console,
_constants: constants,
_osEndianness: os.endianness()
});
context.self = context;
script.runInContext(context);
return runCode(code, done);
})
.catch(done)
});
Expand All @@ -61,6 +69,7 @@ describe('rollup-plugin-node-polyfills', function() {
plugins: [
nodePolyfills({
include: null,
// this flag has no effect
crypto: true
})
]
Expand All @@ -74,4 +83,90 @@ describe('rollup-plugin-node-polyfills', function() {
done(err)
});
});

it('can exclude a polyfill', function(done) {
rollup.rollup({
input: 'test/examples/filter.js',
plugins: [
nodePolyfills({
include: null,
onPolyfill: function (module, implementation) {
if (module === 'util') {
// exclude the util module
return false
}
return true;
}
})
]
}).then(bundle => bundle.generate({format: 'esm'}))
.then(generated => {
if (generated.output[0].imports.includes('util')) {
done();
} else {
done(new Error('util module was not excluded'));
}
}).catch(done);
});

it('can replace a polyfill', function(done) {
rollup.rollup({
input: 'test/examples/alt-assert.js',
plugins: [
nodePolyfills({
include: null,
onPolyfill: function (module, implementation) {
if (module === 'assert') {
assert(implementation !== undefined, 'assert implementation should be defined')
// replace the assert module with a custom one
// must be a properly formatted as cjs formatted code

// use a partial mock implementation of assert
return `
function assert(value, message) {
// custom assert implementation, upper-cases the message
// call the 'callback' below with the upper-cased message
if (!value) done(message.toUpperCase());
}
export default assert;
`;
}
return true;
}
})
]
}).then(bundle => bundle.generate({format: 'cjs'}))
.then(generated => {
const code = generated.output[0].code;

const callback = (assertMsg) => {
assert.equal(assertMsg, 'CUSTOM POLYFILL ASSERT SHOULD BE INVOKED, AS CAPITALIZED MESSAGE')
done()
}
return runCode(code, callback);
}).catch(done);
});

it('can note an empty polyfill implementation', function(done) {
rollup.rollup({
input: 'test/examples/crypto.js',
plugins: [
nodePolyfills({
include: null,
onPolyfill: function (module, implementation) {
if (module === 'crypto') {
// crypto currently is a no-op polyfill, with an empty implementation
assert(implementation === undefined, 'crypto implementation should be undefined')
}
return true;
}
})
]
}).then(bundle => bundle.generate({format: 'cjs'}))
.then(generated => {
const code = generated.output[0].code;
return runCode(code, done);
}).catch(done);
});

})