ui5-tooling-modules
Version:
UI5 CLI extensions to load and convert node modules as UI5 AMD-like modules
141 lines (135 loc) • 5.97 kB
JavaScript
/**
* Rollup plugin: layered Node-builtin polyfill resolver.
*
* Wraps `rollup-plugin-polyfill-node` with a four-layer resolution chain
* for `node:<x>` / bare-builtin imports:
*
* 1. Per-project escape hatch: `<cwd>/_polyfill-overrides_/<module>.js` is
* loaded verbatim if present. Use this for app-specific overrides
* that should not become workspace defaults.
* 2. Centralized augmentation: `POLYFILL_AUGMENTATIONS[<module>]` is
* appended to the upstream polyfill source. Use this for small,
* workspace-wide patches (e.g. surfacing browser globals like
* `TextEncoder` / `TextDecoder` that the upstream `util` polyfill is
* missing). Tree-shaking removes the appended exports if nothing
* imports them by name, so registering augmentations is safe.
* 3. Upstream `rollup-plugin-polyfill-node` polyfill set.
* 4. Empty-module fallback (`?polyfill-node-ignore`) so unsupported
* builtins do not fail the bundle.
*
* The plugin also ships a companion {@link module.exports.inject} factory
* which produces an `@rollup/plugin-inject` configured to swap `process`,
* `Buffer`, `global`, `__filename`, `__dirname` references in user code
* for the polyfilled equivalents — kept in sync with the upstream plugin.
*
* @param {object} options plugin options
* @param {{ verbose?: (msg: string) => void }} [options.log] optional logger
* @param {string} options.cwd
* project root used to locate `_polyfill-overrides_`
* @param {string[]} [options.moduleNames]
* explicit entry-point names; entries here are NOT treated as
* Node builtins even if `require.resolve` would map them as such
* @returns {import('rollup').Plugin} configured rollup plugin
*/
/* eslint-disable no-unused-vars */
const { existsSync, readFileSync } = require("fs");
const { join } = require("path");
// reuse logic from polyfill-node plugin
const nodePolyfills = require("rollup-plugin-polyfill-node");
const PREFIX = `\0polyfill-node.`;
const PREFIX_LENGTH = PREFIX.length;
const DIRNAME_PATH = "\0node-polyfills:dirname";
const FILENAME_PATH = "\0node-polyfills:filename";
const inject = require("@rollup/plugin-inject");
// Augmentations that get appended to the upstream rollup-plugin-polyfill-node
// polyfill source for a given Node built-in. Used to plug exports the
// upstream polyfill does not provide (typically WHATWG / Web-Platform APIs
// that exist as globals in the browser but only became `node:<module>` named
// exports in newer Node versions).
//
// Each entry is keyed by built-in module name and contains an ESM source
// snippet. The snippet is appended verbatim to the upstream polyfill source
// and may reference `globalThis.*`. Tree-shaking removes the appended
// exports from the final bundle when nothing imports them by name, so it is
// safe to register augmentations defensively.
const POLYFILL_AUGMENTATIONS = {
// node:util gained TextEncoder/TextDecoder as named exports in Node 11
// (Encoder) / 12 (Decoder). rollup-plugin-polyfill-node's `util.js` is
// frozen at the legacy shape and does not expose them. Re-export the
// browser globals so consumers that do `import {TextDecoder} from "util"`
// (or destructure `require("util").TextDecoder`) resolve to a working
// implementation in the bundled output.
util: `
export const TextEncoder = globalThis.TextEncoder;
export const TextDecoder = globalThis.TextDecoder;
`,
};
const isBuiltInModule = function isBuiltInModule(module, entryPoints) {
try {
const isEntryPoint = entryPoints?.includes(module);
if (!isEntryPoint && !require("path").isAbsolute(module) && require.resolve(module) === module) {
return true;
}
} catch (ex) {
/* ignore */
}
return false;
};
module.exports = function nodePolyfillsOverride({ log, cwd, moduleNames } = {}) {
const { resolveId, load, transform } = nodePolyfills();
const overridesDir = join(cwd, "_polyfill-overrides_");
return {
name: "polyfill-node-override",
resolveId: function (importee, importer, options) {
if (isBuiltInModule(importee, moduleNames)) {
const builtInModule = importee.startsWith("node:") ? importee.substr("node:".length) : importee;
if (existsSync(join(overridesDir, `${builtInModule}.js`))) {
return { id: `${PREFIX}${builtInModule}.js`, moduleSideEffects: false };
}
const resolvedId = resolveId.call(this, builtInModule, importer, options);
if (resolvedId) {
return resolvedId;
} else {
return { id: `${builtInModule}?polyfill-node-ignore`, moduleSideEffects: false };
}
}
},
load: function (importee) {
if (importee.startsWith(PREFIX)) {
const builtInModule = importee.substr(PREFIX_LENGTH);
let content;
if (existsSync(join(overridesDir, `${builtInModule}`))) {
content = readFileSync(join(overridesDir, `${builtInModule}`), { encoding: "utf8" });
} else {
content = load.apply(this, arguments);
// Append any augmentations registered for this built-in
// (e.g. TextEncoder/TextDecoder for `util`). The match key
// is the module name without the trailing `.js`.
const moduleKey = builtInModule.replace(/\.js$/, "");
const augmentation = POLYFILL_AUGMENTATIONS[moduleKey];
if (augmentation && typeof content === "string") {
content += augmentation;
}
}
return content;
} else if (importee.endsWith("?polyfill-node-ignore")) {
return "";
}
return null;
},
};
};
// prevent the renaming of global, process, Buffer in any module
// => needs to be used as a standalone rollup plugin in build
// !!!! KEEP IN SYNC WITH >> rollup-plugin-polyfill-node << !!!!
module.exports.inject = function nodePolyfillsOverrideInject(additionalOptions) {
return inject(
Object.assign({}, additionalOptions, {
process: PREFIX + "process",
Buffer: [PREFIX + "buffer", "Buffer"],
global: PREFIX + "global",
__filename: FILENAME_PATH,
__dirname: DIRNAME_PATH,
}),
);
};