Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/playground/website/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ 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';
Expand Down Expand Up @@ -190,6 +191,7 @@ export default defineConfig(({ command, mode }) => {
server.middlewares.use(oAuthMiddleware);
},
},
serveClientLibrary(),
/**
* Copy the `.htaccess` file to the `dist` directory.
*/
Expand Down
162 changes: 162 additions & 0 deletions packages/playground/website/vite.serve-client-library.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/**
* 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('all', (_event, changedPath) => {
const rel = relative(clientSrcDir, changedPath);
if (!rel.startsWith('..') && !isAbsolute(rel)) {
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) || statSync(filePath).isDirectory()) {
return next();
}
const contentTypes: Record<string, string> = {
'.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));
});
},
};
}
Loading