Skip to content

Commit 99ba292

Browse files
vite-plugin: update to use the new @cloudflare/unenv-preset (#7830)
* vite-plugin: update to use the new `@cloudflare/unenv-preset` * update to use Vite 6.1 * refactor: move nodejs-compat stuff into its own Vite plugin * add changesets * pin the cf unenv preset dependency to 1.1.1 * update sub-plugin name * throw if we fail to resolve an aliased import * Update virtual module prefix * fix comment typo * Error if there is no alias found for an import marked as a virtual node.js compat module * move node.js compat plugin back into index.ts and do some code clean up * revert to short-circuiting the config when in preview mode * tweak comment
1 parent e8272b0 commit 99ba292

File tree

8 files changed

+217
-159
lines changed

8 files changed

+217
-159
lines changed

.changeset/eleven-lemons-jump.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+
add support for Vite 6.1

.changeset/kind-ducks-bake.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+
implement the new Cloudflare unenv preset into the Vite plugin

packages/vite-plugin-cloudflare/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,10 @@
4040
"test:ci": "vitest run"
4141
},
4242
"dependencies": {
43+
"@cloudflare/unenv-preset": "1.1.1",
4344
"@hattip/adapter-node": "^0.0.49",
4445
"miniflare": "workspace:*",
45-
"unenv": "catalog:vite-plugin",
46+
"unenv": "2.0.0-rc.1",
4647
"ws": "^8.18.0"
4748
},
4849
"devDependencies": {

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import assert from "node:assert";
22
import { builtinModules } from "node:module";
33
import * as vite from "vite";
4-
import { getNodeCompatExternals } from "./node-js-compat";
54
import { INIT_PATH, UNKNOWN_HOST } from "./shared";
65
import { getOutputDirectory } from "./utils";
76
import type { ResolvedPluginConfig, WorkerConfig } from "./plugin-config";
@@ -156,7 +155,7 @@ export function createCloudflareEnvironmentOptions(
156155
// dev pre-bundling crawling (were we not to set this input field we'd have to appropriately set
157156
// optimizeDeps.entries in the dev config)
158157
input: workerConfig.main,
159-
external: [...cloudflareBuiltInModules, ...getNodeCompatExternals()],
158+
external: [...cloudflareBuiltInModules],
160159
},
161160
},
162161
optimizeDeps: {

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

Lines changed: 89 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@ import {
1616
getPreviewMiniflareOptions,
1717
} from "./miniflare-options";
1818
import {
19+
dealiasVirtualNodeJSImport,
1920
getNodeCompatAliases,
21+
getNodeCompatExternals,
2022
injectGlobalCode,
21-
resolveNodeCompatId,
23+
isNodeCompat,
24+
maybeStripNodeJsVirtualPrefix,
2225
} from "./node-js-compat";
2326
import { resolvePluginConfig } from "./plugin-config";
2427
import { MODULE_PATTERN } from "./shared";
@@ -49,6 +52,7 @@ export function cloudflare(pluginConfig: PluginConfig = {}): vite.Plugin[] {
4952
name: "vite-plugin-cloudflare",
5053
config(userConfig, env) {
5154
if (env.isPreview) {
55+
// Short-circuit the whole configuration if we are in preview mode
5256
return { appType: "custom" };
5357
}
5458

@@ -70,9 +74,6 @@ export function cloudflare(pluginConfig: PluginConfig = {}): vite.Plugin[] {
7074

7175
return {
7276
appType: "custom",
73-
resolve: {
74-
alias: getNodeCompatAliases(),
75-
},
7677
environments:
7778
resolvedPluginConfig.type === "workers"
7879
? {
@@ -142,37 +143,6 @@ export function cloudflare(pluginConfig: PluginConfig = {}): vite.Plugin[] {
142143
configResolved(config) {
143144
resolvedViteConfig = config;
144145
},
145-
async resolveId(source) {
146-
if (resolvedPluginConfig.type === "assets-only") {
147-
return;
148-
}
149-
150-
const workerConfig =
151-
resolvedPluginConfig.workers[this.environment.name];
152-
if (!workerConfig) {
153-
return;
154-
}
155-
156-
return resolveNodeCompatId(this.environment, workerConfig, source);
157-
},
158-
async transform(code, id) {
159-
if (resolvedPluginConfig.type === "assets-only") {
160-
return;
161-
}
162-
163-
const workerConfig =
164-
resolvedPluginConfig.workers[this.environment.name];
165-
166-
if (!workerConfig) {
167-
return;
168-
}
169-
170-
const resolvedId = await this.resolve(workerConfig.main);
171-
172-
if (id === resolvedId?.id) {
173-
return injectGlobalCode(id, code, workerConfig);
174-
}
175-
},
176146
generateBundle(_, bundle) {
177147
let config: Unstable_RawConfig | undefined;
178148

@@ -339,13 +309,8 @@ export function cloudflare(pluginConfig: PluginConfig = {}): vite.Plugin[] {
339309
// Otherwise the `vite:wasm-fallback` plugin prevents the `.wasm` extension being used for module imports.
340310
enforce: "pre",
341311
applyToEnvironment(environment) {
342-
if (resolvedPluginConfig.type === "assets-only") {
343-
return false;
344-
}
345-
346-
return Object.keys(resolvedPluginConfig.workers).includes(
347-
environment.name
348-
);
312+
// Note that this hook does not get called in preview mode.
313+
return getWorkerConfig(environment.name) !== undefined;
349314
},
350315
async resolveId(source, importer) {
351316
if (!source.endsWith(".wasm")) {
@@ -419,7 +384,89 @@ export function cloudflare(pluginConfig: PluginConfig = {}): vite.Plugin[] {
419384
}
420385
},
421386
},
387+
// Plugin that can provide Node.js compatibility support for Vite Environments that are hosted in Cloudflare Workers.
388+
{
389+
name: "vite-plugin-cloudflare:nodejs-compat",
390+
apply(_config, env) {
391+
// Skip this whole plugin if we are in preview mode
392+
return !env.isPreview;
393+
},
394+
config() {
395+
// Configure Vite with the Node.js polyfill aliases
396+
// We have to do this across the whole Vite config because it is not possible to do it per Environment.
397+
// These aliases are to virtual modules that then get Environment specific handling in the resolveId hook.
398+
return {
399+
resolve: {
400+
alias: getNodeCompatAliases(),
401+
},
402+
};
403+
},
404+
configEnvironment(environmentName) {
405+
// Ignore Node.js external modules when building environments that use Node.js compat.
406+
const workerConfig = getWorkerConfig(environmentName);
407+
if (isNodeCompat(workerConfig)) {
408+
return {
409+
build: {
410+
rollupOptions: {
411+
external: getNodeCompatExternals(),
412+
},
413+
},
414+
};
415+
}
416+
},
417+
async resolveId(source, importer, options) {
418+
// Handle the virtual modules that come from Node.js compat aliases.
419+
const from = maybeStripNodeJsVirtualPrefix(source);
420+
if (!from) {
421+
return;
422+
}
423+
424+
const workerConfig = getWorkerConfig(this.environment.name);
425+
if (!isNodeCompat(workerConfig)) {
426+
return this.resolve(from, importer, options);
427+
}
428+
429+
const unresolvedAlias = dealiasVirtualNodeJSImport(from);
430+
const resolvedAlias = await this.resolve(
431+
unresolvedAlias,
432+
import.meta.url
433+
);
434+
assert(
435+
resolvedAlias,
436+
"Failed to resolve aliased nodejs import: " + unresolvedAlias
437+
);
438+
439+
if (this.environment.mode === "dev" && this.environment.depsOptimizer) {
440+
// Make sure the dependency optimizer is aware of this aliased import
441+
this.environment.depsOptimizer.registerMissingImport(
442+
unresolvedAlias,
443+
resolvedAlias.id
444+
);
445+
}
446+
447+
return resolvedAlias;
448+
},
449+
async transform(code, id) {
450+
// Inject the Node.js compat globals into the entry module for Node.js compat environments.
451+
const workerConfig = getWorkerConfig(this.environment.name);
452+
if (!isNodeCompat(workerConfig)) {
453+
return;
454+
}
455+
456+
const resolvedId = await this.resolve(workerConfig.main);
457+
if (id === resolvedId?.id) {
458+
return injectGlobalCode(id, code);
459+
}
460+
},
461+
},
422462
];
463+
464+
function getWorkerConfig(environmentName: string) {
465+
assert(resolvedPluginConfig, "Expected resolvedPluginConfig to be defined");
466+
return resolvedPluginConfig.type !== "assets-only"
467+
? resolvedPluginConfig.workers[environmentName]
468+
: undefined;
469+
}
423470
}
424471

425472
/**
Lines changed: 42 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,29 @@
1-
import { createRequire } from "node:module";
1+
import assert from "node:assert";
2+
import { cloudflare } from "@cloudflare/unenv-preset";
23
import MagicString from "magic-string";
34
import { getNodeCompat } from "miniflare";
4-
import * as unenv from "unenv";
5+
import { defineEnv } from "unenv";
56
import type { WorkerConfig } from "./plugin-config";
6-
import type { Environment } from "vite";
77

8-
const require = createRequire(import.meta.url);
9-
const preset = unenv.env(unenv.nodeless, unenv.cloudflare);
10-
const CLOUDFLARE_VIRTUAL_PREFIX = "\0cloudflare-";
8+
const { env } = defineEnv({
9+
nodeCompat: true,
10+
presets: [cloudflare],
11+
});
12+
13+
const CLOUDFLARE_VIRTUAL_PREFIX = "\0__CLOUDFLARE_NODEJS_COMPAT__";
1114

1215
/**
1316
* Returns true if the given combination of compat dates and flags means that we need Node.js compatibility.
1417
*/
15-
export function isNodeCompat({
16-
compatibility_date,
17-
compatibility_flags,
18-
}: WorkerConfig): boolean {
18+
export function isNodeCompat(
19+
workerConfig: WorkerConfig | undefined
20+
): workerConfig is WorkerConfig {
21+
if (workerConfig === undefined) {
22+
return false;
23+
}
1924
const nodeCompatMode = getNodeCompat(
20-
compatibility_date,
21-
compatibility_flags ?? []
25+
workerConfig.compatibility_date,
26+
workerConfig.compatibility_flags ?? []
2227
).mode;
2328
if (nodeCompatMode === "v2") {
2429
return true;
@@ -40,16 +45,8 @@ export function isNodeCompat({
4045
* If the current environment needs Node.js compatibility,
4146
* then inject the necessary global polyfills into the code.
4247
*/
43-
export function injectGlobalCode(
44-
id: string,
45-
code: string,
46-
workerConfig: WorkerConfig
47-
) {
48-
if (!isNodeCompat(workerConfig)) {
49-
return;
50-
}
51-
52-
const injectedCode = Object.entries(preset.inject)
48+
export function injectGlobalCode(id: string, code: string) {
49+
const injectedCode = Object.entries(env.inject)
5350
.map(([globalName, globalInject]) => {
5451
if (typeof globalInject === "string") {
5552
const moduleSpecifier = globalInject;
@@ -78,63 +75,47 @@ export function injectGlobalCode(
7875
*/
7976
export function getNodeCompatAliases() {
8077
const aliases: Record<string, string> = {};
81-
Object.keys(preset.alias).forEach((key) => {
78+
Object.keys(env.alias).forEach((key) => {
8279
// Don't create aliases for modules that are already marked as external
83-
if (!preset.external.includes(key)) {
80+
if (!env.external.includes(key)) {
8481
aliases[key] = CLOUDFLARE_VIRTUAL_PREFIX + key;
8582
}
8683
});
8784
return aliases;
8885
}
8986

9087
/**
91-
* Attempt to resolve the `id` to an unenv alias or polyfill.
88+
* Get an array of modules that should be considered external.
9289
*/
93-
export function resolveNodeCompatId(
94-
environment: Environment,
95-
workerConfig: WorkerConfig,
96-
id: string
97-
) {
98-
const aliased = resolveNodeAliases(id, workerConfig) ?? id;
99-
100-
if (aliased.startsWith("unenv/")) {
101-
const resolvedDep = require.resolve(aliased).replace(/\.cjs$/, ".mjs");
102-
if (environment.mode === "dev" && environment.depsOptimizer) {
103-
const dep = environment.depsOptimizer.registerMissingImport(
104-
aliased,
105-
resolvedDep
106-
);
107-
return dep.id;
108-
} else {
109-
return resolvedDep;
110-
}
111-
}
90+
export function getNodeCompatExternals(): string[] {
91+
return env.external;
11292
}
11393

11494
/**
115-
* Get an array of modules that should be considered external.
95+
* If the `source` module id starts with the virtual prefix then strip it and return the rest of the id.
96+
* Otherwise return undefined.
11697
*/
117-
export function getNodeCompatExternals(): string[] {
118-
return preset.external;
98+
export function maybeStripNodeJsVirtualPrefix(
99+
source: string
100+
): string | undefined {
101+
return source.startsWith(CLOUDFLARE_VIRTUAL_PREFIX)
102+
? source.slice(CLOUDFLARE_VIRTUAL_PREFIX.length)
103+
: undefined;
119104
}
120105

121106
/**
122107
* Convert any virtual module Id that was generated by the aliases returned from `getNodeCompatAliases()`
123108
* back to real a module Id and whether it is an external (built-in) package or not.
124109
*/
125-
function resolveNodeAliases(source: string, workerConfig: WorkerConfig) {
126-
if (
127-
!source.startsWith(CLOUDFLARE_VIRTUAL_PREFIX) ||
128-
!isNodeCompat(workerConfig)
129-
) {
130-
return;
131-
}
132-
133-
const from = source.slice(CLOUDFLARE_VIRTUAL_PREFIX.length);
134-
const alias = preset.alias[from];
135-
136-
if (alias && preset.external.includes(alias)) {
137-
throw new Error(`Alias to external: ${source} -> ${alias}`);
138-
}
110+
export function dealiasVirtualNodeJSImport(source: string) {
111+
const alias = env.alias[source];
112+
assert(
113+
alias,
114+
`Expected "${source}" to have a Node.js compat alias, but none was found`
115+
);
116+
assert(
117+
!env.external.includes(alias),
118+
`Unexpected unenv alias to external module: ${source} -> ${alias}`
119+
);
139120
return alias;
140121
}

0 commit comments

Comments
 (0)