Files
open-design/scripts/dev-all.mjs
4db0483721 chore: migrate frontend toolchain from Vite to Next.js 16 App Router (#66)
* Refactor project name from "Open Claude Design" to "Open Design"

- Updated project name in package.json, package-lock.json, and README files.
- Changed CLI commands and references from "ocd" to "od".
- Adjusted file structure references in documentation and code to reflect new naming conventions.
- Enhanced .gitignore to include new runtime data files.
- Updated metadata in LICENSE file to match new project name.

* chore: migrate frontend toolchain from Vite to Next.js 16 App Router

Replace the Vite SPA scaffold with Next.js 16 App Router while keeping
the existing daemon as the API/SSE/sqlite backend. The whole client
tree now mounts under a single optional catch-all route
(app/[[...slug]]) loaded with ssr:false; static export emits one shell
HTML the daemon serves as the SPA fallback for deep links. Dev uses
next.config rewrites to proxy /api, /artifacts, /frames to the daemon,
matching the previous Vite setup.

Made-with: Cursor

* fix: address Next migration review feedback

* fix: serve static export in preview script

---------

Co-authored-by: mrcfps <mrc@powerformer.com>
2026-04-29 21:33:21 +08:00

90 lines
2.9 KiB
JavaScript
Executable File

#!/usr/bin/env node
// Launcher for `npm run dev:all`.
//
// Probes for free ports for the daemon (OD_PORT, default 7456) and the
// Next.js dev server (NEXT_PORT, default 3000) before spawning
// `concurrently`, so a stray process holding either port doesn't kill the
// whole boot. The resolved ports are exported into the child env, which
// means:
// * the daemon's cli.js sees the new OD_PORT and binds to it
// * next.config.ts reads the same OD_PORT and proxies /api, /artifacts,
// /frames to the daemon's actual port
// * Next.js binds to NEXT_PORT (we pass `next dev -p $NEXT_PORT` so the
// `dev` script can stay parameter-free for the common single-process
// case where the user runs just `pnpm dev`)
//
// If a port is busy we walk forward up to PORT_SEARCH_RANGE steps and log
// the switch so the user notices.
import { spawn } from 'node:child_process';
import net from 'node:net';
const HOST = '127.0.0.1';
const PORT_SEARCH_RANGE = 50;
function isPortFree(port, host = HOST) {
return new Promise((resolve) => {
const server = net.createServer();
server.unref();
server.once('error', () => resolve(false));
server.listen({ port, host, exclusive: true }, () => {
server.close(() => resolve(true));
});
});
}
async function findFreePort(start, label) {
for (let port = start; port < start + PORT_SEARCH_RANGE; port++) {
if (await isPortFree(port)) return port;
}
throw new Error(
`[dev:all] could not find a free ${label} port near ${start} (tried ${PORT_SEARCH_RANGE})`,
);
}
const desiredDaemon = Number(process.env.OD_PORT) || 7456;
const desiredNext = Number(process.env.NEXT_PORT) || 3000;
const daemonPort = await findFreePort(desiredDaemon, 'daemon');
const nextPort = await findFreePort(desiredNext, 'next');
if (daemonPort !== desiredDaemon) {
console.log(
`[dev:all] daemon port ${desiredDaemon} is busy, switching to ${daemonPort}`,
);
}
if (nextPort !== desiredNext) {
console.log(
`[dev:all] next port ${desiredNext} is busy, switching to ${nextPort}`,
);
}
const env = {
...process.env,
OD_PORT: String(daemonPort),
NEXT_PORT: String(nextPort),
PORT: String(nextPort),
};
// `npm:daemon` is the shorthand for the daemon script, and `next dev -p
// <port>` is invoked directly so we can pass the resolved port without
// round-tripping through npm scripts. Keep the port numeric before it reaches
// the command string, and avoid shell interpretation on POSIX; Windows needs
// shell mode so the local `.cmd` shim can resolve.
const child = spawn(
'concurrently',
['-k', '-n', 'daemon,web', '-c', 'cyan,magenta', 'npm:daemon', `next dev -p ${nextPort}`],
{ env, stdio: 'inherit', shell: process.platform === 'win32' },
);
child.on('exit', (code, signal) => {
if (signal) process.kill(process.pid, signal);
else process.exit(code ?? 0);
});
for (const sig of ['SIGINT', 'SIGTERM']) {
process.on(sig, () => {
if (!child.killed) child.kill(sig);
});
}