From a2ce582d9208c60ac92d6c97179f2d115701fba4 Mon Sep 17 00:00:00 2001 From: ashfame Date: Tue, 24 Feb 2026 23:49:17 +0400 Subject: [PATCH 1/8] serve client on `127.0.0.1:5400/client/index.js` when running `npm run dev` can auto-build client as necessary --- packages/playground/website/vite.config.ts | 124 ++++++++++++++++++++- 1 file changed, 123 insertions(+), 1 deletion(-) diff --git a/packages/playground/website/vite.config.ts b/packages/playground/website/vite.config.ts index 28c7aaf2f6d..fb33bbcaaba 100644 --- a/packages/playground/website/vite.config.ts +++ b/packages/playground/website/vite.config.ts @@ -18,7 +18,13 @@ import { // eslint-disable-next-line @nx/enforce-module-boundaries import { oAuthMiddleware } from './vite.oauth'; import { fileURLToPath } from 'node:url'; -import { copyFileSync, existsSync } from 'node:fs'; +import { + copyFileSync, + existsSync, + readFileSync, + readdirSync, + statSync, +} from 'node:fs'; import { join } from 'node:path'; // eslint-disable-next-line @nx/enforce-module-boundaries import { buildVersionPlugin } from '../../vite-extensions/vite-build-version'; @@ -123,6 +129,122 @@ export default defineConfig(({ command, mode }) => { server.middlewares.use(oAuthMiddleware); }, }, + // Serve the built @wp-playground/client library at /client/ + // to match production where playground.wordpress.net/client/index.js + // is available. Auto-builds if missing, warns if stale. + { + name: 'serve-client-library', + configureServer(server: ViteDevServer) { + const repoRoot = join(__dirname, '../../../'); + const clientDistDir = join( + repoRoot, + 'dist/packages/playground/client' + ); + const clientSrcDir = join(__dirname, '../client/src'); + let buildInProgress = false; + let stalenessWarned = false; + + function newestMtimeIn(dir: string): number { + let newest = 0; + try { + for (const entry of readdirSync(dir, { + withFileTypes: true, + })) { + const full = join(dir, entry.name); + if (entry.isDirectory()) { + newest = Math.max( + newest, + newestMtimeIn(full) + ); + } else if (entry.isFile()) { + newest = Math.max( + newest, + statSync(full).mtimeMs + ); + } + } + } catch { + // Directory may not exist yet + } + return newest; + } + + function triggerClientBuild() { + if (buildInProgress) { + return; + } + buildInProgress = true; + server.config.logger.warn( + '\n Building @wp-playground/client… Refresh when done.\n' + ); + const { exec } = require('child_process'); + exec( + 'npx nx build playground-client', + { cwd: repoRoot }, + (error: unknown) => { + buildInProgress = false; + stalenessWarned = false; + if (error) { + server.config.logger.error( + ' @wp-playground/client build failed. ' + + 'Run manually: npx nx build playground-client\n' + ); + } else { + server.config.logger.info( + ' @wp-playground/client built. Refresh to load.\n' + ); + } + } + ); + } + + server.middlewares.use((req, res, next) => { + if (!req.url?.startsWith('/client/')) { + return next(); + } + + const distIndexPath = join(clientDistDir, 'index.js'); + + if (!existsSync(distIndexPath)) { + triggerClientBuild(); + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader( + 'Content-Type', + 'application/javascript' + ); + res.statusCode = 503; + res.end( + 'throw new Error(' + + '"@wp-playground/client is not built yet. ' + + 'A build was triggered automatically — refresh in a few seconds.\\n' + + 'Or build manually: npx nx build playground-client"' + + ');' + ); + return; + } + + if (!stalenessWarned && !buildInProgress) { + const distMtime = statSync(distIndexPath).mtimeMs; + const srcMtime = newestMtimeIn(clientSrcDir); + if (srcMtime > distMtime) { + stalenessWarned = true; + triggerClientBuild(); + } + } + + const filePath = join( + clientDistDir, + req.url.slice('/client/'.length) + ); + if (!existsSync(filePath)) { + return next(); + } + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Content-Type', 'application/javascript'); + res.end(readFileSync(filePath)); + }); + }, + }, /** * Copy the `.htaccess` file to the `dist` directory. */ From 294951fabc1a8c4fe8137b269065653dcbf1c12b Mon Sep 17 00:00:00 2001 From: ashfame Date: Wed, 25 Feb 2026 00:24:03 +0400 Subject: [PATCH 2/8] be a bit better in setting Content-Type header --- packages/playground/website/vite.config.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/playground/website/vite.config.ts b/packages/playground/website/vite.config.ts index fb33bbcaaba..766db579e81 100644 --- a/packages/playground/website/vite.config.ts +++ b/packages/playground/website/vite.config.ts @@ -239,8 +239,20 @@ export default defineConfig(({ command, mode }) => { if (!existsSync(filePath)) { return next(); } + const contentTypes: Record = { + '.js': 'application/javascript', + '.cjs': 'application/javascript', + '.json': 'application/json', + '.map': 'application/json', + }; + const ext = Object.keys(contentTypes).find((e) => + filePath.endsWith(e) + ); res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Content-Type', 'application/javascript'); + res.setHeader( + 'Content-Type', + ext ? contentTypes[ext] : 'application/octet-stream' + ); res.end(readFileSync(filePath)); }); }, From 4ee4eae3938f801605b8290e662fb4f37ad8a72e Mon Sep 17 00:00:00 2001 From: ashfame Date: Wed, 25 Feb 2026 00:31:25 +0400 Subject: [PATCH 3/8] Harden serve-client-library Vite plugin per PR review feedback Replace runtime require('child_process') with a static import from node:child_process, log stderr on build failures, and sanitize the request URL to prevent path traversal and query-string issues. --- packages/playground/website/vite.config.ts | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/playground/website/vite.config.ts b/packages/playground/website/vite.config.ts index 766db579e81..7e85d873985 100644 --- a/packages/playground/website/vite.config.ts +++ b/packages/playground/website/vite.config.ts @@ -25,7 +25,8 @@ import { readdirSync, statSync, } from 'node:fs'; -import { join } from 'node:path'; +import { join, resolve } from 'node:path'; +import { exec } from 'node:child_process'; // eslint-disable-next-line @nx/enforce-module-boundaries import { buildVersionPlugin } from '../../vite-extensions/vite-build-version'; // eslint-disable-next-line @nx/enforce-module-boundaries @@ -177,11 +178,10 @@ export default defineConfig(({ command, mode }) => { server.config.logger.warn( '\n Building @wp-playground/client… Refresh when done.\n' ); - const { exec } = require('child_process'); exec( 'npx nx build playground-client', { cwd: repoRoot }, - (error: unknown) => { + (error, stdout, stderr) => { buildInProgress = false; stalenessWarned = false; if (error) { @@ -189,6 +189,9 @@ export default defineConfig(({ command, mode }) => { ' @wp-playground/client build failed. ' + 'Run manually: npx nx build playground-client\n' ); + if (stderr) { + server.config.logger.error(stderr); + } } else { server.config.logger.info( ' @wp-playground/client built. Refresh to load.\n' @@ -232,10 +235,17 @@ export default defineConfig(({ command, mode }) => { } } - const filePath = join( + const urlPath = new URL(req.url, 'http://localhost') + .pathname; + const filePath = resolve( clientDistDir, - req.url.slice('/client/'.length) + urlPath.slice('/client/'.length) ); + if (!filePath.startsWith(clientDistDir)) { + res.statusCode = 403; + res.end(); + return; + } if (!existsSync(filePath)) { return next(); } From 79e8c2b4e82737bffc30b4ae5d43705ce0702d14 Mon Sep 17 00:00:00 2001 From: ashfame Date: Wed, 25 Feb 2026 00:39:46 +0400 Subject: [PATCH 4/8] Use path.relative() for cross-platform path traversal check --- packages/playground/website/vite.config.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/playground/website/vite.config.ts b/packages/playground/website/vite.config.ts index 7e85d873985..52914fe547d 100644 --- a/packages/playground/website/vite.config.ts +++ b/packages/playground/website/vite.config.ts @@ -25,7 +25,7 @@ import { readdirSync, statSync, } from 'node:fs'; -import { join, resolve } from 'node:path'; +import { join, resolve, relative, isAbsolute } from 'node:path'; import { exec } from 'node:child_process'; // eslint-disable-next-line @nx/enforce-module-boundaries import { buildVersionPlugin } from '../../vite-extensions/vite-build-version'; @@ -241,7 +241,8 @@ export default defineConfig(({ command, mode }) => { clientDistDir, urlPath.slice('/client/'.length) ); - if (!filePath.startsWith(clientDistDir)) { + const rel = relative(clientDistDir, filePath); + if (rel.startsWith('..') || isAbsolute(rel)) { res.statusCode = 403; res.end(); return; From 54ca269789184e1c311705ccbac0ebdac8c25543 Mon Sep 17 00:00:00 2001 From: ashfame Date: Thu, 19 Mar 2026 20:18:52 +0545 Subject: [PATCH 5/8] Throttle staleness check with file watcher instead of per-request dir walk The recursive newestMtimeIn() directory walk was running on every /client/* request when the build was up-to-date, since stalenessWarned only flipped to true when staleness was detected. Now the check runs once on first request (or after a build), and Vite's file watcher resets it when client source files actually change. Made-with: Cursor --- packages/playground/website/vite.config.ts | 30 +++++++++++++++++----- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/packages/playground/website/vite.config.ts b/packages/playground/website/vite.config.ts index 52914fe547d..25bfbc9ae52 100644 --- a/packages/playground/website/vite.config.ts +++ b/packages/playground/website/vite.config.ts @@ -143,7 +143,8 @@ export default defineConfig(({ command, mode }) => { ); const clientSrcDir = join(__dirname, '../client/src'); let buildInProgress = false; - let stalenessWarned = false; + let stalenessChecked = false; + let sourcesDirty = false; function newestMtimeIn(dir: string): number { let newest = 0; @@ -183,7 +184,8 @@ export default defineConfig(({ command, mode }) => { { cwd: repoRoot }, (error, stdout, stderr) => { buildInProgress = false; - stalenessWarned = false; + stalenessChecked = true; + sourcesDirty = false; if (error) { server.config.logger.error( ' @wp-playground/client build failed. ' + @@ -201,6 +203,14 @@ export default defineConfig(({ command, mode }) => { ); } + server.watcher.add(clientSrcDir); + server.watcher.on('change', (changedPath) => { + if (changedPath.startsWith(clientSrcDir)) { + sourcesDirty = true; + stalenessChecked = false; + } + }); + server.middlewares.use((req, res, next) => { if (!req.url?.startsWith('/client/')) { return next(); @@ -226,14 +236,22 @@ export default defineConfig(({ command, mode }) => { return; } - if (!stalenessWarned && !buildInProgress) { - const distMtime = statSync(distIndexPath).mtimeMs; + if ( + !stalenessChecked && + !buildInProgress + ) { + stalenessChecked = true; + const distMtime = + statSync(distIndexPath).mtimeMs; const srcMtime = newestMtimeIn(clientSrcDir); if (srcMtime > distMtime) { - stalenessWarned = true; - triggerClientBuild(); + sourcesDirty = true; } } + if (sourcesDirty && !buildInProgress) { + sourcesDirty = false; + triggerClientBuild(); + } const urlPath = new URL(req.url, 'http://localhost') .pathname; From d48bf24154593fb3b51206467a3a9027152f0103 Mon Sep 17 00:00:00 2001 From: ashfame Date: Thu, 19 Mar 2026 20:19:03 +0545 Subject: [PATCH 6/8] Guard URL parsing with try-catch for malformed requests Wrap the `new URL(req.url, ...)` call in a try-catch so that malformed request URLs return a 400 instead of crashing the middleware with an unhandled exception. Made-with: Cursor --- packages/playground/website/vite.config.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/playground/website/vite.config.ts b/packages/playground/website/vite.config.ts index 25bfbc9ae52..4d7f7d9452c 100644 --- a/packages/playground/website/vite.config.ts +++ b/packages/playground/website/vite.config.ts @@ -253,8 +253,15 @@ export default defineConfig(({ command, mode }) => { triggerClientBuild(); } - const urlPath = new URL(req.url, 'http://localhost') - .pathname; + let urlPath: string; + try { + urlPath = new URL(req.url, 'http://localhost') + .pathname; + } catch { + res.statusCode = 400; + res.end('Invalid request URL'); + return; + } const filePath = resolve( clientDistDir, urlPath.slice('/client/'.length) From ba51a54fad3d73e1bd07779f4613907f06f9d2a1 Mon Sep 17 00:00:00 2001 From: ashfame Date: Thu, 19 Mar 2026 20:28:25 +0545 Subject: [PATCH 7/8] Extract serve-client-library Vite plugin to its own file Move the ~170-line serve-client-library plugin out of vite.config.ts into vite.serve-client-library.ts to keep the config declarative and readable. Also removes the duplicate `import { exec }` that was shadowed by the promisified version in the config. Made-with: Cursor --- packages/playground/website/vite.config.ts | 176 +---------------- .../website/vite.serve-client-library.ts | 180 ++++++++++++++++++ 2 files changed, 184 insertions(+), 172 deletions(-) create mode 100644 packages/playground/website/vite.serve-client-library.ts diff --git a/packages/playground/website/vite.config.ts b/packages/playground/website/vite.config.ts index eca8e8ad8ec..4c3015e34c7 100644 --- a/packages/playground/website/vite.config.ts +++ b/packages/playground/website/vite.config.ts @@ -17,18 +17,12 @@ import { } from '../build-config'; // eslint-disable-next-line @nx/enforce-module-boundaries import { oAuthMiddleware } from './vite.oauth'; +import { serveClientLibrary } from './vite.serve-client-library'; import { exec as execCb } from 'node:child_process'; import { promisify } from 'node:util'; import { fileURLToPath } from 'node:url'; -import { - copyFileSync, - existsSync, - readFileSync, - readdirSync, - statSync, -} from 'node:fs'; -import { join, resolve, relative, isAbsolute } from 'node:path'; -import { exec } from 'node:child_process'; +import { copyFileSync, existsSync } from 'node:fs'; +import { join } from 'node:path'; // eslint-disable-next-line @nx/enforce-module-boundaries import { buildVersionPlugin } from '../../vite-extensions/vite-build-version'; // eslint-disable-next-line @nx/enforce-module-boundaries @@ -197,169 +191,7 @@ export default defineConfig(({ command, mode }) => { server.middlewares.use(oAuthMiddleware); }, }, - // Serve the built @wp-playground/client library at /client/ - // to match production where playground.wordpress.net/client/index.js - // is available. Auto-builds if missing, warns if stale. - { - name: 'serve-client-library', - configureServer(server: ViteDevServer) { - const repoRoot = join(__dirname, '../../../'); - const clientDistDir = join( - repoRoot, - 'dist/packages/playground/client' - ); - const clientSrcDir = join(__dirname, '../client/src'); - let buildInProgress = false; - let stalenessChecked = false; - let sourcesDirty = false; - - function newestMtimeIn(dir: string): number { - let newest = 0; - try { - for (const entry of readdirSync(dir, { - withFileTypes: true, - })) { - const full = join(dir, entry.name); - if (entry.isDirectory()) { - newest = Math.max( - newest, - newestMtimeIn(full) - ); - } else if (entry.isFile()) { - newest = Math.max( - newest, - statSync(full).mtimeMs - ); - } - } - } catch { - // Directory may not exist yet - } - return newest; - } - - function triggerClientBuild() { - if (buildInProgress) { - return; - } - buildInProgress = true; - server.config.logger.warn( - '\n Building @wp-playground/client… Refresh when done.\n' - ); - exec( - 'npx nx build playground-client', - { cwd: repoRoot }, - (error, stdout, stderr) => { - buildInProgress = false; - stalenessChecked = true; - sourcesDirty = false; - if (error) { - server.config.logger.error( - ' @wp-playground/client build failed. ' + - 'Run manually: npx nx build playground-client\n' - ); - if (stderr) { - server.config.logger.error(stderr); - } - } else { - server.config.logger.info( - ' @wp-playground/client built. Refresh to load.\n' - ); - } - } - ); - } - - server.watcher.add(clientSrcDir); - server.watcher.on('change', (changedPath) => { - if (changedPath.startsWith(clientSrcDir)) { - sourcesDirty = true; - stalenessChecked = false; - } - }); - - server.middlewares.use((req, res, next) => { - if (!req.url?.startsWith('/client/')) { - return next(); - } - - const distIndexPath = join(clientDistDir, 'index.js'); - - if (!existsSync(distIndexPath)) { - triggerClientBuild(); - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader( - 'Content-Type', - 'application/javascript' - ); - res.statusCode = 503; - res.end( - 'throw new Error(' + - '"@wp-playground/client is not built yet. ' + - 'A build was triggered automatically — refresh in a few seconds.\\n' + - 'Or build manually: npx nx build playground-client"' + - ');' - ); - return; - } - - if ( - !stalenessChecked && - !buildInProgress - ) { - stalenessChecked = true; - const distMtime = - statSync(distIndexPath).mtimeMs; - const srcMtime = newestMtimeIn(clientSrcDir); - if (srcMtime > distMtime) { - sourcesDirty = true; - } - } - if (sourcesDirty && !buildInProgress) { - sourcesDirty = false; - triggerClientBuild(); - } - - let urlPath: string; - try { - urlPath = new URL(req.url, 'http://localhost') - .pathname; - } catch { - res.statusCode = 400; - res.end('Invalid request URL'); - return; - } - const filePath = resolve( - clientDistDir, - urlPath.slice('/client/'.length) - ); - const rel = relative(clientDistDir, filePath); - if (rel.startsWith('..') || isAbsolute(rel)) { - res.statusCode = 403; - res.end(); - return; - } - if (!existsSync(filePath)) { - return next(); - } - const contentTypes: Record = { - '.js': 'application/javascript', - '.cjs': 'application/javascript', - '.json': 'application/json', - '.map': 'application/json', - }; - const ext = Object.keys(contentTypes).find((e) => - filePath.endsWith(e) - ); - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader( - 'Content-Type', - ext ? contentTypes[ext] : 'application/octet-stream' - ); - res.end(readFileSync(filePath)); - }); - }, - }, + serveClientLibrary(), /** * Copy the `.htaccess` file to the `dist` directory. */ diff --git a/packages/playground/website/vite.serve-client-library.ts b/packages/playground/website/vite.serve-client-library.ts new file mode 100644 index 00000000000..7f54914056a --- /dev/null +++ b/packages/playground/website/vite.serve-client-library.ts @@ -0,0 +1,180 @@ +/** + * Vite plugin that serves the built @wp-playground/client library at + * /client/ during development, matching production where + * playground.wordpress.net/client/index.js is available. + * + * Auto-builds the client if missing and triggers a rebuild when source + * files change (detected via Vite's file watcher). + */ +import type { Plugin, ViteDevServer } from 'vite'; +import { + existsSync, + readFileSync, + readdirSync, + statSync, +} from 'node:fs'; +import { join, resolve, relative, isAbsolute } from 'node:path'; +import { exec } from 'node:child_process'; + +export function serveClientLibrary(): Plugin { + return { + name: 'serve-client-library', + configureServer(server: ViteDevServer) { + const repoRoot = join(__dirname, '../../../'); + const clientDistDir = join( + repoRoot, + 'dist/packages/playground/client' + ); + const clientSrcDir = join(__dirname, '../client/src'); + let buildInProgress = false; + let stalenessChecked = false; + let sourcesDirty = false; + + function newestMtimeIn(dir: string): number { + let newest = 0; + try { + for (const entry of readdirSync(dir, { + withFileTypes: true, + })) { + const full = join(dir, entry.name); + if (entry.isDirectory()) { + newest = Math.max( + newest, + newestMtimeIn(full) + ); + } else if (entry.isFile()) { + newest = Math.max( + newest, + statSync(full).mtimeMs + ); + } + } + } catch { + // Directory may not exist yet + } + return newest; + } + + function triggerClientBuild() { + if (buildInProgress) { + return; + } + buildInProgress = true; + server.config.logger.warn( + '\n Building @wp-playground/client… Refresh when done.\n' + ); + exec( + 'npx nx build playground-client', + { cwd: repoRoot }, + (error, stdout, stderr) => { + buildInProgress = false; + stalenessChecked = true; + sourcesDirty = false; + if (error) { + server.config.logger.error( + ' @wp-playground/client build failed. ' + + 'Run manually: npx nx build playground-client\n' + ); + if (stderr) { + server.config.logger.error(stderr); + } + } else { + server.config.logger.info( + ' @wp-playground/client built. Refresh to load.\n' + ); + } + } + ); + } + + server.watcher.add(clientSrcDir); + server.watcher.on('change', (changedPath) => { + if (changedPath.startsWith(clientSrcDir)) { + sourcesDirty = true; + stalenessChecked = false; + } + }); + + server.middlewares.use((req, res, next) => { + if (!req.url?.startsWith('/client/')) { + return next(); + } + + const distIndexPath = join(clientDistDir, 'index.js'); + + if (!existsSync(distIndexPath)) { + triggerClientBuild(); + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader( + 'Content-Type', + 'application/javascript' + ); + res.statusCode = 503; + res.end( + 'throw new Error(' + + '"@wp-playground/client is not built yet. ' + + 'A build was triggered automatically — refresh in a few seconds.\\n' + + 'Or build manually: npx nx build playground-client"' + + ');' + ); + return; + } + + if ( + !stalenessChecked && + !buildInProgress + ) { + stalenessChecked = true; + const distMtime = + statSync(distIndexPath).mtimeMs; + const srcMtime = newestMtimeIn(clientSrcDir); + if (srcMtime > distMtime) { + sourcesDirty = true; + } + } + if (sourcesDirty && !buildInProgress) { + sourcesDirty = false; + triggerClientBuild(); + } + + let urlPath: string; + try { + urlPath = new URL(req.url, 'http://localhost') + .pathname; + } catch { + res.statusCode = 400; + res.end('Invalid request URL'); + return; + } + const filePath = resolve( + clientDistDir, + urlPath.slice('/client/'.length) + ); + const rel = relative(clientDistDir, filePath); + if (rel.startsWith('..') || isAbsolute(rel)) { + res.statusCode = 403; + res.end(); + return; + } + if (!existsSync(filePath)) { + return next(); + } + const contentTypes: Record = { + '.js': 'application/javascript', + '.cjs': 'application/javascript', + '.json': 'application/json', + '.map': 'application/json', + }; + const ext = Object.keys(contentTypes).find((e) => + filePath.endsWith(e) + ); + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader( + 'Content-Type', + ext ? contentTypes[ext] : 'application/octet-stream' + ); + res.end(readFileSync(filePath)); + }); + }, + }; +} From 51784309d8ecf02dac3b6a767d35c1c224c1acbd Mon Sep 17 00:00:00 2001 From: ashfame Date: Thu, 19 Mar 2026 21:45:33 +0545 Subject: [PATCH 8/8] Handle directory requests and watch for file adds/removes - Guard against EISDIR by checking isDirectory() before readFileSync - Switch watcher from 'change' to 'all' so new/deleted/renamed source files also mark the build as stale - Use relative() for path containment check in the watcher callback Made-with: Cursor --- .../website/vite.serve-client-library.ts | 40 +++++-------------- 1 file changed, 11 insertions(+), 29 deletions(-) diff --git a/packages/playground/website/vite.serve-client-library.ts b/packages/playground/website/vite.serve-client-library.ts index 7f54914056a..c030a9e17c8 100644 --- a/packages/playground/website/vite.serve-client-library.ts +++ b/packages/playground/website/vite.serve-client-library.ts @@ -7,12 +7,7 @@ * files change (detected via Vite's file watcher). */ import type { Plugin, ViteDevServer } from 'vite'; -import { - existsSync, - readFileSync, - readdirSync, - statSync, -} from 'node:fs'; +import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'; import { join, resolve, relative, isAbsolute } from 'node:path'; import { exec } from 'node:child_process'; @@ -38,15 +33,9 @@ export function serveClientLibrary(): Plugin { })) { const full = join(dir, entry.name); if (entry.isDirectory()) { - newest = Math.max( - newest, - newestMtimeIn(full) - ); + newest = Math.max(newest, newestMtimeIn(full)); } else if (entry.isFile()) { - newest = Math.max( - newest, - statSync(full).mtimeMs - ); + newest = Math.max(newest, statSync(full).mtimeMs); } } } catch { @@ -88,8 +77,9 @@ export function serveClientLibrary(): Plugin { } server.watcher.add(clientSrcDir); - server.watcher.on('change', (changedPath) => { - if (changedPath.startsWith(clientSrcDir)) { + server.watcher.on('all', (_event, changedPath) => { + const rel = relative(clientSrcDir, changedPath); + if (!rel.startsWith('..') && !isAbsolute(rel)) { sourcesDirty = true; stalenessChecked = false; } @@ -105,10 +95,7 @@ export function serveClientLibrary(): Plugin { if (!existsSync(distIndexPath)) { triggerClientBuild(); res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader( - 'Content-Type', - 'application/javascript' - ); + res.setHeader('Content-Type', 'application/javascript'); res.statusCode = 503; res.end( 'throw new Error(' + @@ -120,13 +107,9 @@ export function serveClientLibrary(): Plugin { return; } - if ( - !stalenessChecked && - !buildInProgress - ) { + if (!stalenessChecked && !buildInProgress) { stalenessChecked = true; - const distMtime = - statSync(distIndexPath).mtimeMs; + const distMtime = statSync(distIndexPath).mtimeMs; const srcMtime = newestMtimeIn(clientSrcDir); if (srcMtime > distMtime) { sourcesDirty = true; @@ -139,8 +122,7 @@ export function serveClientLibrary(): Plugin { let urlPath: string; try { - urlPath = new URL(req.url, 'http://localhost') - .pathname; + urlPath = new URL(req.url, 'http://localhost').pathname; } catch { res.statusCode = 400; res.end('Invalid request URL'); @@ -156,7 +138,7 @@ export function serveClientLibrary(): Plugin { res.end(); return; } - if (!existsSync(filePath)) { + if (!existsSync(filePath) || statSync(filePath).isDirectory()) { return next(); } const contentTypes: Record = {