From c5868a8f18491f58b4cfb45b0e657891387ba14e Mon Sep 17 00:00:00 2001 From: daishi Date: Thu, 28 Nov 2024 12:46:05 +0900 Subject: [PATCH 01/23] chore(tests): hot-reload e2e test --- e2e/fixtures/hot-reload/package.json | 22 +++++ .../hot-reload/src/components/counter.tsx | 18 ++++ e2e/fixtures/hot-reload/src/pages/index.tsx | 16 ++++ e2e/fixtures/hot-reload/tsconfig.json | 17 ++++ e2e/hot-reload.spec.ts | 84 +++++++++++++++++++ tsconfig.e2e.json | 3 + 6 files changed, 160 insertions(+) create mode 100644 e2e/fixtures/hot-reload/package.json create mode 100644 e2e/fixtures/hot-reload/src/components/counter.tsx create mode 100644 e2e/fixtures/hot-reload/src/pages/index.tsx create mode 100644 e2e/fixtures/hot-reload/tsconfig.json create mode 100644 e2e/hot-reload.spec.ts diff --git a/e2e/fixtures/hot-reload/package.json b/e2e/fixtures/hot-reload/package.json new file mode 100644 index 000000000..6238df44d --- /dev/null +++ b/e2e/fixtures/hot-reload/package.json @@ -0,0 +1,22 @@ +{ + "name": "hot-reload", + "version": "0.1.0", + "type": "module", + "private": true, + "scripts": { + "dev": "waku dev", + "build": "waku build", + "start": "waku start" + }, + "dependencies": { + "react": "19.0.0-rc-5c56b873-20241107", + "react-dom": "19.0.0-rc-5c56b873-20241107", + "react-server-dom-webpack": "19.0.0-rc-5c56b873-20241107", + "waku": "workspace:*" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "typescript": "^5.6.3" + } +} diff --git a/e2e/fixtures/hot-reload/src/components/counter.tsx b/e2e/fixtures/hot-reload/src/components/counter.tsx new file mode 100644 index 000000000..c39437575 --- /dev/null +++ b/e2e/fixtures/hot-reload/src/components/counter.tsx @@ -0,0 +1,18 @@ +'use client'; + +import { useState } from 'react'; + +export const Counter = () => { + const [count, setCount] = useState(0); + return ( +
+

{count}

+ +
+ ); +}; diff --git a/e2e/fixtures/hot-reload/src/pages/index.tsx b/e2e/fixtures/hot-reload/src/pages/index.tsx new file mode 100644 index 000000000..64034677f --- /dev/null +++ b/e2e/fixtures/hot-reload/src/pages/index.tsx @@ -0,0 +1,16 @@ +import { Counter } from '../components/counter.js'; + +export default async function HomePage() { + return ( +
+

Home Page

+ +
+ ); +} + +export const getConfig = async () => { + return { + render: 'static', + } as const; +}; diff --git a/e2e/fixtures/hot-reload/tsconfig.json b/e2e/fixtures/hot-reload/tsconfig.json new file mode 100644 index 000000000..2590e13de --- /dev/null +++ b/e2e/fixtures/hot-reload/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "composite": true, + "strict": true, + "target": "esnext", + "downlevelIteration": true, + "esModuleInterop": true, + "module": "nodenext", + "skipLibCheck": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "types": ["react/experimental"], + "jsx": "react-jsx", + "rootDir": "./src", + "outDir": "./dist" + } +} diff --git a/e2e/hot-reload.spec.ts b/e2e/hot-reload.spec.ts new file mode 100644 index 000000000..2fda90d59 --- /dev/null +++ b/e2e/hot-reload.spec.ts @@ -0,0 +1,84 @@ +import { expect } from '@playwright/test'; +import { execSync, exec } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import { cp, mkdtemp, readFile, writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { createRequire } from 'node:module'; +import waitPort from 'wait-port'; + +import { debugChildProcess, getFreePort, terminate, test } from './utils.js'; + +let standaloneDir: string; +const fixtureDir = fileURLToPath( + new URL('./fixtures/hot-reload', import.meta.url), +); +const wakuDir = fileURLToPath(new URL('../packages/waku', import.meta.url)); +const { version } = createRequire(import.meta.url)( + join(wakuDir, 'package.json'), +); + +async function run() { + const port = await getFreePort(); + const cp = exec( + `node ${join(standaloneDir, './node_modules/waku/dist/cli.js')} dev --port ${port}`, + { cwd: standaloneDir }, + ); + debugChildProcess(cp, fileURLToPath(import.meta.url), [ + /ExperimentalWarning: Custom ESM Loaders is an experimental feature and might change at any time/, + ]); + await waitPort({ port }); + return [port, cp.pid] as const; +} + +async function modifyFile(file: string, search: string, replace: string) { + const content = await readFile(join(standaloneDir, file), 'utf-8'); + await writeFile(join(standaloneDir, file), content.replace(search, replace)); +} + +test.describe('hot reload', () => { + test.beforeEach(async () => { + // GitHub Action on Windows doesn't support mkdtemp on global temp dir, + // Which will cause files in `src` folder to be empty. + // I don't know why + const tmpDir = process.env.TEMP_DIR ? process.env.TEMP_DIR : tmpdir(); + standaloneDir = await mkdtemp(join(tmpDir, 'waku-hot-reload-')); + await cp(fixtureDir, standaloneDir, { + filter: (src) => { + return !src.includes('node_modules') && !src.includes('dist'); + }, + recursive: true, + }); + execSync(`pnpm pack --pack-destination ${standaloneDir}`, { + cwd: wakuDir, + stdio: 'inherit', + }); + const name = `waku-${version}.tgz`; + execSync(`npm install --force ${join(standaloneDir, name)}`, { + cwd: standaloneDir, + stdio: 'inherit', + }); + }); + + test('simple case', async ({ page }) => { + const [port, pid] = await run(); + await page.goto(`http://localhost:${port}/`); + await expect(page.getByText('Home Page')).toBeVisible(); + await modifyFile('src/pages/index.tsx', 'Home Page', 'Modified Page'); + await page.waitForTimeout(100); + await expect(page.getByText('Home Page')).toBeHidden(); + await expect(page.getByText('Modified Page')).toBeVisible(); + await expect(page.getByText('Increment')).toBeVisible(); + await expect(page.getByTestId('count')).toHaveText('0'); + await page.getByTestId('increment').click(); + await expect(page.getByTestId('count')).toHaveText('1'); + await modifyFile('src/components/counter.tsx', 'Increment', 'Plus One'); + await page.waitForTimeout(100); + await expect(page.getByText('Increment')).toBeHidden(); + await expect(page.getByText('Plus One')).toBeVisible(); + await expect(page.getByTestId('count')).toHaveText('1'); + await page.getByTestId('increment').click(); + await expect(page.getByTestId('count')).toHaveText('2'); + await terminate(pid!); + }); +}); diff --git a/tsconfig.e2e.json b/tsconfig.e2e.json index 9b2e8c2ea..66d82d064 100644 --- a/tsconfig.e2e.json +++ b/tsconfig.e2e.json @@ -47,6 +47,9 @@ }, { "path": "./e2e/fixtures/broken-links/tsconfig.json" + }, + { + "path": "./e2e/fixtures/hot-reload/tsconfig.json" } ] } From b9067cfb7f21b4d7d6a39da3e9df56145ad32438 Mon Sep 17 00:00:00 2001 From: daishi Date: Thu, 28 Nov 2024 12:49:31 +0900 Subject: [PATCH 02/23] fix lockfile --- pnpm-lock.yaml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index df4cf08ee..15794a47f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -100,6 +100,31 @@ importers: specifier: ^5.6.3 version: 5.6.3 + e2e/fixtures/hot-reload: + dependencies: + react: + specifier: 19.0.0-rc-5c56b873-20241107 + version: 19.0.0-rc-5c56b873-20241107 + react-dom: + specifier: 19.0.0-rc-5c56b873-20241107 + version: 19.0.0-rc-5c56b873-20241107(react@19.0.0-rc-5c56b873-20241107) + react-server-dom-webpack: + specifier: 19.0.0-rc-5c56b873-20241107 + version: 19.0.0-rc-5c56b873-20241107(react-dom@19.0.0-rc-5c56b873-20241107(react@19.0.0-rc-5c56b873-20241107))(react@19.0.0-rc-5c56b873-20241107)(webpack@5.96.1) + waku: + specifier: workspace:* + version: link:../../../packages/waku + devDependencies: + '@types/react': + specifier: ^18.3.12 + version: 18.3.12 + '@types/react-dom': + specifier: ^18.3.1 + version: 18.3.1 + typescript: + specifier: ^5.6.3 + version: 5.6.3 + e2e/fixtures/partial-build: dependencies: react: From b79e658dc1388ec05683bc9a87c393978dbfa3f7 Mon Sep 17 00:00:00 2001 From: daishi Date: Thu, 28 Nov 2024 21:25:29 +0900 Subject: [PATCH 03/23] fix rsc hot reload --- .../waku/src/lib/plugins/vite-plugin-rsc-hmr.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/waku/src/lib/plugins/vite-plugin-rsc-hmr.ts b/packages/waku/src/lib/plugins/vite-plugin-rsc-hmr.ts index 6f0c42624..20dedda4e 100644 --- a/packages/waku/src/lib/plugins/vite-plugin-rsc-hmr.ts +++ b/packages/waku/src/lib/plugins/vite-plugin-rsc-hmr.ts @@ -50,8 +50,8 @@ if (import.meta.hot && !globalThis.__WAKU_HMR_CONFIGURED__) { `; export function rscHmrPlugin(): Plugin { - const wakuClientDist = decodeFilePathFromAbsolute( - joinPath(fileURLToFilePath(import.meta.url), '../../../client.js'), + const wakuMinimalClientDist = decodeFilePathFromAbsolute( + joinPath(fileURLToFilePath(import.meta.url), '../../../minimal/client.js'), ); const wakuRouterClientDist = decodeFilePathFromAbsolute( joinPath(fileURLToFilePath(import.meta.url), '../../../router/client.js'), @@ -75,7 +75,7 @@ export function rscHmrPlugin(): Plugin { ]; }, async transform(code, id) { - if (id.startsWith(wakuClientDist)) { + if (id.startsWith(wakuMinimalClientDist)) { // FIXME this is fragile. Can we do it better? return code.replace( /\nexport const fetchRsc = \(.*?\)=>\{/, @@ -101,15 +101,16 @@ export function rscHmrPlugin(): Plugin { ); } else if (id.startsWith(wakuRouterClientDist)) { // FIXME this is fragile. Can we do it better? - const INNER_ROUTER_LINE = 'function InnerRouter() {'; return code.replace( - INNER_ROUTER_LINE, - INNER_ROUTER_LINE + + /\nconst InnerRouter = \(.*?\)=>\{/, + (m) => + m + ` { const refetchRoute = () => { - const rscPath = getInputString(loc.path); - refetch(rscPath, loc.searchParams); + const rscPath = encodeRoutePath(route.path); + const rscParams = createRscParams(route.query, []); + refetch(rscPath, rscParams); }; globalThis.__WAKU_RSC_RELOAD_LISTENERS__ ||= []; const index = globalThis.__WAKU_RSC_RELOAD_LISTENERS__.indexOf(globalThis.__WAKU_REFETCH_ROUTE__); From 0a32bb3555a6058fb775b84fb0e4e546807fdef5 Mon Sep 17 00:00:00 2001 From: daishi Date: Thu, 28 Nov 2024 21:26:54 +0900 Subject: [PATCH 04/23] wip disable it for now --- packages/waku/src/lib/middleware/dev-server-impl.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/waku/src/lib/middleware/dev-server-impl.ts b/packages/waku/src/lib/middleware/dev-server-impl.ts index 6623cb846..30591de5d 100644 --- a/packages/waku/src/lib/middleware/dev-server-impl.ts +++ b/packages/waku/src/lib/middleware/dev-server-impl.ts @@ -109,7 +109,9 @@ const createMainViteServer = ( rscIndexPlugin(config), rscTransformPlugin({ isClient: true, isBuild: false }), rscHmrPlugin(), - fsRouterTypegenPlugin(config), + ...('FIXME disabled for now'.length + ? [] + : [fsRouterTypegenPlugin(config)]), ], optimizeDeps: { include: ['react-server-dom-webpack/client', 'react-dom'], From 3a6829b1134012a7d1cb808d12e6d5e28292e295 Mon Sep 17 00:00:00 2001 From: daishi Date: Thu, 28 Nov 2024 22:33:22 +0900 Subject: [PATCH 05/23] Revert "wip disable it for now" This reverts commit 0a32bb3555a6058fb775b84fb0e4e546807fdef5. --- packages/waku/src/lib/middleware/dev-server-impl.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/waku/src/lib/middleware/dev-server-impl.ts b/packages/waku/src/lib/middleware/dev-server-impl.ts index 30591de5d..6623cb846 100644 --- a/packages/waku/src/lib/middleware/dev-server-impl.ts +++ b/packages/waku/src/lib/middleware/dev-server-impl.ts @@ -109,9 +109,7 @@ const createMainViteServer = ( rscIndexPlugin(config), rscTransformPlugin({ isClient: true, isBuild: false }), rscHmrPlugin(), - ...('FIXME disabled for now'.length - ? [] - : [fsRouterTypegenPlugin(config)]), + fsRouterTypegenPlugin(config), ], optimizeDeps: { include: ['react-server-dom-webpack/client', 'react-dom'], From 199548984738c3adeefa80f324f35765d5d2e55b Mon Sep 17 00:00:00 2001 From: daishi Date: Thu, 28 Nov 2024 23:10:20 +0900 Subject: [PATCH 06/23] exclude pages.gen.ts --- packages/waku/src/lib/plugins/vite-plugin-rsc-delegate.ts | 4 ++++ packages/waku/src/lib/plugins/vite-plugin-rsc-hmr.ts | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/packages/waku/src/lib/plugins/vite-plugin-rsc-delegate.ts b/packages/waku/src/lib/plugins/vite-plugin-rsc-delegate.ts index b7ec3435c..5ac14fce0 100644 --- a/packages/waku/src/lib/plugins/vite-plugin-rsc-delegate.ts +++ b/packages/waku/src/lib/plugins/vite-plugin-rsc-delegate.ts @@ -77,6 +77,10 @@ export function rscDelegatePlugin( }, async handleHotUpdate(ctx) { if (mode === 'development') { + if (ctx.file.endsWith('/pages.gen.ts')) { + // auto generated file by fsRouterTypegenPlugin + return []; + } await updateAllStyles(); // FIXME is this too aggressive? if (moduleImports.has(ctx.file)) { // re-inject diff --git a/packages/waku/src/lib/plugins/vite-plugin-rsc-hmr.ts b/packages/waku/src/lib/plugins/vite-plugin-rsc-hmr.ts index 20dedda4e..d1057f79b 100644 --- a/packages/waku/src/lib/plugins/vite-plugin-rsc-hmr.ts +++ b/packages/waku/src/lib/plugins/vite-plugin-rsc-hmr.ts @@ -126,6 +126,10 @@ export function rscHmrPlugin(): Plugin { } }, handleHotUpdate({ file }) { + if (file.endsWith('/pages.gen.ts')) { + // auto generated file by fsRouterTypegenPlugin + return []; + } const moduleLoading = (globalThis as any).__WAKU_CLIENT_MODULE_LOADING__; const moduleCache = (globalThis as any).__WAKU_CLIENT_MODULE_CACHE__; if (!moduleLoading || !moduleCache) { From 4f4709cf3f449ee81e7e1e65ebf126cc3d20a6b4 Mon Sep 17 00:00:00 2001 From: daishi Date: Thu, 28 Nov 2024 23:10:34 +0900 Subject: [PATCH 07/23] update e2e spec --- e2e/hot-reload.spec.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/e2e/hot-reload.spec.ts b/e2e/hot-reload.spec.ts index 2fda90d59..6f97be336 100644 --- a/e2e/hot-reload.spec.ts +++ b/e2e/hot-reload.spec.ts @@ -64,21 +64,21 @@ test.describe('hot reload', () => { const [port, pid] = await run(); await page.goto(`http://localhost:${port}/`); await expect(page.getByText('Home Page')).toBeVisible(); + await expect(page.getByTestId('count')).toHaveText('0'); + await page.getByTestId('increment').click(); + await expect(page.getByTestId('count')).toHaveText('1'); await modifyFile('src/pages/index.tsx', 'Home Page', 'Modified Page'); await page.waitForTimeout(100); - await expect(page.getByText('Home Page')).toBeHidden(); await expect(page.getByText('Modified Page')).toBeVisible(); - await expect(page.getByText('Increment')).toBeVisible(); - await expect(page.getByTestId('count')).toHaveText('0'); - await page.getByTestId('increment').click(); await expect(page.getByTestId('count')).toHaveText('1'); + await page.getByTestId('increment').click(); + await expect(page.getByTestId('count')).toHaveText('2'); await modifyFile('src/components/counter.tsx', 'Increment', 'Plus One'); await page.waitForTimeout(100); - await expect(page.getByText('Increment')).toBeHidden(); await expect(page.getByText('Plus One')).toBeVisible(); - await expect(page.getByTestId('count')).toHaveText('1'); - await page.getByTestId('increment').click(); await expect(page.getByTestId('count')).toHaveText('2'); + await page.getByTestId('increment').click(); + await expect(page.getByTestId('count')).toHaveText('3'); await terminate(pid!); }); }); From a0a04037dafc309138e0f8aaad94f3f1aeabd4af Mon Sep 17 00:00:00 2001 From: daishi Date: Thu, 28 Nov 2024 23:27:25 +0900 Subject: [PATCH 08/23] revert hidden checks --- e2e/hot-reload.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/e2e/hot-reload.spec.ts b/e2e/hot-reload.spec.ts index 6f97be336..39e2fc686 100644 --- a/e2e/hot-reload.spec.ts +++ b/e2e/hot-reload.spec.ts @@ -69,12 +69,14 @@ test.describe('hot reload', () => { await expect(page.getByTestId('count')).toHaveText('1'); await modifyFile('src/pages/index.tsx', 'Home Page', 'Modified Page'); await page.waitForTimeout(100); + await expect(page.getByText('Home Page')).toBeHidden(); await expect(page.getByText('Modified Page')).toBeVisible(); await expect(page.getByTestId('count')).toHaveText('1'); await page.getByTestId('increment').click(); await expect(page.getByTestId('count')).toHaveText('2'); await modifyFile('src/components/counter.tsx', 'Increment', 'Plus One'); await page.waitForTimeout(100); + await expect(page.getByText('Increment')).toBeHidden(); await expect(page.getByText('Plus One')).toBeVisible(); await expect(page.getByTestId('count')).toHaveText('2'); await page.getByTestId('increment').click(); From 7c91864c11300be4280a8b875569266ae39b5a71 Mon Sep 17 00:00:00 2001 From: daishi Date: Fri, 29 Nov 2024 09:51:24 +0900 Subject: [PATCH 09/23] Revert "revert hidden checks" This reverts commit a0a04037dafc309138e0f8aaad94f3f1aeabd4af. --- e2e/hot-reload.spec.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/e2e/hot-reload.spec.ts b/e2e/hot-reload.spec.ts index 39e2fc686..6f97be336 100644 --- a/e2e/hot-reload.spec.ts +++ b/e2e/hot-reload.spec.ts @@ -69,14 +69,12 @@ test.describe('hot reload', () => { await expect(page.getByTestId('count')).toHaveText('1'); await modifyFile('src/pages/index.tsx', 'Home Page', 'Modified Page'); await page.waitForTimeout(100); - await expect(page.getByText('Home Page')).toBeHidden(); await expect(page.getByText('Modified Page')).toBeVisible(); await expect(page.getByTestId('count')).toHaveText('1'); await page.getByTestId('increment').click(); await expect(page.getByTestId('count')).toHaveText('2'); await modifyFile('src/components/counter.tsx', 'Increment', 'Plus One'); await page.waitForTimeout(100); - await expect(page.getByText('Increment')).toBeHidden(); await expect(page.getByText('Plus One')).toBeVisible(); await expect(page.getByTestId('count')).toHaveText('2'); await page.getByTestId('increment').click(); From 787322acebab95f31fe9997f53f6e6f6ec480e28 Mon Sep 17 00:00:00 2001 From: daishi Date: Fri, 29 Nov 2024 10:42:18 +0900 Subject: [PATCH 10/23] fix test --- e2e/hot-reload.spec.ts | 22 +++++++++++++++++++--- e2e/utils.ts | 22 ++++++++++++++++++++-- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/e2e/hot-reload.spec.ts b/e2e/hot-reload.spec.ts index 6f97be336..28a73aa7e 100644 --- a/e2e/hot-reload.spec.ts +++ b/e2e/hot-reload.spec.ts @@ -7,7 +7,13 @@ import { tmpdir } from 'node:os'; import { createRequire } from 'node:module'; import waitPort from 'wait-port'; -import { debugChildProcess, getFreePort, terminate, test } from './utils.js'; +import { + debugChildProcess, + getFreePort, + isPortAvailable, + terminate, + test, +} from './utils.js'; let standaloneDir: string; const fixtureDir = fileURLToPath( @@ -19,6 +25,9 @@ const { version } = createRequire(import.meta.url)( ); async function run() { + if (!(await isPortAvailable(24678))) { + throw new Error('HMR port is not available'); + } const port = await getFreePort(); const cp = exec( `node ${join(standaloneDir, './node_modules/waku/dist/cli.js')} dev --port ${port}`, @@ -68,17 +77,24 @@ test.describe('hot reload', () => { await page.getByTestId('increment').click(); await expect(page.getByTestId('count')).toHaveText('1'); await modifyFile('src/pages/index.tsx', 'Home Page', 'Modified Page'); - await page.waitForTimeout(100); await expect(page.getByText('Modified Page')).toBeVisible(); + await page.waitForTimeout(500); // need to wait not to full reload await expect(page.getByTestId('count')).toHaveText('1'); await page.getByTestId('increment').click(); await expect(page.getByTestId('count')).toHaveText('2'); await modifyFile('src/components/counter.tsx', 'Increment', 'Plus One'); - await page.waitForTimeout(100); await expect(page.getByText('Plus One')).toBeVisible(); + await page.waitForTimeout(500); // need to wait not to full reload await expect(page.getByTestId('count')).toHaveText('2'); await page.getByTestId('increment').click(); await expect(page.getByTestId('count')).toHaveText('3'); + await modifyFile('src/pages/index.tsx', 'Modified Page', 'Edited Page'); + await expect(page.getByText('Edited Page')).toBeVisible(); + await page.waitForTimeout(500); // need to wait not to full reload + // FIXME the following should pass but not for now. + // await expect(page.getByTestId('count')).toHaveText('3'); + // await page.getByTestId('increment').click(); + // await expect(page.getByTestId('count')).toHaveText('4'); await terminate(pid!); }); }); diff --git a/e2e/utils.ts b/e2e/utils.ts index f0e4bd207..2d05b0257 100644 --- a/e2e/utils.ts +++ b/e2e/utils.ts @@ -21,15 +21,33 @@ const unexpectedErrors: RegExp[] = [ ]; export async function getFreePort(): Promise { - return new Promise((res) => { + return new Promise((resolve) => { const srv = net.createServer(); srv.listen(0, () => { const port = (srv.address() as net.AddressInfo).port; - srv.close(() => res(port)); + srv.close(() => resolve(port)); }); }); } +export async function isPortAvailable(port: number): Promise { + return new Promise((resolve, reject) => { + const srv = net.createServer(); + srv.once('error', (err) => { + if ((err as any).code === 'EADDRINUSE') { + resolve(false); + } else { + reject(err); + } + }); + srv.once('listening', () => { + srv.close(); + resolve(true); + }); + srv.listen(port); + }); +} + export function debugChildProcess( cp: ChildProcess, sourceFile: string, From cb99c5f6de1bb82ffc2a703a4681543cf13756fe Mon Sep 17 00:00:00 2001 From: daishi Date: Fri, 29 Nov 2024 11:03:39 +0900 Subject: [PATCH 11/23] wip: netstat --- e2e/hot-reload.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/e2e/hot-reload.spec.ts b/e2e/hot-reload.spec.ts index 28a73aa7e..514e60e1e 100644 --- a/e2e/hot-reload.spec.ts +++ b/e2e/hot-reload.spec.ts @@ -26,6 +26,8 @@ const { version } = createRequire(import.meta.url)( async function run() { if (!(await isPortAvailable(24678))) { + const output = execSync('netstat -an', { encoding: 'utf8' }); + console.info('netstat -an output:', output); throw new Error('HMR port is not available'); } const port = await getFreePort(); From 3f9d2b82b45272b02fbfe2a6a5694dad1703534f Mon Sep 17 00:00:00 2001 From: daishi Date: Fri, 29 Nov 2024 11:30:51 +0900 Subject: [PATCH 12/23] wip: netstat & lsof --- e2e/hot-reload.spec.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/e2e/hot-reload.spec.ts b/e2e/hot-reload.spec.ts index 514e60e1e..bb2e83bbe 100644 --- a/e2e/hot-reload.spec.ts +++ b/e2e/hot-reload.spec.ts @@ -25,9 +25,17 @@ const { version } = createRequire(import.meta.url)( ); async function run() { - if (!(await isPortAvailable(24678))) { - const output = execSync('netstat -an', { encoding: 'utf8' }); - console.info('netstat -an output:', output); + const HMR_PORT = 24678; + if (!(await isPortAvailable(HMR_PORT))) { + if (process.platform === 'win32') { + const output = execSync(`netstat -ano | findstr :${HMR_PORT}`, { + encoding: 'utf8', + }); + console.info('Win32: netstat -an output:', output); + } else { + const output = execSync(`lsof -i:${HMR_PORT}`, { encoding: 'utf8' }); + console.info('lsof output:', output); + } throw new Error('HMR port is not available'); } const port = await getFreePort(); From 14f4b2a3ac78975a8ea30b95df473e3cd9916aea Mon Sep 17 00:00:00 2001 From: daishi Date: Fri, 29 Nov 2024 12:39:02 +0900 Subject: [PATCH 13/23] wip: terminate it --- e2e/hot-reload.spec.ts | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/e2e/hot-reload.spec.ts b/e2e/hot-reload.spec.ts index bb2e83bbe..bdf171e50 100644 --- a/e2e/hot-reload.spec.ts +++ b/e2e/hot-reload.spec.ts @@ -28,15 +28,25 @@ async function run() { const HMR_PORT = 24678; if (!(await isPortAvailable(HMR_PORT))) { if (process.platform === 'win32') { - const output = execSync(`netstat -ano | findstr :${HMR_PORT}`, { + const output = execSync( + `for /f "tokens=5" %A in ('netstat -ano ^| findstr :${HMR_PORT} ^| findstr LISTENING') do @echo %A`, + { + encoding: 'utf8', + }, + ); + console.info('Win32 output:', output); + if (output) { + await terminate(parseInt(output)); + } + } else { + const output = execSync(`lsof -i:${HMR_PORT} | awk 'NR==2 {print $2}'`, { encoding: 'utf8', }); - console.info('Win32: netstat -an output:', output); - } else { - const output = execSync(`lsof -i:${HMR_PORT}`, { encoding: 'utf8' }); - console.info('lsof output:', output); + console.info('Ubuntu output:', output); + if (output) { + await terminate(parseInt(output)); + } } - throw new Error('HMR port is not available'); } const port = await getFreePort(); const cp = exec( From 19bad216790ae248d76418c6336aed14ec5d779e Mon Sep 17 00:00:00 2001 From: daishi Date: Fri, 29 Nov 2024 13:04:44 +0900 Subject: [PATCH 14/23] remove debug log --- e2e/hot-reload.spec.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/e2e/hot-reload.spec.ts b/e2e/hot-reload.spec.ts index bdf171e50..588ba1c92 100644 --- a/e2e/hot-reload.spec.ts +++ b/e2e/hot-reload.spec.ts @@ -34,7 +34,6 @@ async function run() { encoding: 'utf8', }, ); - console.info('Win32 output:', output); if (output) { await terminate(parseInt(output)); } @@ -42,7 +41,6 @@ async function run() { const output = execSync(`lsof -i:${HMR_PORT} | awk 'NR==2 {print $2}'`, { encoding: 'utf8', }); - console.info('Ubuntu output:', output); if (output) { await terminate(parseInt(output)); } From 40fe8c60fbe4e8263d031e252d6fc4104b16b6ec Mon Sep 17 00:00:00 2001 From: daishi Date: Fri, 29 Nov 2024 13:05:47 +0900 Subject: [PATCH 15/23] disable fsRouterTypegenPlugin --- packages/waku/src/lib/middleware/dev-server-impl.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/waku/src/lib/middleware/dev-server-impl.ts b/packages/waku/src/lib/middleware/dev-server-impl.ts index 6623cb846..30591de5d 100644 --- a/packages/waku/src/lib/middleware/dev-server-impl.ts +++ b/packages/waku/src/lib/middleware/dev-server-impl.ts @@ -109,7 +109,9 @@ const createMainViteServer = ( rscIndexPlugin(config), rscTransformPlugin({ isClient: true, isBuild: false }), rscHmrPlugin(), - fsRouterTypegenPlugin(config), + ...('FIXME disabled for now'.length + ? [] + : [fsRouterTypegenPlugin(config)]), ], optimizeDeps: { include: ['react-server-dom-webpack/client', 'react-dom'], From 1a60bca9c305bfc5673afea35292f05150f154e9 Mon Sep 17 00:00:00 2001 From: daishi Date: Fri, 29 Nov 2024 13:35:19 +0900 Subject: [PATCH 16/23] Revert "disable fsRouterTypegenPlugin" This reverts commit 40fe8c60fbe4e8263d031e252d6fc4104b16b6ec. --- packages/waku/src/lib/middleware/dev-server-impl.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/waku/src/lib/middleware/dev-server-impl.ts b/packages/waku/src/lib/middleware/dev-server-impl.ts index 30591de5d..6623cb846 100644 --- a/packages/waku/src/lib/middleware/dev-server-impl.ts +++ b/packages/waku/src/lib/middleware/dev-server-impl.ts @@ -109,9 +109,7 @@ const createMainViteServer = ( rscIndexPlugin(config), rscTransformPlugin({ isClient: true, isBuild: false }), rscHmrPlugin(), - ...('FIXME disabled for now'.length - ? [] - : [fsRouterTypegenPlugin(config)]), + fsRouterTypegenPlugin(config), ], optimizeDeps: { include: ['react-server-dom-webpack/client', 'react-dom'], From f8d22144629546e7f500b82dc53a60ab0079dc36 Mon Sep 17 00:00:00 2001 From: daishi Date: Fri, 29 Nov 2024 13:37:48 +0900 Subject: [PATCH 17/23] wip: throw unexpected case --- packages/waku/src/lib/plugins/vite-plugin-rsc-delegate.ts | 3 +++ packages/waku/src/lib/plugins/vite-plugin-rsc-hmr.ts | 3 +++ 2 files changed, 6 insertions(+) diff --git a/packages/waku/src/lib/plugins/vite-plugin-rsc-delegate.ts b/packages/waku/src/lib/plugins/vite-plugin-rsc-delegate.ts index 5ac14fce0..eca0ea4ce 100644 --- a/packages/waku/src/lib/plugins/vite-plugin-rsc-delegate.ts +++ b/packages/waku/src/lib/plugins/vite-plugin-rsc-delegate.ts @@ -81,6 +81,9 @@ export function rscDelegatePlugin( // auto generated file by fsRouterTypegenPlugin return []; } + if (ctx.file.includes('pages.gen')) { + throw new Error('Unexpected ctx.file: ' + ctx.file); + } await updateAllStyles(); // FIXME is this too aggressive? if (moduleImports.has(ctx.file)) { // re-inject diff --git a/packages/waku/src/lib/plugins/vite-plugin-rsc-hmr.ts b/packages/waku/src/lib/plugins/vite-plugin-rsc-hmr.ts index d1057f79b..67e7427b3 100644 --- a/packages/waku/src/lib/plugins/vite-plugin-rsc-hmr.ts +++ b/packages/waku/src/lib/plugins/vite-plugin-rsc-hmr.ts @@ -130,6 +130,9 @@ export function rscHmrPlugin(): Plugin { // auto generated file by fsRouterTypegenPlugin return []; } + if (file.includes('pages.gen')) { + throw new Error('Unexpected file: ' + file); + } const moduleLoading = (globalThis as any).__WAKU_CLIENT_MODULE_LOADING__; const moduleCache = (globalThis as any).__WAKU_CLIENT_MODULE_CACHE__; if (!moduleLoading || !moduleCache) { From a3f8a8ef930e38e6e3c53a025e9864278ed90551 Mon Sep 17 00:00:00 2001 From: daishi Date: Fri, 29 Nov 2024 14:40:16 +0900 Subject: [PATCH 18/23] Revert "wip: throw unexpected case" This reverts commit f8d22144629546e7f500b82dc53a60ab0079dc36. --- packages/waku/src/lib/plugins/vite-plugin-rsc-delegate.ts | 3 --- packages/waku/src/lib/plugins/vite-plugin-rsc-hmr.ts | 3 --- 2 files changed, 6 deletions(-) diff --git a/packages/waku/src/lib/plugins/vite-plugin-rsc-delegate.ts b/packages/waku/src/lib/plugins/vite-plugin-rsc-delegate.ts index eca0ea4ce..5ac14fce0 100644 --- a/packages/waku/src/lib/plugins/vite-plugin-rsc-delegate.ts +++ b/packages/waku/src/lib/plugins/vite-plugin-rsc-delegate.ts @@ -81,9 +81,6 @@ export function rscDelegatePlugin( // auto generated file by fsRouterTypegenPlugin return []; } - if (ctx.file.includes('pages.gen')) { - throw new Error('Unexpected ctx.file: ' + ctx.file); - } await updateAllStyles(); // FIXME is this too aggressive? if (moduleImports.has(ctx.file)) { // re-inject diff --git a/packages/waku/src/lib/plugins/vite-plugin-rsc-hmr.ts b/packages/waku/src/lib/plugins/vite-plugin-rsc-hmr.ts index 67e7427b3..d1057f79b 100644 --- a/packages/waku/src/lib/plugins/vite-plugin-rsc-hmr.ts +++ b/packages/waku/src/lib/plugins/vite-plugin-rsc-hmr.ts @@ -130,9 +130,6 @@ export function rscHmrPlugin(): Plugin { // auto generated file by fsRouterTypegenPlugin return []; } - if (file.includes('pages.gen')) { - throw new Error('Unexpected file: ' + file); - } const moduleLoading = (globalThis as any).__WAKU_CLIENT_MODULE_LOADING__; const moduleCache = (globalThis as any).__WAKU_CLIENT_MODULE_CACHE__; if (!moduleLoading || !moduleCache) { From 7dbe48602b999acefc22979b32c19b207659ea53 Mon Sep 17 00:00:00 2001 From: daishi Date: Fri, 29 Nov 2024 15:18:21 +0900 Subject: [PATCH 19/23] minor fix --- packages/waku/src/lib/plugins/vite-plugin-rsc-hmr.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/waku/src/lib/plugins/vite-plugin-rsc-hmr.ts b/packages/waku/src/lib/plugins/vite-plugin-rsc-hmr.ts index d1057f79b..842b8339f 100644 --- a/packages/waku/src/lib/plugins/vite-plugin-rsc-hmr.ts +++ b/packages/waku/src/lib/plugins/vite-plugin-rsc-hmr.ts @@ -154,7 +154,7 @@ export function rscHmrPlugin(): Plugin { const pendingMap = new WeakMap, Set>(); -export function viteHot(viteServer: ViteDevServer): HMRBroadcaster { +function viteHot(viteServer: ViteDevServer): HMRBroadcaster { return viteServer.hot ?? viteServer.ws; } From e65f36c6b96a4c0245983a9bd18abce63a9f8f3d Mon Sep 17 00:00:00 2001 From: daishi Date: Fri, 29 Nov 2024 19:17:21 +0900 Subject: [PATCH 20/23] possible fix --- packages/waku/src/lib/plugins/vite-plugin-fs-router-typegen.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/waku/src/lib/plugins/vite-plugin-fs-router-typegen.ts b/packages/waku/src/lib/plugins/vite-plugin-fs-router-typegen.ts index a89abd1ef..240216911 100644 --- a/packages/waku/src/lib/plugins/vite-plugin-fs-router-typegen.ts +++ b/packages/waku/src/lib/plugins/vite-plugin-fs-router-typegen.ts @@ -225,7 +225,6 @@ export const fsRouterTypegenPlugin = (opts: { srcDir: string }): Plugin => { await writeFile(outputFile, formatted, 'utf-8'); }; - server.watcher.add(opts.srcDir); server.watcher.on('change', async (file) => { if (!outputFile || outputFile.endsWith(file)) { return; From 1089c2a2377a6b9daaaeea87329ff1e909bef2e4 Mon Sep 17 00:00:00 2001 From: daishi Date: Fri, 29 Nov 2024 19:37:43 +0900 Subject: [PATCH 21/23] fix delegate plugin --- packages/waku/src/lib/plugins/vite-plugin-rsc-delegate.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/waku/src/lib/plugins/vite-plugin-rsc-delegate.ts b/packages/waku/src/lib/plugins/vite-plugin-rsc-delegate.ts index 5ac14fce0..3f604c536 100644 --- a/packages/waku/src/lib/plugins/vite-plugin-rsc-delegate.ts +++ b/packages/waku/src/lib/plugins/vite-plugin-rsc-delegate.ts @@ -77,10 +77,6 @@ export function rscDelegatePlugin( }, async handleHotUpdate(ctx) { if (mode === 'development') { - if (ctx.file.endsWith('/pages.gen.ts')) { - // auto generated file by fsRouterTypegenPlugin - return []; - } await updateAllStyles(); // FIXME is this too aggressive? if (moduleImports.has(ctx.file)) { // re-inject @@ -100,6 +96,7 @@ export function rscDelegatePlugin( callback({ type: 'custom', event: 'rsc-reload' }); } } + return []; }, async transform(code, id) { // id can contain query string with vite deps optimization From a3761cd30f131d30edd4363f188c5dc5d84264d5 Mon Sep 17 00:00:00 2001 From: daishi Date: Fri, 29 Nov 2024 21:16:09 +0900 Subject: [PATCH 22/23] fix hmr plugin --- packages/waku/src/lib/plugins/vite-plugin-rsc-hmr.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/waku/src/lib/plugins/vite-plugin-rsc-hmr.ts b/packages/waku/src/lib/plugins/vite-plugin-rsc-hmr.ts index 842b8339f..0267646e2 100644 --- a/packages/waku/src/lib/plugins/vite-plugin-rsc-hmr.ts +++ b/packages/waku/src/lib/plugins/vite-plugin-rsc-hmr.ts @@ -135,8 +135,8 @@ export function rscHmrPlugin(): Plugin { if (!moduleLoading || !moduleCache) { return; } - if (file.startsWith(viteServer.config.root)) { - file = file.slice(viteServer.config.root.length); + if (file.startsWith(viteServer.config.root + '/')) { + file = file.slice(viteServer.config.root.length + 1); } const id = filePathToFileURL(file); if (moduleLoading.has(id)) { From 34383313d2bd9d6ae44b141fecdbc83bd344164b Mon Sep 17 00:00:00 2001 From: daishi Date: Fri, 29 Nov 2024 21:55:10 +0900 Subject: [PATCH 23/23] add comments --- e2e/hot-reload.spec.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/e2e/hot-reload.spec.ts b/e2e/hot-reload.spec.ts index 588ba1c92..386f6baec 100644 --- a/e2e/hot-reload.spec.ts +++ b/e2e/hot-reload.spec.ts @@ -94,22 +94,26 @@ test.describe('hot reload', () => { await expect(page.getByTestId('count')).toHaveText('0'); await page.getByTestId('increment').click(); await expect(page.getByTestId('count')).toHaveText('1'); + // Server component hot reload await modifyFile('src/pages/index.tsx', 'Home Page', 'Modified Page'); await expect(page.getByText('Modified Page')).toBeVisible(); await page.waitForTimeout(500); // need to wait not to full reload await expect(page.getByTestId('count')).toHaveText('1'); await page.getByTestId('increment').click(); await expect(page.getByTestId('count')).toHaveText('2'); + // Client component HMR await modifyFile('src/components/counter.tsx', 'Increment', 'Plus One'); await expect(page.getByText('Plus One')).toBeVisible(); await page.waitForTimeout(500); // need to wait not to full reload await expect(page.getByTestId('count')).toHaveText('2'); await page.getByTestId('increment').click(); await expect(page.getByTestId('count')).toHaveText('3'); + // Server component hot reload again await modifyFile('src/pages/index.tsx', 'Modified Page', 'Edited Page'); await expect(page.getByText('Edited Page')).toBeVisible(); await page.waitForTimeout(500); // need to wait not to full reload - // FIXME the following should pass but not for now. + // FIXME The following should pass but not for now. + // It's probably because Vite adds `?t=...` timestamp with HMR. // await expect(page.getByTestId('count')).toHaveText('3'); // await page.getByTestId('increment').click(); // await expect(page.getByTestId('count')).toHaveText('4');