Skip to content

Commit 6bc0a2f

Browse files
fix(vite-plugin): ensure node compat globals are injected before any imports (#9581)
* add failing test * add fix * Apply suggestions from code review Co-authored-by: Victor Berchet <[email protected]> --------- Co-authored-by: Victor Berchet <[email protected]>
1 parent 3db206d commit 6bc0a2f

File tree

5 files changed

+82
-18
lines changed

5 files changed

+82
-18
lines changed

.changeset/six-parrots-clap.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@cloudflare/vite-plugin": patch
3+
---
4+
5+
fix: ensure that globals are polyfilled before every import
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const stderr = process.stderr;

packages/vite-plugin-cloudflare/playground/node-compat/worker-process/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import assert from "node:assert";
2+
import { stderr } from "./early-process-access";
23

34
export default {
45
async fetch() {
@@ -7,6 +8,8 @@ export default {
78
} satisfies ExportedHandler;
89

910
function testProcessBehaviour() {
11+
// workerd does not implement `process.stderr`, it comes from unenv.
12+
assert(stderr, "process.stderr was not polyfilled early enough!");
1013
const originalProcess = process;
1114
try {
1215
assert(process !== undefined, "process is missing");

packages/vite-plugin-cloudflare/src/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ import {
3535
getPreviewMiniflareOptions,
3636
} from "./miniflare-options";
3737
import {
38+
getGlobalVirtualModule,
3839
injectGlobalCode,
40+
isGlobalVirtualModule,
3941
isNodeAls,
4042
isNodeAlsModule,
4143
isNodeCompat,
@@ -600,6 +602,9 @@ export function cloudflare(pluginConfig: PluginConfig = {}): vite.Plugin[] {
600602
// rather than allowing the resolve hook here to alias then to polyfills.
601603
enforce: "pre",
602604
async resolveId(source, importer, options) {
605+
if (isGlobalVirtualModule(source)) {
606+
return source;
607+
}
603608
// See if we can map the `source` to a Node.js compat alias.
604609
const result = resolveNodeJSImport(source);
605610
if (!result) {
@@ -626,6 +631,9 @@ export function cloudflare(pluginConfig: PluginConfig = {}): vite.Plugin[] {
626631
// We are in build mode so return the absolute path to the polyfill.
627632
return this.resolve(result.resolved, importer, options);
628633
},
634+
load(id) {
635+
return getGlobalVirtualModule(id);
636+
},
629637
async transform(code, id) {
630638
// Inject the Node.js compat globals into the entry module for Node.js compat environments.
631639
const workerConfig = getWorkerConfig(this.environment.name);

packages/vite-plugin-cloudflare/src/node-js-compat.ts

Lines changed: 65 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -67,28 +67,75 @@ export function isNodeAlsModule(path: string) {
6767
return /^(node:)?async_hooks$/.test(path);
6868
}
6969

70+
/**
71+
* Map of module identifiers to
72+
* - `injectedName`: the name injected on `globalThis`
73+
* - `exportName`: the export name from the module
74+
* - `importName`: the imported name
75+
*/
76+
const injectsByModule = new Map<
77+
string,
78+
{ injectedName: string; exportName: string; importName: string }[]
79+
>();
80+
/**
81+
* Map of virtual module (prefixed by `virtualModulePrefix`) to injectable module id,
82+
* which then maps via `injectsByModule` to the global code to be injected.
83+
*/
84+
const virtualModulePathToSpecifier = new Map<string, string>();
85+
const virtualModulePrefix = `\0_nodejs_global_inject-`;
86+
87+
for (const [injectedName, moduleSpecifier] of Object.entries(env.inject)) {
88+
const [module, exportName, importName] = Array.isArray(moduleSpecifier)
89+
? [moduleSpecifier[0], moduleSpecifier[1], moduleSpecifier[1]]
90+
: [moduleSpecifier, "default", "defaultExport"];
91+
92+
if (!injectsByModule.has(module)) {
93+
injectsByModule.set(module, []);
94+
virtualModulePathToSpecifier.set(
95+
virtualModulePrefix + module.replaceAll("/", "-"),
96+
module
97+
);
98+
}
99+
const injects = injectsByModule.get(module);
100+
assert(injects, `expected injects for ${module} to be defined`);
101+
injects.push({ injectedName, exportName, importName });
102+
}
103+
104+
/**
105+
* Does the given module id resolve to a virtual module corresponding to a global injection module?
106+
*/
107+
export function isGlobalVirtualModule(modulePath: string) {
108+
return virtualModulePathToSpecifier.has(modulePath);
109+
}
110+
111+
/**
112+
* Get the contents of the virtual module corresponding to a global injection module.
113+
*/
114+
export function getGlobalVirtualModule(modulePath: string) {
115+
const module = virtualModulePathToSpecifier.get(modulePath);
116+
if (!module) {
117+
return undefined;
118+
}
119+
const injects = injectsByModule.get(module);
120+
assert(injects, `expected injects for ${module} to be defined`);
121+
122+
const imports = injects.map(({ exportName, importName }) =>
123+
importName === exportName ? exportName : `${exportName} as ${importName}`
124+
);
125+
126+
return (
127+
`import { ${imports.join(", ")} } from "${module}";\n` +
128+
`${injects.map(({ injectedName, importName }) => `globalThis.${injectedName} = ${importName};`).join("\n")}`
129+
);
130+
}
131+
70132
/**
71133
* Gets the necessary global polyfills to inject into the entry-point of the user's code.
72134
*/
73135
export function injectGlobalCode(id: string, code: string) {
74-
const injectedCode = Object.entries(env.inject)
75-
.map(([globalName, globalInject]) => {
76-
if (typeof globalInject === "string") {
77-
const moduleSpecifier = globalInject;
78-
// the mapping is a simple string, indicating a default export, so the string is just the module specifier.
79-
return `import var_${globalName} from "${moduleSpecifier}";\nglobalThis.${globalName} = var_${globalName};\n`;
80-
}
81-
82-
// the mapping is a 2 item tuple, indicating a named export, made up of a module specifier and an export name.
83-
const [moduleSpecifier, exportName] = globalInject;
84-
assert(
85-
moduleSpecifier !== undefined,
86-
"Expected moduleSpecifier to be defined"
87-
);
88-
assert(exportName !== undefined, "Expected exportName to be defined");
89-
return `import var_${globalName} from "${moduleSpecifier}";\nglobalThis.${globalName} = var_${globalName}.${exportName};\n`;
90-
})
91-
.join("\n");
136+
const injectedCode = Array.from(virtualModulePathToSpecifier.keys())
137+
.map((moduleId) => `import "${moduleId}";\n`)
138+
.join("");
92139

93140
// Some globals are not injected using the approach above but are added to globalThis via side-effect imports of polyfills from the unenv-preset.
94141
const polyfillCode = env.polyfill

0 commit comments

Comments
 (0)