Skip to content

Commit

Permalink
fix: keep server assets on server build (#1258)
Browse files Browse the repository at this point in the history
- 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]>
  • Loading branch information
hi-ogawa and dai-shi authored Feb 22, 2025
1 parent ec0c419 commit 0f09d95
Show file tree
Hide file tree
Showing 12 changed files with 205 additions and 2 deletions.
23 changes: 23 additions & 0 deletions e2e/fixtures/rsc-asset/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "rsc-asset",
"version": "0.1.0",
"type": "module",
"private": true,
"scripts": {
"dev": "waku dev",
"build": "waku build",
"start": "waku start"
},
"dependencies": {
"react": "19.0.0",
"react-dom": "19.0.0",
"react-server-dom-webpack": "19.0.0",
"waku": "workspace:*"
},
"devDependencies": {
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"typescript": "^5.7.3",
"vite": "6.1.0"
}
}
31 changes: 31 additions & 0 deletions e2e/fixtures/rsc-asset/src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import fs from 'node:fs';
import testClientTxtUrl from './test-client.txt?no-inline';

const App = (_: { name: string }) => {
// vite doesn't handle `new URL` for ssr,
// so this is handled by a custom plugin in waku.config.ts
const testServerTxtUrl = new URL('./test-server.txt', import.meta.url);

return (
<html>
<head>
<title>e2e-rsc-asset</title>
</head>
<body>
<main>
<div>
client asset:{' '}
<a href={testClientTxtUrl} data-testid="client-link">
{testClientTxtUrl}
</a>
</div>
<div data-testid="server-file">
server asset: {fs.readFileSync(testServerTxtUrl, 'utf-8')}
</div>
</main>
</body>
</html>
);
};

export default App;
1 change: 1 addition & 0 deletions e2e/fixtures/rsc-asset/src/components/test-client.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
test-client-ok
1 change: 1 addition & 0 deletions e2e/fixtures/rsc-asset/src/components/test-server.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
test-server-ok
18 changes: 18 additions & 0 deletions e2e/fixtures/rsc-asset/src/entries.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { unstable_defineEntries as defineEntries } from 'waku/minimal/server';

import App from './components/App.js';

const entries: ReturnType<typeof defineEntries> = defineEntries({
handleRequest: async (input, { renderRsc }) => {
if (input.type === 'component') {
return renderRsc({ App: <App name={input.rscPath || 'Waku'} /> });
}
if (input.type === 'function') {
const value = await input.fn(...input.args);
return renderRsc({ _value: value });
}
},
handleBuild: () => null,
});

export default entries;
13 changes: 13 additions & 0 deletions e2e/fixtures/rsc-asset/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { Root, Slot } from 'waku/minimal/client';

const rootElement = (
<StrictMode>
<Root>
<Slot id="App" />
</Root>
</StrictMode>
);

createRoot(document as any).render(rootElement);
17 changes: 17 additions & 0 deletions e2e/fixtures/rsc-asset/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"strict": true,
"target": "esnext",
"downlevelIteration": true,
"esModuleInterop": true,
"module": "nodenext",
"skipLibCheck": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"types": ["react/experimental", "vite/client"],
"jsx": "react-jsx",
"outDir": "./dist",
"composite": true
},
"include": ["./src", "./waku.config.ts"]
}
37 changes: 37 additions & 0 deletions e2e/fixtures/rsc-asset/waku.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { defineConfig } from 'waku/config';
import type { Plugin } from 'vite';
import path from 'node:path';
import fs from 'node:fs';

export default defineConfig({
unstable_viteConfigs: {
'build-server': () => ({
plugins: [importMetaUrlServerPlugin()],
}),
},
});

// emit asset and rewrite `new URL("./xxx", import.meta.url)` syntax for build.
function importMetaUrlServerPlugin(): Plugin {
// https://github.com/vitejs/vite/blob/0f56e1724162df76fffd5508148db118767ebe32/packages/vite/src/node/plugins/assetImportMetaUrl.ts#L51-L52
const assetImportMetaUrlRE =
/\bnew\s+URL\s*\(\s*('[^']+'|"[^"]+"|`[^`]+`)\s*,\s*import\.meta\.url\s*(?:,\s*)?\)/dg;

return {
name: 'test-server-asset',
transform(code, id) {
return code.replace(assetImportMetaUrlRE, (s, match) => {
const absPath = path.resolve(path.dirname(id), match.slice(1, -1));
if (fs.existsSync(absPath)) {
const referenceId = this.emitFile({
type: 'asset',
name: path.basename(absPath),
source: new Uint8Array(fs.readFileSync(absPath)),
});
return `new URL(import.meta.ROLLUP_FILE_URL_${referenceId})`;
}
return s;
});
},
};
}
31 changes: 31 additions & 0 deletions e2e/rsc-asset.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { expect } from '@playwright/test';

import { test, prepareNormalSetup } from './utils.js';

const startApp = prepareNormalSetup('rsc-asset');

for (const mode of ['DEV', 'PRD'] as const) {
test.describe(`rsc-asset: ${mode}`, () => {
let port: number;
let stopApp: () => Promise<void>;
test.beforeAll(async () => {
({ port, stopApp } = await startApp(mode));
});
test.afterAll(async () => {
await stopApp();
});

test('basic', async ({ page }) => {
await page.goto(`http://localhost:${port}/`);

// server asset
await expect(page.getByTestId('server-file')).toContainText(
'server asset: test-server-ok',
);

// client asset
await page.getByTestId('client-link').click();
await page.getByText('test-client-ok').click();
});
});
}
4 changes: 2 additions & 2 deletions packages/waku/src/lib/builder/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ import {
import { extendViteConfig } from '../utils/vite-config.js';
import {
appendFile,
copyFile,
createWriteStream,
existsSync,
mkdir,
readdir,
readFile,
rename,
unlink,
writeFile,
} from '../utils/node-fs.js';
Expand Down Expand Up @@ -441,7 +441,7 @@ const buildClientBundle = async (
for (const nonJsAsset of nonJsAssets) {
const from = joinPath(rootDir, config.distDir, nonJsAsset);
const to = joinPath(rootDir, config.distDir, DIST_PUBLIC, nonJsAsset);
await rename(from, to);
await copyFile(from, to);
}
return clientBuildOutput;
};
Expand Down
28 changes: 28 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions tsconfig.e2e.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
},
"include": ["playwright.config.base.ts", "playwright.config.ts", "./e2e"],
"references": [
{
"path": "./e2e/fixtures/rsc-asset/tsconfig.json"
},
{
"path": "./e2e/fixtures/rsc-basic/tsconfig.json"
},
Expand Down

0 comments on commit 0f09d95

Please sign in to comment.