Skip to content

Commit 0f09d95

Browse files
hi-ogawadai-shi
andauthored
fix: keep server assets on server build (#1258)
- Related #1245 While trying to come up with a simpler example, I realized that server-only asset concept doesn't actually exist as a builtin Vite feature and this can be only achieved by using Rollup's `emitFile + import.meta.ROLLUP_FILE_URL_(id)` feature on Vite ssr build via custom plugin. So, I think cloudflare wasm import scenario I mentioned in the issue is fairly an edge case. Nonetheless, there might be other vite/rollup plugins relying on `ROLLUP_FILE_URL` feature on server build, so simply replacing `rename` with `copyFile` to save this specific case might be still worth it. --------- Co-authored-by: Daishi Kato <[email protected]>
1 parent ec0c419 commit 0f09d95

File tree

12 files changed

+205
-2
lines changed

12 files changed

+205
-2
lines changed

e2e/fixtures/rsc-asset/package.json

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "rsc-asset",
3+
"version": "0.1.0",
4+
"type": "module",
5+
"private": true,
6+
"scripts": {
7+
"dev": "waku dev",
8+
"build": "waku build",
9+
"start": "waku start"
10+
},
11+
"dependencies": {
12+
"react": "19.0.0",
13+
"react-dom": "19.0.0",
14+
"react-server-dom-webpack": "19.0.0",
15+
"waku": "workspace:*"
16+
},
17+
"devDependencies": {
18+
"@types/react": "^19.0.10",
19+
"@types/react-dom": "^19.0.4",
20+
"typescript": "^5.7.3",
21+
"vite": "6.1.0"
22+
}
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import fs from 'node:fs';
2+
import testClientTxtUrl from './test-client.txt?no-inline';
3+
4+
const App = (_: { name: string }) => {
5+
// vite doesn't handle `new URL` for ssr,
6+
// so this is handled by a custom plugin in waku.config.ts
7+
const testServerTxtUrl = new URL('./test-server.txt', import.meta.url);
8+
9+
return (
10+
<html>
11+
<head>
12+
<title>e2e-rsc-asset</title>
13+
</head>
14+
<body>
15+
<main>
16+
<div>
17+
client asset:{' '}
18+
<a href={testClientTxtUrl} data-testid="client-link">
19+
{testClientTxtUrl}
20+
</a>
21+
</div>
22+
<div data-testid="server-file">
23+
server asset: {fs.readFileSync(testServerTxtUrl, 'utf-8')}
24+
</div>
25+
</main>
26+
</body>
27+
</html>
28+
);
29+
};
30+
31+
export default App;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
test-client-ok
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
test-server-ok
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { unstable_defineEntries as defineEntries } from 'waku/minimal/server';
2+
3+
import App from './components/App.js';
4+
5+
const entries: ReturnType<typeof defineEntries> = defineEntries({
6+
handleRequest: async (input, { renderRsc }) => {
7+
if (input.type === 'component') {
8+
return renderRsc({ App: <App name={input.rscPath || 'Waku'} /> });
9+
}
10+
if (input.type === 'function') {
11+
const value = await input.fn(...input.args);
12+
return renderRsc({ _value: value });
13+
}
14+
},
15+
handleBuild: () => null,
16+
});
17+
18+
export default entries;

e2e/fixtures/rsc-asset/src/main.tsx

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { StrictMode } from 'react';
2+
import { createRoot } from 'react-dom/client';
3+
import { Root, Slot } from 'waku/minimal/client';
4+
5+
const rootElement = (
6+
<StrictMode>
7+
<Root>
8+
<Slot id="App" />
9+
</Root>
10+
</StrictMode>
11+
);
12+
13+
createRoot(document as any).render(rootElement);

e2e/fixtures/rsc-asset/tsconfig.json

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"compilerOptions": {
3+
"strict": true,
4+
"target": "esnext",
5+
"downlevelIteration": true,
6+
"esModuleInterop": true,
7+
"module": "nodenext",
8+
"skipLibCheck": true,
9+
"noUncheckedIndexedAccess": true,
10+
"exactOptionalPropertyTypes": true,
11+
"types": ["react/experimental", "vite/client"],
12+
"jsx": "react-jsx",
13+
"outDir": "./dist",
14+
"composite": true
15+
},
16+
"include": ["./src", "./waku.config.ts"]
17+
}

e2e/fixtures/rsc-asset/waku.config.ts

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { defineConfig } from 'waku/config';
2+
import type { Plugin } from 'vite';
3+
import path from 'node:path';
4+
import fs from 'node:fs';
5+
6+
export default defineConfig({
7+
unstable_viteConfigs: {
8+
'build-server': () => ({
9+
plugins: [importMetaUrlServerPlugin()],
10+
}),
11+
},
12+
});
13+
14+
// emit asset and rewrite `new URL("./xxx", import.meta.url)` syntax for build.
15+
function importMetaUrlServerPlugin(): Plugin {
16+
// https://github.com/vitejs/vite/blob/0f56e1724162df76fffd5508148db118767ebe32/packages/vite/src/node/plugins/assetImportMetaUrl.ts#L51-L52
17+
const assetImportMetaUrlRE =
18+
/\bnew\s+URL\s*\(\s*('[^']+'|"[^"]+"|`[^`]+`)\s*,\s*import\.meta\.url\s*(?:,\s*)?\)/dg;
19+
20+
return {
21+
name: 'test-server-asset',
22+
transform(code, id) {
23+
return code.replace(assetImportMetaUrlRE, (s, match) => {
24+
const absPath = path.resolve(path.dirname(id), match.slice(1, -1));
25+
if (fs.existsSync(absPath)) {
26+
const referenceId = this.emitFile({
27+
type: 'asset',
28+
name: path.basename(absPath),
29+
source: new Uint8Array(fs.readFileSync(absPath)),
30+
});
31+
return `new URL(import.meta.ROLLUP_FILE_URL_${referenceId})`;
32+
}
33+
return s;
34+
});
35+
},
36+
};
37+
}

e2e/rsc-asset.spec.ts

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { expect } from '@playwright/test';
2+
3+
import { test, prepareNormalSetup } from './utils.js';
4+
5+
const startApp = prepareNormalSetup('rsc-asset');
6+
7+
for (const mode of ['DEV', 'PRD'] as const) {
8+
test.describe(`rsc-asset: ${mode}`, () => {
9+
let port: number;
10+
let stopApp: () => Promise<void>;
11+
test.beforeAll(async () => {
12+
({ port, stopApp } = await startApp(mode));
13+
});
14+
test.afterAll(async () => {
15+
await stopApp();
16+
});
17+
18+
test('basic', async ({ page }) => {
19+
await page.goto(`http://localhost:${port}/`);
20+
21+
// server asset
22+
await expect(page.getByTestId('server-file')).toContainText(
23+
'server asset: test-server-ok',
24+
);
25+
26+
// client asset
27+
await page.getByTestId('client-link').click();
28+
await page.getByText('test-client-ok').click();
29+
});
30+
});
31+
}

packages/waku/src/lib/builder/build.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@ import {
2323
import { extendViteConfig } from '../utils/vite-config.js';
2424
import {
2525
appendFile,
26+
copyFile,
2627
createWriteStream,
2728
existsSync,
2829
mkdir,
2930
readdir,
3031
readFile,
31-
rename,
3232
unlink,
3333
writeFile,
3434
} from '../utils/node-fs.js';
@@ -441,7 +441,7 @@ const buildClientBundle = async (
441441
for (const nonJsAsset of nonJsAssets) {
442442
const from = joinPath(rootDir, config.distDir, nonJsAsset);
443443
const to = joinPath(rootDir, config.distDir, DIST_PUBLIC, nonJsAsset);
444-
await rename(from, to);
444+
await copyFile(from, to);
445445
}
446446
return clientBuildOutput;
447447
};

pnpm-lock.yaml

+28
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tsconfig.e2e.json

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
},
77
"include": ["playwright.config.base.ts", "playwright.config.ts", "./e2e"],
88
"references": [
9+
{
10+
"path": "./e2e/fixtures/rsc-asset/tsconfig.json"
11+
},
912
{
1013
"path": "./e2e/fixtures/rsc-basic/tsconfig.json"
1114
},

0 commit comments

Comments
 (0)