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

fix(hybrid-nodejs-compat): inject modules only once #8191

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
5 changes: 5 additions & 0 deletions .changeset/beige-buses-poke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"wrangler": patch
---

Optimize module injection in node compat mode
47 changes: 0 additions & 47 deletions packages/wrangler/src/__tests__/hybrid-nodejs-compat.test.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -169,71 +169,61 @@ function handleNodeJSGlobals(
build: PluginBuild,
inject: Record<string, string | string[]>
) {
const UNENV_GLOBALS_RE = /_virtual_unenv_global_polyfill-([^.]+)\.js$/;
const UNENV_GLOBALS_RE = /_virtual_unenv_global_polyfill-(.+)$/;
const prefix = nodePath.resolve(
getBasePath(),
"_virtual_unenv_global_polyfill-"
);

/**
* Map of module identifiers to
* - `injectedName`: the name injected on `globalThis`
* - `exportName`: the export name from the module
* - `importName`: the imported name
*/
const injectsByModule = new Map<
string,
{ injectedName: string; exportName: string; importName: string }[]
>();

const moduleIdSpecifier = new Map<string, string>();

for (const [injectedName, moduleSpecifier] of Object.entries(inject)) {
const [module, exportName, importName] = Array.isArray(moduleSpecifier)
? [moduleSpecifier[0], moduleSpecifier[1], moduleSpecifier[1]]
: [moduleSpecifier, "default", "defaultExport"];

if (!injectsByModule.has(module)) {
injectsByModule.set(module, []);
moduleIdSpecifier.set(module.replaceAll("/", "-"), module);
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
injectsByModule.get(module)!.push({ injectedName, exportName, importName });
}

build.initialOptions.inject = [
...(build.initialOptions.inject ?? []),
//convert unenv's inject keys to absolute specifiers of custom virtual modules that will be provided via a custom onLoad
...Object.keys(inject).map(
(globalName) => `${prefix}${encodeToLowerCase(globalName)}.js`
),
...[...moduleIdSpecifier.keys()].map((id) => `${prefix}${id}`),
];

build.onResolve({ filter: UNENV_GLOBALS_RE }, ({ path }) => ({ path }));

build.onLoad({ filter: UNENV_GLOBALS_RE }, ({ path }) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const globalName = decodeFromLowerCase(path.match(UNENV_GLOBALS_RE)![1]);
const { importStatement, exportName } = getGlobalInject(inject[globalName]);
const module = moduleIdSpecifier.get(path.match(UNENV_GLOBALS_RE)![1])!;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const injects = injectsByModule.get(module)!;

const imports = injects.map(({ exportName, importName }) =>
importName === exportName ? exportName : `${exportName} as ${importName}`
);

return {
contents: dedent`
${importStatement}
globalThis.${globalName} = ${exportName};
import { ${imports.join(", ")} } from "${module}";
${injects.map(({ injectedName, importName }) => `globalThis.${injectedName} = ${importName};`).join("\n")}
`,
};
});
}

/**
* Get the import statement and export name to be used for the given global inject setting.
*/
function getGlobalInject(globalInject: string | string[]) {
if (typeof globalInject === "string") {
// the mapping is a simple string, indicating a default export, so the string is just the module specifier.
return {
importStatement: `import globalVar from "${globalInject}";`,
exportName: "globalVar",
};
}
// the mapping is a 2 item tuple, indicating a named export, made up of a module specifier and an export name.
const [moduleSpecifier, exportName] = globalInject;
return {
importStatement: `import { ${exportName} } from "${moduleSpecifier}";`,
exportName,
};
}

/**
* Encodes a case sensitive string to lowercase string.
*
* - Escape $ with another $ ("$" -> "$$")
* - Escape uppercase letters with $ and turn them into lowercase letters ("L" -> "$L")
*
* This function exists because ESBuild requires that all resolved paths are case insensitive.
* Without this transformation, ESBuild will clobber /foo/bar.js with /foo/Bar.js
*/
export function encodeToLowerCase(str: string): string {
return str.replace(/[A-Z$]/g, (escape) => `$${escape.toLowerCase()}`);
}

/**
* Decodes a string lowercased using `encodeToLowerCase` to the original strings
*/
export function decodeFromLowerCase(str: string): string {
return str.replace(/\$[a-z$]/g, (escaped) => escaped[1].toUpperCase());
}
Loading