Skip to content

Commit 8a17526

Browse files
committed
feat: implement Node ESM loader
1 parent 55972e5 commit 8a17526

26 files changed

+526
-67
lines changed

ci/ci.test.ts

+46-58
Original file line numberDiff line numberDiff line change
@@ -18,75 +18,63 @@ const browser = await puppeteer.launch({
1818
const pages = await browser.pages();
1919
const page = pages[0];
2020

21-
const cases = [
22-
{
23-
framework: "simple-standalone",
24-
env: "development",
25-
file: "handler.ts",
26-
},
27-
28-
{ framework: "simple-standalone", env: "production", file: "handler.ts" },
29-
30-
{ framework: "express", env: "development", file: "routes/home.ts" },
31-
{ framework: "express", env: "production", file: "routes/home.ts" },
32-
33-
{ framework: "fastify", env: "development", file: "routes/home.ts" },
34-
{ framework: "fastify", env: "production", file: "routes/home.ts" },
35-
36-
{ framework: "koa", env: "development", file: "routes/home.ts" },
37-
{ framework: "koa", env: "production", file: "routes/home.ts" },
38-
39-
{ framework: "hapi", env: "development", file: "routes/home.ts" },
40-
{ framework: "hapi", env: "production", file: "routes/home.ts" },
41-
42-
{
43-
framework: "ssr-react-express",
44-
env: "development",
45-
file: "pages/Home.tsx",
46-
},
47-
{
48-
framework: "ssr-react-express",
49-
env: "production",
50-
file: "pages/Home.tsx",
51-
},
52-
{
53-
framework: "ssr-vue-express",
54-
env: "development",
55-
file: "pages/Home.vue",
56-
},
57-
{
58-
framework: "ssr-vue-express",
59-
env: "production",
60-
file: "pages/Home.vue",
61-
},
62-
{
63-
framework: "vite-plugin-ssr",
64-
env: "development",
65-
file: "pages/index/index.page.tsx",
66-
},
67-
{
68-
framework: "vite-plugin-ssr",
69-
env: "production",
70-
file: "pages/index/index.page.tsx",
71-
},
72-
] as const;
73-
74-
describe.each(cases)("$framework - $env", ({ framework, env, file }) => {
21+
const baseCases: Array<{
22+
framework: string;
23+
file: string;
24+
}> = [
25+
{ framework: "simple-standalone", file: "handler.ts" },
26+
{ framework: "express", file: "routes/home.ts" },
27+
{ framework: "fastify", file: "routes/home.ts" },
28+
{ framework: "koa", file: "routes/home.ts" },
29+
{ framework: "hapi", file: "routes/home.ts" },
30+
{ framework: "ssr-react-express", file: "pages/Home.tsx" },
31+
{ framework: "ssr-vue-express", file: "pages/Home.vue" },
32+
{ framework: "vite-plugin-ssr", file: "pages/index/index.page.tsx" },
33+
];
34+
35+
const [major, minor] = process.version
36+
.slice(1)
37+
.split(".")
38+
.map((x) => Number(x));
39+
40+
const cases: Array<{
41+
framework: string;
42+
file: string;
43+
env: "production" | "development" | "with-loader";
44+
}> = [
45+
...baseCases.map((x) => ({ ...x, env: "production" as const })),
46+
...baseCases.map((x) => ({ ...x, env: "development" as const })),
47+
];
48+
49+
const loaderAvailable = major > 16 || (major === 16 && minor >= 12);
50+
if (loaderAvailable) {
51+
cases.push(...baseCases.map((x) => ({ ...x, env: "with-loader" as const })));
52+
}
53+
54+
describe.each(cases)("$framework - $env ", ({ framework, env, file }) => {
7555
const ssr = framework.includes("ssr");
7656
const dir = path.resolve(__dirname, "..", "examples", framework);
7757

7858
let cp: ChildProcess | undefined;
7959

8060
beforeAll(async () => {
8161
const command =
82-
env === "development"
83-
? "pnpm exec vite serve --strictPort --port 3000 --logLevel silent"
84-
: "pnpm run build && pnpm start";
62+
env === "production"
63+
? "pnpm run build && pnpm start"
64+
: "pnpm exec vite serve --strictPort --port 3000 --logLevel silent";
8565

8666
cp = spawn(command, {
8767
shell: true,
8868
stdio: "inherit",
8969
cwd: dir,
70+
env: {
71+
...process.env,
72+
...(env === "with-loader" && {
73+
NODE_OPTIONS:
74+
(process.env.NODE_OPTIONS ?? "") +
75+
" -r vavite/suppress-loader-warnings --loader vavite/node-loader",
76+
}),
77+
},
9078
});
9179

9280
// Wait until server is ready
@@ -155,7 +143,7 @@ describe.each(cases)("$framework - $env", ({ framework, env, file }) => {
155143
}, 15_000);
156144
}
157145

158-
if (env === "development") {
146+
if (env !== "production") {
159147
test("hot reloads page", async () => {
160148
await page.goto(TEST_HOST);
161149

examples/vite-plugin-ssr/server/index.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ startServer();
99
async function startServer() {
1010
const app = express();
1111

12-
if (import.meta.env.PROD) {
12+
if (!httpDevServer) {
1313
app.use(express.static("dist/client"));
1414
}
1515

@@ -24,11 +24,11 @@ async function startServer() {
2424
res.status(statusCode).send(body);
2525
});
2626

27-
if (import.meta.env.PROD) {
27+
if (httpDevServer) {
28+
httpDevServer!.on("request", app);
29+
} else {
2830
const port = process.env.PORT || 3000;
2931
app.listen(port);
3032
console.log(`Server running at http://localhost:${port}`);
31-
} else {
32-
httpDevServer!.on("request", app);
3333
}
3434
}

examples/vite-plugin-ssr/vite.config.ts

+7
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,12 @@ export default defineConfig({
3131
}),
3232
react(),
3333
ssr({ disableAutoFullBuild: true }),
34+
{
35+
name: "test",
36+
37+
configResolved(config) {
38+
console.log(config.experimental);
39+
},
40+
},
3441
],
3542
});

examples/with-loader/.stackblitzrc

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"installDependencies": true,
3+
"startCommand": "npm run dev"
4+
}

examples/with-loader/entry-node.ts

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import express from "express";
2+
3+
const app = express();
4+
5+
app.get("/foo", (req, res) => {
6+
res.send("foo");
7+
});
8+
9+
export default app;
10+
11+
if (typeof __vite_dev_server__ === "undefined") {
12+
app.listen(3000, () => console.log("Listening on http://localhost:3000"));
13+
}

examples/with-loader/package.json

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "@vavite/example-with-loader",
3+
"type": "module",
4+
"private": true,
5+
"scripts": {
6+
"start": "node dist",
7+
"dev": "vite",
8+
"build": "vite build --ssr --mode=production"
9+
},
10+
"devDependencies": {
11+
"@types/express": "^4.17.14",
12+
"@types/node": "^18.11.14",
13+
"@vavite/node-loader": "1.5.2",
14+
"typescript": "^4.9.4",
15+
"vite": "^4.0.1"
16+
},
17+
"dependencies": {
18+
"express": "^4.18.2"
19+
}
20+
}

examples/with-loader/readme.md

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Vavite Simple Standalone example
2+
3+
Simplest example that specifies a `handlerEntry` and handles incoming requests.
4+
5+
> [Try on StackBlitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/simple-standalone)
6+
7+
Clone with:
8+
9+
```bash
10+
npx degit cyco130/vavite/examples/simple-standalone
11+
```
12+
13+
> All examples have `"type": "module"` in their `package.json`.
14+
>
15+
> - For Vite v2, remove it to use CommonJS (CJS).
16+
> - If you want to use CommonJS with Vite v3, add `legacy.buildSsrCjsExternalHeuristics: true` to your Vite config.

examples/with-loader/some-module.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function greet(message: string) {
2+
console.log(message);
3+
}

examples/with-loader/tsconfig.json

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"compilerOptions": {
3+
"target": "es2020",
4+
"module": "ESNext",
5+
"esModuleInterop": true,
6+
"forceConsistentCasingInFileNames": true,
7+
"strict": true,
8+
"skipLibCheck": true,
9+
"moduleResolution": "Node"
10+
}
11+
}

examples/with-loader/vite.config.ts

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { defineConfig, ViteDevServer } from "vite";
2+
3+
declare global {
4+
// eslint-disable-next-line no-var
5+
var __vite_dev_server__: ViteDevServer | undefined;
6+
}
7+
8+
export default defineConfig({
9+
experimental: {
10+
skipSsrTransform: true,
11+
},
12+
appType: "custom",
13+
plugins: [
14+
{
15+
name: "vite-plugin-node-loader",
16+
apply: "serve",
17+
async configureServer(server) {
18+
global.__vite_dev_server__ = server;
19+
20+
return () => {
21+
server.middlewares.use(async (req, res, next) => {
22+
const entry = await (
23+
(0, eval)(`import("./entry-node?vavite-entry")`) as Promise<
24+
typeof import("./entry-node")
25+
>
26+
).then((m) => m.default);
27+
28+
entry(req as any, res as any, next);
29+
});
30+
};
31+
},
32+
buildEnd() {
33+
delete global.__vite_dev_server__;
34+
},
35+
config() {
36+
return {
37+
optimizeDeps: { include: [] },
38+
};
39+
},
40+
},
41+
],
42+
});

examples/with-loader/x.module.css

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.xxx {
2+
background: green;
3+
}

examples/with-loader/y.css

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.main {
2+
color: red;
3+
}

packages/connect/src/index.ts

+26-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
1-
import type { Plugin, UserConfig } from "vite";
1+
import type { Plugin, UserConfig, ViteDevServer } from "vite";
22
import path from "path";
33
import url from "url";
44

5+
declare global {
6+
// eslint-disable-next-line no-var
7+
var __vite_dev_server__: ViteDevServer | undefined;
8+
// eslint-disable-next-line no-var
9+
var __vavite_loader__: boolean;
10+
}
11+
12+
const hasLoader = typeof __vavite_loader__ !== "undefined";
13+
514
const dirname =
615
typeof __dirname === "undefined"
716
? url.fileURLToPath(new URL(".", import.meta.url))
@@ -84,6 +93,7 @@ export default function vaviteConnect(
8493
// This silences the "could not auto-determine entry point" warning
8594
include: [],
8695
},
96+
experimental: hasLoader ? { skipSsrTransform: true } : undefined,
8797
};
8898

8999
if (env.command === "build" && config.build?.ssr) {
@@ -113,6 +123,15 @@ export default function vaviteConnect(
113123
},
114124

115125
configureServer(server) {
126+
if (hasLoader) {
127+
global.__vite_dev_server__ = server;
128+
server.ssrLoadModule = (id) => import(id + "?vavite-entry");
129+
server.ssrFixStacktrace = () => {
130+
/* noop */
131+
};
132+
server.ssrRewriteStacktrace = (s) => s;
133+
}
134+
116135
function addMiddleware() {
117136
server.middlewares.use(async (req, res) => {
118137
function renderError(status: number, message: string) {
@@ -146,6 +165,12 @@ export default function vaviteConnect(
146165
addMiddleware();
147166
}
148167
},
168+
169+
buildEnd() {
170+
if (hasLoader) {
171+
global.__vite_dev_server__ = undefined;
172+
}
173+
},
149174
},
150175
];
151176
}

packages/expose-vite-dev-server/src/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ export default function vaviteDevServerPlugin(): Plugin {
2828
resolveId(source, _importer, options) {
2929
if (
3030
(source === "@vavite/expose-vite-dev-server/vite-dev-server" ||
31-
source === "vavite/vite-dev-server") &&
31+
source === "vavite/vite-dev-server" ||
32+
source === "virtual:vavite-vite-dev-server") &&
3233
dev &&
3334
options.ssr
3435
) {

packages/node-loader/.eslintrc.cjs

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
require("@cyco130/eslint-config/patch");
2+
3+
module.exports = {
4+
extends: ["@cyco130/eslint-config/node"],
5+
parserOptions: { tsconfigRootDir: __dirname },
6+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default {
2+
"**/*.ts?(x)": [
3+
() => "tsc -p tsconfig.json --noEmit",
4+
"eslint --max-warnings 0 --ignore-pattern dist",
5+
],
6+
"*": "prettier --ignore-unknown --write",
7+
};

0 commit comments

Comments
 (0)