Skip to content

Commit 13518ae

Browse files
himself65dai-shi
andauthored
fix: standalone 07_router example (#322)
* test: add standalone test * fix: crypto * fix it * restore waku-client * fix one more * does this fix? * test: improve * revert: code * Revert "revert: code" This reverts commit 8fc45bd. * fix: code * refactor --------- Co-authored-by: daishi <[email protected]>
1 parent c599251 commit 13518ae

File tree

7 files changed

+162
-43
lines changed

7 files changed

+162
-43
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ dist
88
.devcontainer
99
.vscode
1010
*.tsbuildinfo
11+
.cache

e2e/07_router_standalone.spec.ts

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { debugChildProcess, getFreePort, test } from './utils.js';
2+
import { fileURLToPath } from 'node:url';
3+
import { cp, mkdir, rm } from 'node:fs/promises';
4+
import { exec, execSync } from 'node:child_process';
5+
import { expect, type Page } from '@playwright/test';
6+
import crypto from 'node:crypto';
7+
import waitPort from 'wait-port';
8+
9+
const cacheDir = fileURLToPath(new URL('./.cache', import.meta.url));
10+
const exampleDir = fileURLToPath(
11+
new URL('../examples/07_router', import.meta.url),
12+
);
13+
const wakuDir = fileURLToPath(new URL('../packages/waku', import.meta.url));
14+
15+
async function testRouterExample(page: Page, port: number) {
16+
await waitPort({
17+
port,
18+
});
19+
20+
await page.goto(`http://localhost:${port}`);
21+
await expect(page.getByRole('heading', { name: 'Home' })).toBeVisible();
22+
23+
await page.click("a[href='/foo']");
24+
25+
await expect(page.getByRole('heading', { name: 'Foo' })).toBeVisible();
26+
27+
await page.goto(`http://localhost:${port}/foo`);
28+
await expect(page.getByRole('heading', { name: 'Foo' })).toBeVisible();
29+
}
30+
31+
test.describe('07_router standalone', () => {
32+
const dirname = crypto.randomUUID();
33+
test.beforeAll('copy code', async () => {
34+
await mkdir(cacheDir, {
35+
recursive: true,
36+
});
37+
await cp(exampleDir, `${cacheDir}/${dirname}`, { recursive: true });
38+
// cleanup node_modules and output
39+
await rm(`${cacheDir}/${dirname}/node_modules`, {
40+
recursive: true,
41+
force: true,
42+
});
43+
await rm(`${cacheDir}/${dirname}/dist`, { recursive: true, force: true });
44+
execSync('pnpm install', {
45+
cwd: `${cacheDir}/${dirname}`,
46+
stdio: 'inherit',
47+
});
48+
// copy waku
49+
await cp(wakuDir, `${cacheDir}/${dirname}/node_modules/waku`, {
50+
recursive: true,
51+
});
52+
});
53+
54+
test('should prod work', async ({ page }) => {
55+
execSync('pnpm run build', {
56+
cwd: `${cacheDir}/${dirname}`,
57+
stdio: 'inherit',
58+
});
59+
const port = await getFreePort();
60+
const cp = exec('pnpm run start', {
61+
cwd: `${cacheDir}/${dirname}`,
62+
env: {
63+
...process.env,
64+
PORT: `${port}`,
65+
},
66+
});
67+
debugChildProcess(cp);
68+
await testRouterExample(page, port);
69+
cp.kill();
70+
});
71+
72+
test('should dev work', async ({ page }) => {
73+
const port = await getFreePort();
74+
const cp = exec('pnpm run dev', {
75+
cwd: `${cacheDir}/${dirname}`,
76+
env: {
77+
...process.env,
78+
PORT: `${port}`,
79+
},
80+
});
81+
debugChildProcess(cp);
82+
await testRouterExample(page, port);
83+
cp.kill();
84+
});
85+
});

e2e/utils.ts

+11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import net from 'node:net';
22
import { test as basicTest } from '@playwright/test';
33
import type { ConsoleMessage } from '@playwright/test';
4+
import type { ChildProcess } from 'node:child_process';
45

56
export async function getFreePort(): Promise<number> {
67
return new Promise<number>((res) => {
@@ -12,6 +13,16 @@ export async function getFreePort(): Promise<number> {
1213
});
1314
}
1415

16+
export function debugChildProcess(cp: ChildProcess) {
17+
cp.stdout?.on('data', (data) => {
18+
console.log(`stdout: ${data}`);
19+
});
20+
21+
cp.stderr?.on('data', (data) => {
22+
console.error(`stderr: ${data}`);
23+
});
24+
}
25+
1526
export const test = basicTest.extend({
1627
page: async ({ page }, use) => {
1728
const unexpectedErrors: RegExp[] = [

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

+18-7
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@ import type { RollupLog, LoggingFunction } from 'rollup';
88

99
import { resolveConfig } from '../config.js';
1010
import type { Config, ResolvedConfig } from '../config.js';
11-
import { joinPath, extname, filePathToFileURL } from '../utils/path.js';
11+
import {
12+
joinPath,
13+
extname,
14+
filePathToFileURL,
15+
fileURLToFilePath,
16+
decodeFilePathFromAbsolute,
17+
} from '../utils/path.js';
1218
import {
1319
createReadStream,
1420
createWriteStream,
@@ -140,15 +146,18 @@ const buildServerBundle = async (
140146
const serverBuildOutput = await buildVite({
141147
plugins: [
142148
nonjsResolvePlugin(),
143-
rscTransformPlugin(
144-
true,
145-
config.assetsDir,
146-
{
147-
[WAKU_CLIENT_MODULE]: WAKU_CLIENT_MODULE_VALUE,
149+
rscTransformPlugin({
150+
isBuild: true,
151+
assetsDir: config.assetsDir,
152+
clientEntryFiles: {
153+
// FIXME this seems very ad-hoc
154+
[WAKU_CLIENT_MODULE]: decodeFilePathFromAbsolute(
155+
joinPath(fileURLToFilePath(import.meta.url), '../../../client.js'),
156+
),
148157
...clientEntryFiles,
149158
},
150159
serverEntryFiles,
151-
),
160+
}),
152161
],
153162
ssr: {
154163
resolve: {
@@ -208,6 +217,8 @@ export function loadModule(id) {
208217
return import('./${psDir}/${RSDW_CLIENT_MODULE}.js');
209218
case 'public/${WAKU_CLIENT_MODULE}':
210219
return import('./${psDir}/${WAKU_CLIENT_MODULE}.js');
220+
case '${psDir}/${WAKU_CLIENT_MODULE}.js':
221+
return import('./${psDir}/${WAKU_CLIENT_MODULE}.js');
211222
${Object.entries(serverEntryFiles || {})
212223
.map(
213224
([k]) => `

packages/waku/src/lib/handlers/dev-worker-impl.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ const moduleImports: Set<string> = new Set();
9696
const mergedViteConfig = await mergeUserViteConfig({
9797
plugins: [
9898
nonjsResolvePlugin(),
99-
rscTransformPlugin(false),
99+
rscTransformPlugin({ isBuild: false }),
100100
rscReloadPlugin(moduleImports, (type) => {
101101
const mesg: MessageRes = { type };
102102
parentPort!.postMessage(mesg);

packages/waku/src/lib/handlers/handler-dev.ts

+21-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { default as viteReact } from '@vitejs/plugin-react';
55
import type { EntriesDev } from '../../server.js';
66
import { resolveConfig } from '../config.js';
77
import type { Config } from '../config.js';
8-
import { joinPath } from '../utils/path.js';
8+
import { joinPath, decodeFilePathFromAbsolute } from '../utils/path.js';
99
import { endStream } from '../utils/stream.js';
1010
import { renderHtml } from '../renderers/html-renderer.js';
1111
import { decodeInput, hasStatusCode } from '../renderers/utils.js';
@@ -188,9 +188,28 @@ export function createHandler<
188188
}
189189
return;
190190
}
191+
// HACK re-export "?v=..." URL to avoid dual module hazard.
192+
const viteUrl = req.url.toString().slice(req.url.origin.length);
193+
const fname = viteUrl.startsWith(config.basePath + '@fs/')
194+
? decodeFilePathFromAbsolute(
195+
viteUrl.slice(config.basePath.length + '@fs'.length),
196+
)
197+
: joinPath(vite.config.root, viteUrl);
198+
for (const item of vite.moduleGraph.idToModuleMap.values()) {
199+
if (
200+
item.file === fname &&
201+
item.url !== viteUrl &&
202+
!item.url.includes('?html-proxy')
203+
) {
204+
res.setHeader('Content-Type', 'application/javascript');
205+
res.setStatus(200);
206+
endStream(res.stream, `export * from "${item.url}";`);
207+
return;
208+
}
209+
}
191210
const viteReq: any = Readable.fromWeb(req.stream as any);
192211
viteReq.method = req.method;
193-
viteReq.url = req.url.toString().slice(req.url.origin.length);
212+
viteReq.url = viteUrl;
194213
viteReq.headers = { 'content-type': req.contentType };
195214
const viteRes: any = Writable.fromWeb(res.stream as any);
196215
Object.defineProperty(viteRes, 'statusCode', {

packages/waku/src/lib/plugins/vite-plugin-rsc-transform.ts

+25-33
Original file line numberDiff line numberDiff line change
@@ -2,49 +2,41 @@ import type { Plugin } from 'vite';
22
import * as RSDWNodeLoader from 'react-server-dom-webpack/node-loader';
33

44
export function rscTransformPlugin(
5-
isBuild: boolean,
6-
assetsDir?: string,
7-
clientEntryFiles?: Record<string, string>,
8-
serverEntryFiles?: Record<string, string>,
5+
opts:
6+
| {
7+
isBuild: false;
8+
}
9+
| {
10+
isBuild: true;
11+
assetsDir: string;
12+
clientEntryFiles: Record<string, string>;
13+
serverEntryFiles: Record<string, string>;
14+
},
915
): Plugin {
10-
const clientFileMap = new Map<string, string>();
11-
const serverFileMap = new Map<string, string>();
1216
const getClientId = (id: string) => {
13-
if (!assetsDir) {
14-
throw new Error('assetsDir is required');
17+
if (!opts.isBuild) {
18+
throw new Error('not buiding');
1519
}
16-
if (!clientFileMap.has(id)) {
17-
throw new Error(`Cannot find client id for ${id}`);
20+
for (const [k, v] of Object.entries(opts.clientEntryFiles)) {
21+
if (v === id) {
22+
return `@id/${opts.assetsDir}/${k}.js`;
23+
}
1824
}
19-
return `@id/${assetsDir}/${clientFileMap.get(id)}.js`;
25+
throw new Error('client id not found: ' + id);
2026
};
2127
const getServerId = (id: string) => {
22-
if (!assetsDir) {
23-
throw new Error('assetsDir is required');
28+
if (!opts.isBuild) {
29+
throw new Error('not buiding');
2430
}
25-
if (!serverFileMap.has(id)) {
26-
throw new Error(`Cannot find server id for ${id}`);
31+
for (const [k, v] of Object.entries(opts.serverEntryFiles)) {
32+
if (v === id) {
33+
return `@id/${opts.assetsDir}/${k}.js`;
34+
}
2735
}
28-
return `@id/${assetsDir}/${serverFileMap.get(id)}.js`;
36+
throw new Error('server id not found: ' + id);
2937
};
30-
let buildStarted = false;
3138
return {
3239
name: 'rsc-transform-plugin',
33-
async buildStart() {
34-
for (const [k, v] of Object.entries(clientEntryFiles || {})) {
35-
const resolvedId = await this.resolve(v);
36-
if (!resolvedId) {
37-
throw new Error(`Cannot resolve ${v}`);
38-
}
39-
clientFileMap.set(resolvedId.id, k);
40-
}
41-
for (const [k, v] of Object.entries(serverEntryFiles || {})) {
42-
serverFileMap.set(v, k);
43-
}
44-
// HACK Without checking buildStarted in transform,
45-
// this.resolve calls transform, and getClientId throws an error.
46-
buildStarted = true;
47-
},
4840
async transform(code, id) {
4941
const resolve = async (
5042
specifier: string,
@@ -71,7 +63,7 @@ export function rscTransformPlugin(
7163
resolve,
7264
);
7365
let { source } = await RSDWNodeLoader.load(id, null, load);
74-
if (isBuild && buildStarted) {
66+
if (opts.isBuild) {
7567
// TODO we should parse the source code by ourselves with SWC
7668
if (
7769
/^import {registerClientReference} from "react-server-dom-webpack\/server";/.test(

0 commit comments

Comments
 (0)