@atolljs/cli 0.1.8 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +238 -32
- package/package.json +1 -1
- package/src/cli.ts +8 -1
- package/src/commands.ts +185 -24
- package/src/templates.ts +91 -9
package/dist/cli.js
CHANGED
|
@@ -209,6 +209,9 @@ var NPMRC = `# Supply-chain hardening \u2014 read by every npm command run in th
|
|
|
209
209
|
# lockfile (requires npm >= 11.10 \u2014 shipped with Node 26; older npm
|
|
210
210
|
# silently ignores this key).
|
|
211
211
|
min-release-age=7
|
|
212
|
+
# First-party packages must install even inside the age window (npm 11.10+;
|
|
213
|
+
# older npm silently ignores the key).
|
|
214
|
+
min-release-age-exclude[]=@atolljs/*
|
|
212
215
|
# Install lifecycle scripts are the dominant malware vector \u2014 none run
|
|
213
216
|
# during install/ci. If a native dep ever legitimately needs one:
|
|
214
217
|
# npm rebuild <pkg> --ignore-scripts=false
|
|
@@ -222,7 +225,7 @@ dist/
|
|
|
222
225
|
`;
|
|
223
226
|
function memoryContract(name) {
|
|
224
227
|
const c = camel(name);
|
|
225
|
-
return `import { defineSharedMemory, field,
|
|
228
|
+
return `import { defineSharedMemory, field, reef } from '@atolljs/core';
|
|
226
229
|
|
|
227
230
|
/**
|
|
228
231
|
* ${c} shared memory \u2014 the single source of truth for state both threads
|
|
@@ -238,10 +241,10 @@ export const ${c}Memory = defineSharedMemory({
|
|
|
238
241
|
// Structured snapshots \u2014 one inline record, rewritten wholesale.
|
|
239
242
|
state: {
|
|
240
243
|
summary: field.object({
|
|
241
|
-
schema:
|
|
242
|
-
total:
|
|
243
|
-
errors:
|
|
244
|
-
updatedAt:
|
|
244
|
+
schema: reef.object({
|
|
245
|
+
total: reef.f64(),
|
|
246
|
+
errors: reef.f64(),
|
|
247
|
+
updatedAt: reef.u32(),
|
|
245
248
|
}),
|
|
246
249
|
}),
|
|
247
250
|
},
|
|
@@ -1789,8 +1792,10 @@ ${serviceImport}${memImport}
|
|
|
1789
1792
|
imports: [
|
|
1790
1793
|
AtollModule.registerPool({
|
|
1791
1794
|
name: '${name}',
|
|
1792
|
-
//
|
|
1793
|
-
|
|
1795
|
+
// the emitted worker bundle \u2014 keep the URL inline. Under a Vite SSR
|
|
1796
|
+
// build declare ${name}.worker.ts as a build input; on webpack point
|
|
1797
|
+
// this at the './${name}.worker.ts' source instead.
|
|
1798
|
+
worker: () => new Worker(new URL('./${name}.worker.js', import.meta.url)),
|
|
1794
1799
|
${memLine} poolSize: 'auto',
|
|
1795
1800
|
}),
|
|
1796
1801
|
],
|
|
@@ -1881,7 +1886,7 @@ function nestjsHousedFiles(name, dir) {
|
|
|
1881
1886
|
// runs in. Message-only by default; to share another pool's buffer, wrap
|
|
1882
1887
|
// the factory:
|
|
1883
1888
|
// worker: withSharedBuffer(
|
|
1884
|
-
// () => new Worker(new URL('./${name}.worker.
|
|
1889
|
+
// () => new Worker(new URL('./${name}.worker.js', import.meta.url)),
|
|
1885
1890
|
// () => getAtollPool('<other-pool>')?.sharedBuffer,
|
|
1886
1891
|
// )
|
|
1887
1892
|
import { Module } from '@nestjs/common';
|
|
@@ -1892,7 +1897,10 @@ import { AtollModule } from '@atolljs/nestjs';
|
|
|
1892
1897
|
imports: [
|
|
1893
1898
|
AtollModule.registerPool({
|
|
1894
1899
|
name: '${name}',
|
|
1895
|
-
worker
|
|
1900
|
+
// the emitted worker bundle \u2014 keep the URL inline. Under a Vite SSR
|
|
1901
|
+
// build declare ${name}.worker.ts as a build input; on webpack point
|
|
1902
|
+
// this at the './${name}.worker.ts' source instead.
|
|
1903
|
+
worker: () => new Worker(new URL('./${name}.worker.js', import.meta.url)),
|
|
1896
1904
|
poolSize: 2,
|
|
1897
1905
|
}),
|
|
1898
1906
|
],
|
|
@@ -2245,6 +2253,63 @@ initDevtools({
|
|
|
2245
2253
|
initDevtools({ session: { name: '${name}' } });
|
|
2246
2254
|
`;
|
|
2247
2255
|
}
|
|
2256
|
+
function nestjsViteConfig() {
|
|
2257
|
+
return `import { readdirSync } from 'node:fs';
|
|
2258
|
+
import { basename } from 'node:path';
|
|
2259
|
+
import { defineConfig } from 'vite';
|
|
2260
|
+
|
|
2261
|
+
// Every *.worker.ts under src/ is its own entry \u2014 pool configs reference the
|
|
2262
|
+
// emitted './x.worker.js' via new URL(..., import.meta.url). Keys are
|
|
2263
|
+
// basenames, so output is flat and the literal resolves from dist/main.js.
|
|
2264
|
+
const workerInputs = Object.fromEntries(
|
|
2265
|
+
readdirSync('src', { recursive: true, withFileTypes: true })
|
|
2266
|
+
.filter((e) => e.isFile() && e.name.endsWith('.worker.ts'))
|
|
2267
|
+
.map((e) => {
|
|
2268
|
+
const dir = (e.parentPath ?? e.path).replaceAll('\\\\', '/');
|
|
2269
|
+
return [basename(e.name, '.ts'), \`\${dir}/\${e.name}\`];
|
|
2270
|
+
}),
|
|
2271
|
+
);
|
|
2272
|
+
|
|
2273
|
+
export default defineConfig({
|
|
2274
|
+
build: {
|
|
2275
|
+
// Node SSR build: node builtins + node_modules stay external.
|
|
2276
|
+
ssr: true,
|
|
2277
|
+
outDir: 'dist',
|
|
2278
|
+
rollupOptions: {
|
|
2279
|
+
input: { main: 'src/main.ts', ...workerInputs },
|
|
2280
|
+
// Flat layout: a './x.worker.js' literal resolves to dist/<name>.worker.js
|
|
2281
|
+
// whether it lands in main.js or a shared chunk (the default assets/
|
|
2282
|
+
// subdir would put chunks one level too deep).
|
|
2283
|
+
output: { entryFileNames: '[name].js', chunkFileNames: '[name]-[hash].js' },
|
|
2284
|
+
},
|
|
2285
|
+
},
|
|
2286
|
+
});
|
|
2287
|
+
`;
|
|
2288
|
+
}
|
|
2289
|
+
function devServerScript() {
|
|
2290
|
+
return `// Dev loop: vite's Rollup watcher rebuilds main + worker bundles on change
|
|
2291
|
+
// while \`node --watch\` restarts the app whenever dist/ updates \u2014 a worker
|
|
2292
|
+
// reload is a respawn anyway. Programmatic API: no npx/.cmd spawn (Node
|
|
2293
|
+
// 20.12+ blocks .cmd spawn without shell:true on Windows \u2192 EINVAL).
|
|
2294
|
+
import { spawn } from 'node:child_process';
|
|
2295
|
+
import { build } from 'vite';
|
|
2296
|
+
|
|
2297
|
+
await build(); // initial build \u2014 throws and exits on failure
|
|
2298
|
+
await build({ build: { watch: {} } }); // keeps the process alive
|
|
2299
|
+
|
|
2300
|
+
const child = spawn(process.execPath, ['--watch', 'dist/main.js'], {
|
|
2301
|
+
stdio: 'inherit',
|
|
2302
|
+
});
|
|
2303
|
+
|
|
2304
|
+
const shutdown = () => {
|
|
2305
|
+
child.kill();
|
|
2306
|
+
process.exit(0);
|
|
2307
|
+
};
|
|
2308
|
+
process.on('SIGINT', shutdown);
|
|
2309
|
+
process.on('SIGTERM', shutdown);
|
|
2310
|
+
process.on('exit', () => child.kill());
|
|
2311
|
+
`;
|
|
2312
|
+
}
|
|
2248
2313
|
|
|
2249
2314
|
// src/commands.ts
|
|
2250
2315
|
var NODE_HOSTS = ["node", "nestjs"];
|
|
@@ -2272,7 +2337,14 @@ async function offerInstall(ctx, project, pkgs, installAll = false) {
|
|
|
2272
2337
|
cwd: project.root ?? ctx.cwd,
|
|
2273
2338
|
stdio: "inherit"
|
|
2274
2339
|
});
|
|
2275
|
-
if (r.status !== 0)
|
|
2340
|
+
if (r.status !== 0) {
|
|
2341
|
+
if (project.framework === "nestjs") {
|
|
2342
|
+
ctx.io.warn(
|
|
2343
|
+
"if npm reported ERESOLVE: the installed @nestjs/* major may be outside the range @atolljs/nestjs supports (@nestjs/common and @nestjs/core 11.x or 12.x). Align all @nestjs/* packages to a supported major, or rerun the install with --legacy-peer-deps."
|
|
2344
|
+
);
|
|
2345
|
+
}
|
|
2346
|
+
throw new Error(`install failed \u2014 run \`${cmd}\` manually`);
|
|
2347
|
+
}
|
|
2276
2348
|
}
|
|
2277
2349
|
async function runInit(ctx) {
|
|
2278
2350
|
const project = detectProject(ctx.cwd);
|
|
@@ -2289,36 +2361,54 @@ async function runInit(ctx) {
|
|
|
2289
2361
|
await writeTree(ctx.io, ctx.cwd, [{ path: rel(join3(project.root, ".npmrc")), content: NPMRC }], {
|
|
2290
2362
|
force: hasFlag(ctx.args, "force")
|
|
2291
2363
|
});
|
|
2292
|
-
await offerInstall(ctx, project, ATOLL_DEPS[project.framework]);
|
|
2293
2364
|
const atollDir = join3(project.srcDir, "atoll");
|
|
2294
2365
|
const host = hostOf(project.framework);
|
|
2295
2366
|
const name = flag(ctx.args, "name") ?? "app";
|
|
2296
2367
|
const files = [
|
|
2297
|
-
{ path: `${rel(atollDir)}/${name}.memory.ts`, content: memoryContract(name) }
|
|
2298
|
-
{
|
|
2299
|
-
path: `${rel(atollDir)}/${name}.worker.ts`,
|
|
2300
|
-
content: workerEntry(name, {
|
|
2301
|
-
host,
|
|
2302
|
-
memoryImport: `./${name}.memory`,
|
|
2303
|
-
memoryName: `${camel(name)}Memory`,
|
|
2304
|
-
demoMemoryWrite: true
|
|
2305
|
-
})
|
|
2306
|
-
},
|
|
2307
|
-
{
|
|
2308
|
-
path: `${rel(atollDir)}/${name}.ts`,
|
|
2309
|
-
content: workerClientFile(name, {
|
|
2310
|
-
host,
|
|
2311
|
-
memoryImport: `./${name}.memory`,
|
|
2312
|
-
memoryName: `${camel(name)}Memory`,
|
|
2313
|
-
// src/atoll/x.ts → <root>/dist/x.worker.js
|
|
2314
|
-
distUrl: `${relative(atollDir, join3(project.root, "dist")).replaceAll("\\", "/")}/${name}.worker.js`
|
|
2315
|
-
})
|
|
2316
|
-
}
|
|
2368
|
+
{ path: `${rel(atollDir)}/${name}.memory.ts`, content: memoryContract(name) }
|
|
2317
2369
|
];
|
|
2370
|
+
if (project.framework === "nestjs") {
|
|
2371
|
+
files.push(
|
|
2372
|
+
...nestjsServiceFiles(name, rel(atollDir), {
|
|
2373
|
+
import_: `./${name}.memory`,
|
|
2374
|
+
export_: `${camel(name)}Memory`
|
|
2375
|
+
})
|
|
2376
|
+
);
|
|
2377
|
+
} else {
|
|
2378
|
+
files.push(
|
|
2379
|
+
{
|
|
2380
|
+
path: `${rel(atollDir)}/${name}.worker.ts`,
|
|
2381
|
+
content: workerEntry(name, {
|
|
2382
|
+
host,
|
|
2383
|
+
memoryImport: `./${name}.memory`,
|
|
2384
|
+
memoryName: `${camel(name)}Memory`,
|
|
2385
|
+
demoMemoryWrite: true
|
|
2386
|
+
})
|
|
2387
|
+
},
|
|
2388
|
+
{
|
|
2389
|
+
path: `${rel(atollDir)}/${name}.ts`,
|
|
2390
|
+
content: workerClientFile(name, {
|
|
2391
|
+
host,
|
|
2392
|
+
memoryImport: `./${name}.memory`,
|
|
2393
|
+
memoryName: `${camel(name)}Memory`,
|
|
2394
|
+
// src/atoll/x.ts → <root>/dist/x.worker.js
|
|
2395
|
+
distUrl: `${relative(atollDir, join3(project.root, "dist")).replaceAll("\\", "/")}/${name}.worker.js`
|
|
2396
|
+
})
|
|
2397
|
+
}
|
|
2398
|
+
);
|
|
2399
|
+
}
|
|
2318
2400
|
await writeTree(ctx.io, ctx.cwd, files, { force: hasFlag(ctx.args, "force") });
|
|
2401
|
+
await offerInstall(ctx, project, ATOLL_DEPS[project.framework]);
|
|
2319
2402
|
ctx.io.print("");
|
|
2320
2403
|
ctx.io.print(ctx.io.fmt.strong("next steps:"));
|
|
2321
|
-
if (
|
|
2404
|
+
if (project.framework === "nestjs") {
|
|
2405
|
+
ctx.io.print(
|
|
2406
|
+
` import ${ctx.io.fmt.accent(`${pascal(name)}Module`)} into AppModule, then inject ${ctx.io.fmt.accent(`${pascal(name)}Service`)} \u2014 ${ctx.io.fmt.accent(`service.echo('hi')`)} runs in a worker`
|
|
2407
|
+
);
|
|
2408
|
+
ctx.io.print(
|
|
2409
|
+
` worker entries emit as ${ctx.io.fmt.accent(`${name}.worker.js`)} \u2014 declare them as build inputs (see docs/frameworks/nestjs.md)`
|
|
2410
|
+
);
|
|
2411
|
+
} else if (host === "node") {
|
|
2322
2412
|
ctx.io.print(
|
|
2323
2413
|
` bundle the worker entry (see the comment in ${name}.ts), then call ${ctx.io.fmt.accent(`${camel(name)}.echo('hi')`)}`
|
|
2324
2414
|
);
|
|
@@ -2332,6 +2422,115 @@ async function runInit(ctx) {
|
|
|
2332
2422
|
);
|
|
2333
2423
|
return 0;
|
|
2334
2424
|
}
|
|
2425
|
+
function rewriteWorkerUrls(srcDir) {
|
|
2426
|
+
const urlRe = /(new URL\(\s*['"`][^'"`]*?\.worker)\.ts(['"`])/g;
|
|
2427
|
+
const changed = [];
|
|
2428
|
+
const walk = (dir) => {
|
|
2429
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
2430
|
+
const p = join3(dir, e.name);
|
|
2431
|
+
if (e.isDirectory()) walk(p);
|
|
2432
|
+
else if (e.name.endsWith(".ts")) {
|
|
2433
|
+
const src = readFileSync2(p, "utf8");
|
|
2434
|
+
const next = src.replace(urlRe, "$1.js$2");
|
|
2435
|
+
if (next !== src) {
|
|
2436
|
+
writeFileSync2(p, next);
|
|
2437
|
+
changed.push(p);
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
};
|
|
2442
|
+
walk(srcDir);
|
|
2443
|
+
return changed;
|
|
2444
|
+
}
|
|
2445
|
+
async function runConvert(ctx) {
|
|
2446
|
+
const target = ctx.args._[1];
|
|
2447
|
+
if (target !== "vite") {
|
|
2448
|
+
ctx.io.error(
|
|
2449
|
+
`unknown convert target ${ctx.io.fmt.accent(target ?? "(missing)")} \u2014 expected 'vite'`
|
|
2450
|
+
);
|
|
2451
|
+
return 1;
|
|
2452
|
+
}
|
|
2453
|
+
const project = detectProject(ctx.cwd);
|
|
2454
|
+
if (!project.root || !project.pkg) {
|
|
2455
|
+
ctx.io.error("no package.json found \u2014 cd into the project to convert");
|
|
2456
|
+
return 1;
|
|
2457
|
+
}
|
|
2458
|
+
if (project.framework !== "nestjs") {
|
|
2459
|
+
ctx.io.error(
|
|
2460
|
+
`convert vite supports nestjs projects \u2014 detected "${project.framework}"`
|
|
2461
|
+
);
|
|
2462
|
+
return 1;
|
|
2463
|
+
}
|
|
2464
|
+
const root = project.root;
|
|
2465
|
+
const rel = (abs) => relative(ctx.cwd, abs).replaceAll("\\", "/");
|
|
2466
|
+
const force = hasFlag(ctx.args, "force");
|
|
2467
|
+
const srcDir = project.srcDir;
|
|
2468
|
+
await writeTree(
|
|
2469
|
+
ctx.io,
|
|
2470
|
+
ctx.cwd,
|
|
2471
|
+
[
|
|
2472
|
+
{ path: rel(join3(root, "vite.config.ts")), content: nestjsViteConfig() },
|
|
2473
|
+
{ path: rel(join3(root, "dev.mjs")), content: devServerScript() }
|
|
2474
|
+
],
|
|
2475
|
+
{ force }
|
|
2476
|
+
);
|
|
2477
|
+
const rewritten = rewriteWorkerUrls(srcDir);
|
|
2478
|
+
for (const p of rewritten) ctx.io.print(` rewrote worker URL \u2014 ${rel(p)}`);
|
|
2479
|
+
const pkgPath = join3(root, "package.json");
|
|
2480
|
+
const pkg = JSON.parse(readFileSync2(pkgPath, "utf8"));
|
|
2481
|
+
pkg.type = "module";
|
|
2482
|
+
pkg.scripts = {
|
|
2483
|
+
...pkg.scripts,
|
|
2484
|
+
dev: "node dev.mjs",
|
|
2485
|
+
build: "tsc --noEmit && vite build",
|
|
2486
|
+
start: "node dist/main.js"
|
|
2487
|
+
};
|
|
2488
|
+
pkg.devDependencies = { ...pkg.devDependencies, vite: pkg.devDependencies?.vite ?? "^8.3.1" };
|
|
2489
|
+
writeFileSync2(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
2490
|
+
ctx.io.print(` package.json \u2014 type:module, dev/build/start scripts, vite devDep`);
|
|
2491
|
+
const tsconfigPath = join3(root, "tsconfig.json");
|
|
2492
|
+
try {
|
|
2493
|
+
const raw = readFileSync2(tsconfigPath, "utf8").replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|\s)\/\/.*$/gm, "$1").replace(/,(\s*[}\]])/g, "$1");
|
|
2494
|
+
const tsconfig = JSON.parse(raw);
|
|
2495
|
+
tsconfig.compilerOptions ??= {};
|
|
2496
|
+
Object.assign(tsconfig.compilerOptions, {
|
|
2497
|
+
module: "ESNext",
|
|
2498
|
+
moduleResolution: "bundler",
|
|
2499
|
+
noEmit: true
|
|
2500
|
+
});
|
|
2501
|
+
tsconfig.include = [.../* @__PURE__ */ new Set([...tsconfig.include ?? ["src"], "vite.config.ts"])];
|
|
2502
|
+
writeFileSync2(tsconfigPath, JSON.stringify(tsconfig, null, 2) + "\n");
|
|
2503
|
+
ctx.io.print(` tsconfig.json \u2014 module ESNext, moduleResolution bundler, noEmit`);
|
|
2504
|
+
} catch {
|
|
2505
|
+
ctx.io.warn(
|
|
2506
|
+
"could not patch tsconfig.json \u2014 set module: ESNext, moduleResolution: bundler, noEmit: true manually"
|
|
2507
|
+
);
|
|
2508
|
+
}
|
|
2509
|
+
const mainTs = join3(srcDir, "main.ts");
|
|
2510
|
+
if (existsSync3(mainTs)) {
|
|
2511
|
+
const src = readFileSync2(mainTs, "utf8");
|
|
2512
|
+
if (!src.includes("reflect-metadata")) {
|
|
2513
|
+
writeFileSync2(mainTs, `import 'reflect-metadata';
|
|
2514
|
+
${src}`);
|
|
2515
|
+
ctx.io.print(` main.ts \u2014 prepended import 'reflect-metadata'`);
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
for (const leftover of ["nest-cli.json", "webpack.config.js", "webpack.config.cjs"]) {
|
|
2519
|
+
if (existsSync3(join3(root, leftover))) {
|
|
2520
|
+
ctx.io.print(ctx.io.fmt.dim(` ${leftover} is no longer used \u2014 safe to delete`));
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
await offerInstall(ctx, project, ["vite"], true);
|
|
2524
|
+
ctx.io.print("");
|
|
2525
|
+
ctx.io.print(ctx.io.fmt.strong("converted to a Vite SSR build:"));
|
|
2526
|
+
ctx.io.print(
|
|
2527
|
+
` ${ctx.io.fmt.accent("npm run dev")} \u2014 vite build --watch + node --watch dist/main.js`
|
|
2528
|
+
);
|
|
2529
|
+
ctx.io.print(
|
|
2530
|
+
` ${ctx.io.fmt.accent("npm run build")} \u2192 dist/ \xB7 ${ctx.io.fmt.accent("npm start")} \u2014 node dist/main.js`
|
|
2531
|
+
);
|
|
2532
|
+
return 0;
|
|
2533
|
+
}
|
|
2335
2534
|
var ADD_KINDS = [
|
|
2336
2535
|
"worker",
|
|
2337
2536
|
"memory",
|
|
@@ -2883,6 +3082,11 @@ var HELP = (fmt) => `${fmt.strong("atoll")} \u2014 bootstrap multithreading ${fm
|
|
|
2883
3082
|
Frameworks: react vue solid svelte angular nestjs nextjs node
|
|
2884
3083
|
[--dir <path>] [--memory <name>|--no-memory] [--variant <v>] [--force]
|
|
2885
3084
|
|
|
3085
|
+
${fmt.accent("atoll convert vite")} [--force]
|
|
3086
|
+
Convert a NestJS project from the webpack build to a Vite SSR build \u2014
|
|
3087
|
+
vite.config.ts + dev.mjs, worker URLs repointed at emitted bundles,
|
|
3088
|
+
package.json/tsconfig patched.
|
|
3089
|
+
|
|
2886
3090
|
${fmt.accent("atoll doctor")} [--fix]
|
|
2887
3091
|
Check the setup \u2014 deps, lockfile, .npmrc policy, bundler-detectable
|
|
2888
3092
|
worker entries, COOP/COEP headers. --fix writes a hardened .npmrc.
|
|
@@ -2910,6 +3114,8 @@ async function runCli(argv, io, cwd) {
|
|
|
2910
3114
|
return await runInit(ctx);
|
|
2911
3115
|
case "add":
|
|
2912
3116
|
return await runAdd(ctx);
|
|
3117
|
+
case "convert":
|
|
3118
|
+
return await runConvert(ctx);
|
|
2913
3119
|
case "doctor":
|
|
2914
3120
|
return runDoctor(ctx);
|
|
2915
3121
|
case "devtools":
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atolljs/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "atoll — scaffold worker pools, shared-memory contracts, and framework islands (experimental)",
|
|
5
5
|
"homepage": "https://jwhenry3.github.io/atolljs/consumer/quickstart/",
|
|
6
6
|
"type": "module",
|
package/src/cli.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { consoleIo, type Fmt, type Io } from './io.ts';
|
|
14
14
|
import { hasFlag, parseArgs } from './parse.ts';
|
|
15
|
-
import { runAdd, runCreate, runDevtools, runDoctor, runInit, type Ctx } from './commands.ts';
|
|
15
|
+
import { runAdd, runConvert, runCreate, runDevtools, runDoctor, runInit, type Ctx } from './commands.ts';
|
|
16
16
|
|
|
17
17
|
const HELP = (fmt: Fmt) => `${fmt.strong('atoll')} — bootstrap multithreading ${fmt.dim('(experimental)')}
|
|
18
18
|
|
|
@@ -44,6 +44,11 @@ const HELP = (fmt: Fmt) => `${fmt.strong('atoll')} — bootstrap multithreading
|
|
|
44
44
|
Frameworks: react vue solid svelte angular nestjs nextjs node
|
|
45
45
|
[--dir <path>] [--memory <name>|--no-memory] [--variant <v>] [--force]
|
|
46
46
|
|
|
47
|
+
${fmt.accent('atoll convert vite')} [--force]
|
|
48
|
+
Convert a NestJS project from the webpack build to a Vite SSR build —
|
|
49
|
+
vite.config.ts + dev.mjs, worker URLs repointed at emitted bundles,
|
|
50
|
+
package.json/tsconfig patched.
|
|
51
|
+
|
|
47
52
|
${fmt.accent('atoll doctor')} [--fix]
|
|
48
53
|
Check the setup — deps, lockfile, .npmrc policy, bundler-detectable
|
|
49
54
|
worker entries, COOP/COEP headers. --fix writes a hardened .npmrc.
|
|
@@ -77,6 +82,8 @@ export async function runCli(argv: readonly string[], io: Io, cwd: string): Prom
|
|
|
77
82
|
return await runInit(ctx);
|
|
78
83
|
case 'add':
|
|
79
84
|
return await runAdd(ctx);
|
|
85
|
+
case 'convert':
|
|
86
|
+
return await runConvert(ctx);
|
|
80
87
|
case 'doctor':
|
|
81
88
|
return runDoctor(ctx);
|
|
82
89
|
case 'devtools':
|
package/src/commands.ts
CHANGED
|
@@ -61,7 +61,14 @@ async function offerInstall(
|
|
|
61
61
|
cwd: project.root ?? ctx.cwd,
|
|
62
62
|
stdio: 'inherit',
|
|
63
63
|
});
|
|
64
|
-
if (r.status !== 0)
|
|
64
|
+
if (r.status !== 0) {
|
|
65
|
+
if (project.framework === 'nestjs') {
|
|
66
|
+
ctx.io.warn(
|
|
67
|
+
'if npm reported ERESOLVE: the installed @nestjs/* major may be outside the range @atolljs/nestjs supports (@nestjs/common and @nestjs/core 11.x or 12.x). Align all @nestjs/* packages to a supported major, or rerun the install with --legacy-peer-deps.',
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
throw new Error(`install failed — run \`${cmd}\` manually`);
|
|
71
|
+
}
|
|
65
72
|
}
|
|
66
73
|
|
|
67
74
|
/* ── atoll init ─────────────────────────────────────────────────────────── */
|
|
@@ -84,39 +91,60 @@ export async function runInit(ctx: Ctx): Promise<number> {
|
|
|
84
91
|
force: hasFlag(ctx.args, 'force'),
|
|
85
92
|
});
|
|
86
93
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
//
|
|
94
|
+
// A working spine — written BEFORE the install so a failed install still
|
|
95
|
+
// leaves a complete, retryable scaffold. NestJS gets the DI-native spine
|
|
96
|
+
// (service facade + registerPool module + worker entry); other hosts get
|
|
97
|
+
// the contract + worker + client trio.
|
|
90
98
|
const atollDir = join(project.srcDir, 'atoll');
|
|
91
99
|
const host = hostOf(project.framework);
|
|
92
100
|
const name = flag(ctx.args, 'name') ?? 'app';
|
|
93
101
|
const files: OutFile[] = [
|
|
94
102
|
{ path: `${rel(atollDir)}/${name}.memory.ts`, content: T.memoryContract(name) },
|
|
95
|
-
{
|
|
96
|
-
path: `${rel(atollDir)}/${name}.worker.ts`,
|
|
97
|
-
content: T.workerEntry(name, {
|
|
98
|
-
host,
|
|
99
|
-
memoryImport: `./${name}.memory`,
|
|
100
|
-
memoryName: `${camel(name)}Memory`,
|
|
101
|
-
demoMemoryWrite: true,
|
|
102
|
-
}),
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
path: `${rel(atollDir)}/${name}.ts`,
|
|
106
|
-
content: T.workerClientFile(name, {
|
|
107
|
-
host,
|
|
108
|
-
memoryImport: `./${name}.memory`,
|
|
109
|
-
memoryName: `${camel(name)}Memory`,
|
|
110
|
-
// src/atoll/x.ts → <root>/dist/x.worker.js
|
|
111
|
-
distUrl: `${relative(atollDir, join(project.root, 'dist')).replaceAll('\\', '/')}/${name}.worker.js`,
|
|
112
|
-
}),
|
|
113
|
-
},
|
|
114
103
|
];
|
|
104
|
+
if (project.framework === 'nestjs') {
|
|
105
|
+
files.push(
|
|
106
|
+
...T.nestjsServiceFiles(name, rel(atollDir), {
|
|
107
|
+
import_: `./${name}.memory`,
|
|
108
|
+
export_: `${camel(name)}Memory`,
|
|
109
|
+
}),
|
|
110
|
+
);
|
|
111
|
+
} else {
|
|
112
|
+
files.push(
|
|
113
|
+
{
|
|
114
|
+
path: `${rel(atollDir)}/${name}.worker.ts`,
|
|
115
|
+
content: T.workerEntry(name, {
|
|
116
|
+
host,
|
|
117
|
+
memoryImport: `./${name}.memory`,
|
|
118
|
+
memoryName: `${camel(name)}Memory`,
|
|
119
|
+
demoMemoryWrite: true,
|
|
120
|
+
}),
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
path: `${rel(atollDir)}/${name}.ts`,
|
|
124
|
+
content: T.workerClientFile(name, {
|
|
125
|
+
host,
|
|
126
|
+
memoryImport: `./${name}.memory`,
|
|
127
|
+
memoryName: `${camel(name)}Memory`,
|
|
128
|
+
// src/atoll/x.ts → <root>/dist/x.worker.js
|
|
129
|
+
distUrl: `${relative(atollDir, join(project.root, 'dist')).replaceAll('\\', '/')}/${name}.worker.js`,
|
|
130
|
+
}),
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
}
|
|
115
134
|
await writeTree(ctx.io, ctx.cwd, files, { force: hasFlag(ctx.args, 'force') });
|
|
116
135
|
|
|
136
|
+
await offerInstall(ctx, project, ATOLL_DEPS[project.framework]);
|
|
137
|
+
|
|
117
138
|
ctx.io.print('');
|
|
118
139
|
ctx.io.print(ctx.io.fmt.strong('next steps:'));
|
|
119
|
-
if (
|
|
140
|
+
if (project.framework === 'nestjs') {
|
|
141
|
+
ctx.io.print(
|
|
142
|
+
` import ${ctx.io.fmt.accent(`${pascal(name)}Module`)} into AppModule, then inject ${ctx.io.fmt.accent(`${pascal(name)}Service`)} — ${ctx.io.fmt.accent(`service.echo('hi')`)} runs in a worker`,
|
|
143
|
+
);
|
|
144
|
+
ctx.io.print(
|
|
145
|
+
` worker entries emit as ${ctx.io.fmt.accent(`${name}.worker.js`)} — declare them as build inputs (see docs/frameworks/nestjs.md)`,
|
|
146
|
+
);
|
|
147
|
+
} else if (host === 'node') {
|
|
120
148
|
ctx.io.print(
|
|
121
149
|
` bundle the worker entry (see the comment in ${name}.ts), then call ${ctx.io.fmt.accent(`${camel(name)}.echo('hi')`)}`,
|
|
122
150
|
);
|
|
@@ -131,6 +159,139 @@ export async function runInit(ctx: Ctx): Promise<number> {
|
|
|
131
159
|
return 0;
|
|
132
160
|
}
|
|
133
161
|
|
|
162
|
+
/* ── atoll convert vite ─────────────────────────────────────────────────── */
|
|
163
|
+
|
|
164
|
+
/** Repoint `new URL('./x.worker.ts', import.meta.url)` literals at the emitted
|
|
165
|
+
* bundles — Vite SSR build takes workers as explicit inputs, so pool
|
|
166
|
+
* factories reference `./x.worker.js`. Returns the files it changed. */
|
|
167
|
+
function rewriteWorkerUrls(srcDir: string): string[] {
|
|
168
|
+
const urlRe = /(new URL\(\s*['"`][^'"`]*?\.worker)\.ts(['"`])/g;
|
|
169
|
+
const changed: string[] = [];
|
|
170
|
+
const walk = (dir: string) => {
|
|
171
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
172
|
+
const p = join(dir, e.name);
|
|
173
|
+
if (e.isDirectory()) walk(p);
|
|
174
|
+
else if (e.name.endsWith('.ts')) {
|
|
175
|
+
const src = readFileSync(p, 'utf8');
|
|
176
|
+
const next = src.replace(urlRe, '$1.js$2');
|
|
177
|
+
if (next !== src) {
|
|
178
|
+
writeFileSync(p, next);
|
|
179
|
+
changed.push(p);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
walk(srcDir);
|
|
185
|
+
return changed;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export async function runConvert(ctx: Ctx): Promise<number> {
|
|
189
|
+
const target = ctx.args._[1];
|
|
190
|
+
if (target !== 'vite') {
|
|
191
|
+
ctx.io.error(
|
|
192
|
+
`unknown convert target ${ctx.io.fmt.accent(target ?? '(missing)')} — expected 'vite'`,
|
|
193
|
+
);
|
|
194
|
+
return 1;
|
|
195
|
+
}
|
|
196
|
+
const project = detectProject(ctx.cwd);
|
|
197
|
+
if (!project.root || !project.pkg) {
|
|
198
|
+
ctx.io.error('no package.json found — cd into the project to convert');
|
|
199
|
+
return 1;
|
|
200
|
+
}
|
|
201
|
+
if (project.framework !== 'nestjs') {
|
|
202
|
+
ctx.io.error(
|
|
203
|
+
`convert vite supports nestjs projects — detected "${project.framework}"`,
|
|
204
|
+
);
|
|
205
|
+
return 1;
|
|
206
|
+
}
|
|
207
|
+
const root = project.root;
|
|
208
|
+
const rel = (abs: string) => relative(ctx.cwd, abs).replaceAll('\\', '/');
|
|
209
|
+
const force = hasFlag(ctx.args, 'force');
|
|
210
|
+
const srcDir = project.srcDir;
|
|
211
|
+
|
|
212
|
+
// Build files: SSR multi-entry config + the watch dev loop.
|
|
213
|
+
await writeTree(
|
|
214
|
+
ctx.io,
|
|
215
|
+
ctx.cwd,
|
|
216
|
+
[
|
|
217
|
+
{ path: rel(join(root, 'vite.config.ts')), content: T.nestjsViteConfig() },
|
|
218
|
+
{ path: rel(join(root, 'dev.mjs')), content: T.devServerScript() },
|
|
219
|
+
],
|
|
220
|
+
{ force },
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
// Pool factories reference the emitted worker bundles now.
|
|
224
|
+
const rewritten = rewriteWorkerUrls(srcDir);
|
|
225
|
+
for (const p of rewritten) ctx.io.print(` rewrote worker URL — ${rel(p)}`);
|
|
226
|
+
|
|
227
|
+
// package.json: ESM, vite scripts, vite devDep.
|
|
228
|
+
const pkgPath = join(root, 'package.json');
|
|
229
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
230
|
+
pkg.type = 'module';
|
|
231
|
+
pkg.scripts = {
|
|
232
|
+
...pkg.scripts,
|
|
233
|
+
dev: 'node dev.mjs',
|
|
234
|
+
build: 'tsc --noEmit && vite build',
|
|
235
|
+
start: 'node dist/main.js',
|
|
236
|
+
};
|
|
237
|
+
pkg.devDependencies = { ...pkg.devDependencies, vite: pkg.devDependencies?.vite ?? '^8.3.1' };
|
|
238
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
239
|
+
ctx.io.print(` package.json — type:module, dev/build/start scripts, vite devDep`);
|
|
240
|
+
|
|
241
|
+
// tsconfig: bundler resolution so extensionless imports keep typechecking;
|
|
242
|
+
// typecheck-only (noEmit) since vite owns emit.
|
|
243
|
+
const tsconfigPath = join(root, 'tsconfig.json');
|
|
244
|
+
try {
|
|
245
|
+
const raw = readFileSync(tsconfigPath, 'utf8')
|
|
246
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
247
|
+
.replace(/(^|\s)\/\/.*$/gm, '$1')
|
|
248
|
+
.replace(/,(\s*[}\]])/g, '$1');
|
|
249
|
+
const tsconfig = JSON.parse(raw);
|
|
250
|
+
tsconfig.compilerOptions ??= {};
|
|
251
|
+
Object.assign(tsconfig.compilerOptions, {
|
|
252
|
+
module: 'ESNext',
|
|
253
|
+
moduleResolution: 'bundler',
|
|
254
|
+
noEmit: true,
|
|
255
|
+
});
|
|
256
|
+
tsconfig.include = [...new Set([...(tsconfig.include ?? ['src']), 'vite.config.ts'])];
|
|
257
|
+
writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2) + '\n');
|
|
258
|
+
ctx.io.print(` tsconfig.json — module ESNext, moduleResolution bundler, noEmit`);
|
|
259
|
+
} catch {
|
|
260
|
+
ctx.io.warn(
|
|
261
|
+
'could not patch tsconfig.json — set module: ESNext, moduleResolution: bundler, noEmit: true manually',
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// esbuild drops the metadata decorators need only when emitDecoratorMetadata
|
|
266
|
+
// carries injections — the shim is still required before any decorated class.
|
|
267
|
+
const mainTs = join(srcDir, 'main.ts');
|
|
268
|
+
if (existsSync(mainTs)) {
|
|
269
|
+
const src = readFileSync(mainTs, 'utf8');
|
|
270
|
+
if (!src.includes('reflect-metadata')) {
|
|
271
|
+
writeFileSync(mainTs, `import 'reflect-metadata';\n${src}`);
|
|
272
|
+
ctx.io.print(` main.ts — prepended import 'reflect-metadata'`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
for (const leftover of ['nest-cli.json', 'webpack.config.js', 'webpack.config.cjs']) {
|
|
277
|
+
if (existsSync(join(root, leftover))) {
|
|
278
|
+
ctx.io.print(ctx.io.fmt.dim(` ${leftover} is no longer used — safe to delete`));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
await offerInstall(ctx, project, ['vite'], true);
|
|
283
|
+
|
|
284
|
+
ctx.io.print('');
|
|
285
|
+
ctx.io.print(ctx.io.fmt.strong('converted to a Vite SSR build:'));
|
|
286
|
+
ctx.io.print(
|
|
287
|
+
` ${ctx.io.fmt.accent('npm run dev')} — vite build --watch + node --watch dist/main.js`,
|
|
288
|
+
);
|
|
289
|
+
ctx.io.print(
|
|
290
|
+
` ${ctx.io.fmt.accent('npm run build')} → dist/ · ${ctx.io.fmt.accent('npm start')} — node dist/main.js`,
|
|
291
|
+
);
|
|
292
|
+
return 0;
|
|
293
|
+
}
|
|
294
|
+
|
|
134
295
|
/* ── atoll add [framework] <kind> [variant] [name] ───────────────────────── */
|
|
135
296
|
|
|
136
297
|
const ADD_KINDS = [
|
package/src/templates.ts
CHANGED
|
@@ -9,6 +9,9 @@ export const NPMRC = `# Supply-chain hardening — read by every npm command run
|
|
|
9
9
|
# lockfile (requires npm >= 11.10 — shipped with Node 26; older npm
|
|
10
10
|
# silently ignores this key).
|
|
11
11
|
min-release-age=7
|
|
12
|
+
# First-party packages must install even inside the age window (npm 11.10+;
|
|
13
|
+
# older npm silently ignores the key).
|
|
14
|
+
min-release-age-exclude[]=@atolljs/*
|
|
12
15
|
# Install lifecycle scripts are the dominant malware vector — none run
|
|
13
16
|
# during install/ci. If a native dep ever legitimately needs one:
|
|
14
17
|
# npm rebuild <pkg> --ignore-scripts=false
|
|
@@ -26,7 +29,7 @@ dist/
|
|
|
26
29
|
|
|
27
30
|
export function memoryContract(name: string): string {
|
|
28
31
|
const c = camel(name);
|
|
29
|
-
return `import { defineSharedMemory, field,
|
|
32
|
+
return `import { defineSharedMemory, field, reef } from '@atolljs/core';
|
|
30
33
|
|
|
31
34
|
/**
|
|
32
35
|
* ${c} shared memory — the single source of truth for state both threads
|
|
@@ -42,10 +45,10 @@ export const ${c}Memory = defineSharedMemory({
|
|
|
42
45
|
// Structured snapshots — one inline record, rewritten wholesale.
|
|
43
46
|
state: {
|
|
44
47
|
summary: field.object({
|
|
45
|
-
schema:
|
|
46
|
-
total:
|
|
47
|
-
errors:
|
|
48
|
-
updatedAt:
|
|
48
|
+
schema: reef.object({
|
|
49
|
+
total: reef.f64(),
|
|
50
|
+
errors: reef.f64(),
|
|
51
|
+
updatedAt: reef.u32(),
|
|
49
52
|
}),
|
|
50
53
|
}),
|
|
51
54
|
},
|
|
@@ -1705,8 +1708,10 @@ ${serviceImport}${memImport}
|
|
|
1705
1708
|
imports: [
|
|
1706
1709
|
AtollModule.registerPool({
|
|
1707
1710
|
name: '${name}',
|
|
1708
|
-
//
|
|
1709
|
-
|
|
1711
|
+
// the emitted worker bundle — keep the URL inline. Under a Vite SSR
|
|
1712
|
+
// build declare ${name}.worker.ts as a build input; on webpack point
|
|
1713
|
+
// this at the './${name}.worker.ts' source instead.
|
|
1714
|
+
worker: () => new Worker(new URL('./${name}.worker.js', import.meta.url)),
|
|
1710
1715
|
${memLine} poolSize: 'auto',
|
|
1711
1716
|
}),
|
|
1712
1717
|
],
|
|
@@ -1817,7 +1822,7 @@ export function nestjsHousedFiles(name: string, dir: string): OutFile[] {
|
|
|
1817
1822
|
// runs in. Message-only by default; to share another pool's buffer, wrap
|
|
1818
1823
|
// the factory:
|
|
1819
1824
|
// worker: withSharedBuffer(
|
|
1820
|
-
// () => new Worker(new URL('./${name}.worker.
|
|
1825
|
+
// () => new Worker(new URL('./${name}.worker.js', import.meta.url)),
|
|
1821
1826
|
// () => getAtollPool('<other-pool>')?.sharedBuffer,
|
|
1822
1827
|
// )
|
|
1823
1828
|
import { Module } from '@nestjs/common';
|
|
@@ -1828,7 +1833,10 @@ import { AtollModule } from '@atolljs/nestjs';
|
|
|
1828
1833
|
imports: [
|
|
1829
1834
|
AtollModule.registerPool({
|
|
1830
1835
|
name: '${name}',
|
|
1831
|
-
worker
|
|
1836
|
+
// the emitted worker bundle — keep the URL inline. Under a Vite SSR
|
|
1837
|
+
// build declare ${name}.worker.ts as a build input; on webpack point
|
|
1838
|
+
// this at the './${name}.worker.ts' source instead.
|
|
1839
|
+
worker: () => new Worker(new URL('./${name}.worker.js', import.meta.url)),
|
|
1832
1840
|
poolSize: 2,
|
|
1833
1841
|
}),
|
|
1834
1842
|
],
|
|
@@ -2226,3 +2234,77 @@ initDevtools({
|
|
|
2226
2234
|
initDevtools({ session: { name: '${name}' } });
|
|
2227
2235
|
`;
|
|
2228
2236
|
}
|
|
2237
|
+
|
|
2238
|
+
/* ── `atoll convert vite` (nestjs → Vite SSR build) ─────────────────────── */
|
|
2239
|
+
|
|
2240
|
+
/**
|
|
2241
|
+
* Consumer-facing vite.config.ts for a converted NestJS project — no repo
|
|
2242
|
+
* aliases (published @atolljs/* packages resolve from node_modules). Every
|
|
2243
|
+
* src/**\/*.worker.ts is an explicit rollup input; output is flat so a
|
|
2244
|
+
* './x.worker.js' literal resolves from dist/main.js or a shared chunk.
|
|
2245
|
+
* Uses readdirSync recursive (Node 20.1+) rather than globSync (Node 22+).
|
|
2246
|
+
*/
|
|
2247
|
+
export function nestjsViteConfig(): string {
|
|
2248
|
+
return `import { readdirSync } from 'node:fs';
|
|
2249
|
+
import { basename } from 'node:path';
|
|
2250
|
+
import { defineConfig } from 'vite';
|
|
2251
|
+
|
|
2252
|
+
// Every *.worker.ts under src/ is its own entry — pool configs reference the
|
|
2253
|
+
// emitted './x.worker.js' via new URL(..., import.meta.url). Keys are
|
|
2254
|
+
// basenames, so output is flat and the literal resolves from dist/main.js.
|
|
2255
|
+
const workerInputs = Object.fromEntries(
|
|
2256
|
+
readdirSync('src', { recursive: true, withFileTypes: true })
|
|
2257
|
+
.filter((e) => e.isFile() && e.name.endsWith('.worker.ts'))
|
|
2258
|
+
.map((e) => {
|
|
2259
|
+
const dir = (e.parentPath ?? e.path).replaceAll('\\\\', '/');
|
|
2260
|
+
return [basename(e.name, '.ts'), \`\${dir}/\${e.name}\`];
|
|
2261
|
+
}),
|
|
2262
|
+
);
|
|
2263
|
+
|
|
2264
|
+
export default defineConfig({
|
|
2265
|
+
build: {
|
|
2266
|
+
// Node SSR build: node builtins + node_modules stay external.
|
|
2267
|
+
ssr: true,
|
|
2268
|
+
outDir: 'dist',
|
|
2269
|
+
rollupOptions: {
|
|
2270
|
+
input: { main: 'src/main.ts', ...workerInputs },
|
|
2271
|
+
// Flat layout: a './x.worker.js' literal resolves to dist/<name>.worker.js
|
|
2272
|
+
// whether it lands in main.js or a shared chunk (the default assets/
|
|
2273
|
+
// subdir would put chunks one level too deep).
|
|
2274
|
+
output: { entryFileNames: '[name].js', chunkFileNames: '[name]-[hash].js' },
|
|
2275
|
+
},
|
|
2276
|
+
},
|
|
2277
|
+
});
|
|
2278
|
+
`;
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
/**
|
|
2282
|
+
* `node --watch` + vite watcher dev loop for the converted project — a
|
|
2283
|
+
* worker reload is a respawn anyway, so watching dist/ is enough. Uses
|
|
2284
|
+
* vite's programmatic build API: no npx/.cmd spawn (Node 20.12+ blocks .cmd
|
|
2285
|
+
* spawn without shell:true on Windows → EINVAL).
|
|
2286
|
+
*/
|
|
2287
|
+
export function devServerScript(): string {
|
|
2288
|
+
return `// Dev loop: vite's Rollup watcher rebuilds main + worker bundles on change
|
|
2289
|
+
// while \`node --watch\` restarts the app whenever dist/ updates — a worker
|
|
2290
|
+
// reload is a respawn anyway. Programmatic API: no npx/.cmd spawn (Node
|
|
2291
|
+
// 20.12+ blocks .cmd spawn without shell:true on Windows → EINVAL).
|
|
2292
|
+
import { spawn } from 'node:child_process';
|
|
2293
|
+
import { build } from 'vite';
|
|
2294
|
+
|
|
2295
|
+
await build(); // initial build — throws and exits on failure
|
|
2296
|
+
await build({ build: { watch: {} } }); // keeps the process alive
|
|
2297
|
+
|
|
2298
|
+
const child = spawn(process.execPath, ['--watch', 'dist/main.js'], {
|
|
2299
|
+
stdio: 'inherit',
|
|
2300
|
+
});
|
|
2301
|
+
|
|
2302
|
+
const shutdown = () => {
|
|
2303
|
+
child.kill();
|
|
2304
|
+
process.exit(0);
|
|
2305
|
+
};
|
|
2306
|
+
process.on('SIGINT', shutdown);
|
|
2307
|
+
process.on('SIGTERM', shutdown);
|
|
2308
|
+
process.on('exit', () => child.kill());
|
|
2309
|
+
`;
|
|
2310
|
+
}
|