Skip to content

Commit ca3cbc4

Browse files
fix node.js compat module resolution (#8118)
* fix node.js compat module resolution Resolves #8108 * add e2e tests for node-compat * fix linting issues * post-review fixups * update changeset
1 parent 35504e9 commit ca3cbc4

33 files changed

+810
-74
lines changed

.changeset/wild-days-jog.md

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
"@cloudflare/vite-plugin": patch
3+
---
4+
5+
fix Node.js compat module resolution
6+
7+
In v0.0.8 we landed support for Vite 6.1 and also switched to using the new Cloudflare owned unenv preset.
8+
Unfortunately, the changes made in that update caused a regression in Node.js support.
9+
This became apparent only when the plugin was being used with certain package managers and outside of the workers-sdk monorepo.
10+
11+
The unenv polyfills that get compiled into the Worker are transitive dependencies of this plugin, not direct dependencies of the user's application were the plugin is being used.
12+
This is on purpose to avoid the user having to install these dependencies themselves.
13+
14+
Unfortunately, the changes in 0.0.8 did not correctly resolve the polyfills from `@cloudflare/unenv-preset` and `unenv` when the dependencies were not also installed directly into the user's application.
15+
16+
The approach was incorrectly relying upon setting the `importer` in calls to Vite's `resolve(id, importer)` method to base the resolution in the context of the vite plugin package rather than the user's application.
17+
This doesn't work because the `importer` is only relevant when the `id` is relative, and not a bare module specifier in the case of the unenv polyfills.
18+
19+
This change fixes how these id are resolved in the plugin by manually resolving the path at the appropriate point, while still leveraging Vite's resolution pipeline to handle aliasing, and dependency optimization.
20+
21+
This change now introduces e2e tests that checks that isolated installations of the plugin works with npm, pnpm and yarn.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# vite-plugin e2e tests
2+
3+
This directory contains e2e test that give more confidence that the plugin will work in real world scenarios outside the comfort of this monorepo.
4+
5+
In general, these tests create test projects by copying a fixture from the `fixtures` directory into a temporary directory and then installing the local builds of the plugin along with its dependencies.
6+
7+
## Running the tests
8+
9+
Simply use turbo to run the tests from the root of the monorepo.
10+
This will also ensure that the required dependencies have all been built before running the tests.
11+
12+
```sh
13+
pnpm test:e2e -F @cloudflare/vite-plugin
14+
```
15+
16+
## Developing e2e tests
17+
18+
These tests use a mock npm registry where the built plugin has been published.
19+
20+
The registry is booted up and loaded with the local build of the plugin and its local dependencies in the global-setup.ts file that runs once at the start of the e2e test run, and the server is killed and its caches removed at the end of the test run.
21+
22+
The Vite `test` function is an extended with additional helpers to setup clean copies of fixtures outside of the monorepo so that they can be isolated from any other dependencies in the project.
23+
24+
The simplest test looks like:
25+
26+
```ts
27+
test("can serve a Worker request", async ({ expect, seed, viteDev }) => {
28+
const projectPath = await seed("basic");
29+
runCommand(`pnpm install`, projectPath);
30+
31+
const proc = await viteDev(projectPath);
32+
const url = await waitForReady(proc);
33+
expect(await fetchJson(url + "/api/")).toEqual({ name: "Cloudflare" });
34+
});
35+
```
36+
37+
- The `seed()` helper makes a copy of the named fixture into a temporary directory. It returns the path to the directory containing the copy (`projectPath` above). This directory will be deleted at the end of the test.
38+
- The `runCommand()` helper simply executes a one-shot command and resolves when it has exited. You can use this to install the dependencies of the fixture from the mock npm registry, as in the example above.
39+
- The `viteDev()` helper boots up the `vite dev` command and returns an object that can be used to monitor its output. The process will be killed at the end of the test.
40+
- The `waitForReady()` helper will resolve when the `vite dev` process has output its ready message, from which it will parse the url that can be fetched in the test.
41+
- The `fetchJson()` helper makes an Undici fetch to the url parsing the response into JSON. It will retry every 250ms for up to 10 secs to minimize flakes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { describe } from "vitest";
2+
import { fetchJson, runCommand, test, waitForReady } from "./helpers.js";
3+
4+
describe("node compatibility", () => {
5+
describe.each(["pnpm", "npm", "yarn"])("using %s", (pm) => {
6+
test("can serve a Worker request", async ({ expect, seed, viteDev }) => {
7+
const projectPath = await seed("basic");
8+
runCommand(`${pm} install`, projectPath);
9+
10+
const proc = await viteDev(projectPath);
11+
const url = await waitForReady(proc);
12+
expect(await fetchJson(url + "/api/")).toEqual({ name: "Cloudflare" });
13+
});
14+
});
15+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
pnpm-debug.log*
8+
lerna-debug.log*
9+
10+
node_modules
11+
dist
12+
dist-ssr
13+
*.local
14+
15+
# Editor directories and files
16+
.vscode/*
17+
!.vscode/extensions.json
18+
.idea
19+
.DS_Store
20+
*.suo
21+
*.ntvs*
22+
*.njsproj
23+
*.sln
24+
*.sw?
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# React + TypeScript + Vite
2+
3+
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4+
5+
Currently, two official plugins are available:
6+
7+
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8+
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9+
10+
## Expanding the ESLint configuration
11+
12+
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
13+
14+
- Configure the top-level `parserOptions` property like this:
15+
16+
```js
17+
export default tseslint.config({
18+
languageOptions: {
19+
// other options...
20+
parserOptions: {
21+
project: ["./tsconfig.node.json", "./tsconfig.app.json"],
22+
tsconfigRootDir: import.meta.dirname,
23+
},
24+
},
25+
});
26+
```
27+
28+
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
29+
- Optionally add `...tseslint.configs.stylisticTypeChecked`
30+
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
31+
32+
```js
33+
// eslint.config.js
34+
import react from "eslint-plugin-react";
35+
36+
export default tseslint.config({
37+
// Set the react version
38+
settings: { react: { version: "18.3" } },
39+
plugins: {
40+
// Add the react plugin
41+
react,
42+
},
43+
rules: {
44+
// other rules...
45+
// Enable its recommended rules
46+
...react.configs.recommended.rules,
47+
...react.configs["jsx-runtime"].rules,
48+
},
49+
});
50+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
interface Env {
2+
ASSETS: Fetcher;
3+
}
4+
5+
export default {
6+
fetch(request, env) {
7+
const url = new URL(request.url);
8+
9+
if (url.pathname.startsWith("/api/")) {
10+
return Response.json({
11+
name: "Cloudflare",
12+
});
13+
}
14+
15+
return env.ASSETS.fetch(request);
16+
},
17+
} satisfies ExportedHandler<Env>;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import js from "@eslint/js";
2+
import reactHooks from "eslint-plugin-react-hooks";
3+
import reactRefresh from "eslint-plugin-react-refresh";
4+
import globals from "globals";
5+
import tseslint from "typescript-eslint";
6+
7+
export default tseslint.config(
8+
{ ignores: ["dist"] },
9+
{
10+
extends: [js.configs.recommended, ...tseslint.configs.recommended],
11+
files: ["**/*.{ts,tsx}"],
12+
languageOptions: {
13+
ecmaVersion: 2020,
14+
globals: globals.browser,
15+
},
16+
plugins: {
17+
"react-hooks": reactHooks,
18+
"react-refresh": reactRefresh,
19+
},
20+
rules: {
21+
...reactHooks.configs.recommended.rules,
22+
"react-refresh/only-export-components": [
23+
"warn",
24+
{ allowConstantExport: true },
25+
],
26+
},
27+
}
28+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>Vite + React + TS</title>
8+
</head>
9+
<body>
10+
<div id="root"></div>
11+
<script type="module" src="/src/main.tsx"></script>
12+
</body>
13+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"name": "cloudflare-vite-tutorial",
3+
"version": "0.0.0",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"build": "tsc -b && vite build",
8+
"dev": "vite",
9+
"lint": "eslint .",
10+
"preview": "vite preview"
11+
},
12+
"dependencies": {
13+
"react": "^19.0.0",
14+
"react-dom": "^19.0.0"
15+
},
16+
"devDependencies": {
17+
"@cloudflare/vite-plugin": "https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/13293829928/npm-package-cloudflare-vite-plugin-8118",
18+
"@cloudflare/workers-types": "^4.20250204.0",
19+
"@eslint/js": "^9.19.0",
20+
"@types/react": "^19.0.8",
21+
"@types/react-dom": "^19.0.3",
22+
"@vitejs/plugin-react": "^4.3.4",
23+
"eslint": "^9.19.0",
24+
"eslint-plugin-react-hooks": "^5.0.0",
25+
"eslint-plugin-react-refresh": "^0.4.18",
26+
"globals": "^15.14.0",
27+
"typescript": "~5.7.2",
28+
"typescript-eslint": "^8.22.0",
29+
"vite": "^6.1.0",
30+
"wrangler": "^3.108.1"
31+
}
32+
}
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#root {
2+
max-width: 1280px;
3+
margin: 0 auto;
4+
padding: 2rem;
5+
text-align: center;
6+
}
7+
8+
.logo {
9+
height: 6em;
10+
padding: 1.5em;
11+
will-change: filter;
12+
transition: filter 300ms;
13+
}
14+
.logo:hover {
15+
filter: drop-shadow(0 0 2em #646cffaa);
16+
}
17+
.logo.react:hover {
18+
filter: drop-shadow(0 0 2em #61dafbaa);
19+
}
20+
21+
@keyframes logo-spin {
22+
from {
23+
transform: rotate(0deg);
24+
}
25+
to {
26+
transform: rotate(360deg);
27+
}
28+
}
29+
30+
@media (prefers-reduced-motion: no-preference) {
31+
a:nth-of-type(2) .logo {
32+
animation: logo-spin infinite 20s linear;
33+
}
34+
}
35+
36+
.card {
37+
padding: 2em;
38+
}
39+
40+
.read-the-docs {
41+
color: #888;
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// src/App.tsx
2+
3+
import viteLogo from "/vite.svg";
4+
import { useState } from "react";
5+
import reactLogo from "./assets/react.svg";
6+
import "./App.css";
7+
8+
function App() {
9+
const [count, setCount] = useState(0);
10+
const [name, setName] = useState("unknown");
11+
12+
return (
13+
<>
14+
<div>
15+
<a href="https://vite.dev" target="_blank">
16+
<img src={viteLogo} className="logo" alt="Vite logo" />
17+
</a>
18+
<a href="https://react.dev" target="_blank">
19+
<img src={reactLogo} className="logo react" alt="React logo" />
20+
</a>
21+
</div>
22+
<h1>Vite + React</h1>
23+
<div className="card">
24+
<button
25+
onClick={() => setCount((count) => count + 1)}
26+
aria-label="increment"
27+
>
28+
count is {count}
29+
</button>
30+
<p>
31+
Edit <code>src/App.tsx</code> and save to test HMR
32+
</p>
33+
</div>
34+
<div className="card">
35+
<button
36+
onClick={() => {
37+
fetch("/api/")
38+
.then((res) => res.json() as Promise<{ name: string }>)
39+
.then((data) => setName(data.name));
40+
}}
41+
aria-label="get name"
42+
>
43+
Name from API is: {name}
44+
</button>
45+
<p>
46+
Edit <code>api/index.ts</code> to change the name
47+
</p>
48+
</div>
49+
<p className="read-the-docs">
50+
Click on the Vite and React logos to learn more
51+
</p>
52+
</>
53+
);
54+
}
55+
56+
export default App;
Loading

0 commit comments

Comments
 (0)