@atolljs/cli 0.0.0-stage → 0.1.4

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.
@@ -0,0 +1,1632 @@
1
+ import { camel, pascal } from './parse.ts';
2
+ import type { Framework } from './project.ts';
3
+ import type { OutFile } from './files.ts';
4
+
5
+ /** The hardened .npmrc the CLI drops into every project it touches. */
6
+ export const NPMRC = `# Supply-chain hardening — read by every npm command run in this directory.
7
+ #
8
+ # Newly published versions must age 7 days before they can enter the
9
+ # lockfile (requires npm >= 11.10 — shipped with Node 26; older npm
10
+ # silently ignores this key).
11
+ min-release-age=7
12
+ # Install lifecycle scripts are the dominant malware vector — none run
13
+ # during install/ci. If a native dep ever legitimately needs one:
14
+ # npm rebuild <pkg> --ignore-scripts=false
15
+ ignore-scripts=true
16
+ # \`npm install <pkg>\` writes exact pins, not ^ ranges.
17
+ save-exact=true
18
+ fund=false
19
+ `;
20
+
21
+ export const GITIGNORE = `node_modules/
22
+ dist/
23
+ `;
24
+
25
+ /* ── shared-memory contract ─────────────────────────────────────────────── */
26
+
27
+ export function memoryContract(name: string): string {
28
+ const c = camel(name);
29
+ return `import { defineSharedMemory, field, mz } from '@atolljs/core';
30
+
31
+ /**
32
+ * ${c} shared memory — the single source of truth for state both threads
33
+ * touch. The spec compiles to one deterministic fixed-width layout; main
34
+ * thread and workers bind the same declaration, so the two sides can never
35
+ * disagree on byte offsets.
36
+ */
37
+ export const ${c}Memory = defineSharedMemory({
38
+ // Versioned scalars — observe()/watch() park on Atomics; zero postMessage.
39
+ signals: {
40
+ progress: field.number(),
41
+ },
42
+ // Structured snapshots — one inline record, rewritten wholesale.
43
+ state: {
44
+ summary: field.object({
45
+ schema: mz.object({
46
+ total: mz.f64(),
47
+ errors: mz.f64(),
48
+ updatedAt: mz.u32(),
49
+ }),
50
+ }),
51
+ },
52
+ });
53
+ export type ${pascal(name)}Memory = typeof ${c}Memory;
54
+ `;
55
+ }
56
+
57
+ /* ── worker pool (defineWorker + typed client) ──────────────────────────── */
58
+
59
+ export interface WorkerOpts {
60
+ /** 'browser' uses connectWorker + DOM Worker; 'node' uses createNodePool. */
61
+ host: 'browser' | 'node';
62
+ /** Import specifier for a shared-memory contract, or null. */
63
+ memoryImport?: string | null;
64
+ /** Memory export name (camelCase, e.g. \`appMemory\`). */
65
+ memoryName?: string;
66
+ /** URL for the bundled node worker, relative to the client file. */
67
+ distUrl?: string;
68
+ /** Emit writes to the generated contract's fields inside the echo task —
69
+ * only valid when memoryName refers to a CLI-generated contract. */
70
+ demoMemoryWrite?: boolean;
71
+ }
72
+
73
+ export function workerEntry(name: string, opts: WorkerOpts): string {
74
+ const c = camel(name);
75
+ const memImport = opts.memoryImport
76
+ ? `import { ${opts.memoryName} } from '${opts.memoryImport}';\n`
77
+ : '';
78
+ const memLine = opts.memoryName ? ` sharedMemory: ${opts.memoryName},\n` : '';
79
+ const shim = opts.host === 'node'
80
+ ? `// Node worker entry — the shim binds self = parentPort BEFORE\n// defineWorker's bootstrap evaluates; import order is the contract.\nimport '@atolljs/node/shim';\n`
81
+ : '';
82
+ return `${shim}import { defineWorker, serviceMethod } from '@atolljs/core';
83
+ import { z } from 'zod';
84
+ ${memImport}
85
+ const echo = serviceMethod({
86
+ def: { argsSchema: z.tuple([z.string()]), resultSchema: z.string() },
87
+ run: (message) => {${
88
+ opts.demoMemoryWrite && opts.memoryName
89
+ ? `
90
+ // Shared memory crosses the boundary without a message — the main
91
+ // thread can read these the moment the task resolves.
92
+ ${opts.memoryName}.signals.progress.write(100);
93
+ ${opts.memoryName}.state.summary.write({
94
+ total: 1,
95
+ errors: 0,
96
+ updatedAt: Math.floor(Date.now() / 1000),
97
+ });`
98
+ : ''
99
+ }
100
+ return \`worker heard: \${message}\`;
101
+ },
102
+ });
103
+
104
+ /**
105
+ * ${c} worker entry — defineWorker installs the message loop and registers
106
+ * every method. The main thread imports only the TYPE (typeof ${c}Worker)
107
+ * and drives calls through the pool client.
108
+ */
109
+ export const ${c}Worker = defineWorker({
110
+ ${memLine} methods: { echo },
111
+ });
112
+ export type ${pascal(name)}Worker = typeof ${c}Worker;
113
+ `;
114
+ }
115
+
116
+ export function workerClientFile(name: string, opts: WorkerOpts): string {
117
+ const c = camel(name);
118
+ const P = pascal(name);
119
+ const memImport = opts.memoryImport
120
+ ? `import { ${opts.memoryName} } from '${opts.memoryImport}';\n`
121
+ : '';
122
+ const memLine = opts.memoryName ? ` sharedMemory: ${opts.memoryName},\n` : '';
123
+ if (opts.host === 'node') {
124
+ return `import { Worker } from 'node:worker_threads';
125
+ import { workerClient, defineTask } from '@atolljs/core';
126
+ import { createNodePool } from '@atolljs/node';
127
+ ${memImport}import type { ${P}Worker } from './${name}.worker';
128
+
129
+ /**
130
+ * ${c} pool — node:worker_threads workers adapted to the pool surface.
131
+ * The worker entry must be bundled before spawning (Node workers run
132
+ * outside tsx's loader):
133
+ *
134
+ * esbuild src/atoll/${name}.worker.ts --bundle --platform=node \\
135
+ * --format=esm --packages=external --outfile=dist/${name}.worker.js
136
+ */
137
+ export const ${c}Pool = createNodePool({
138
+ worker: () => new Worker(new URL('${opts.distUrl ?? `./dist/${name}.worker.js`}', import.meta.url)),
139
+ ${memLine} poolSize: 'auto',
140
+ });
141
+
142
+ /** Typed client — calls read like the worker's own method names. */
143
+ export const ${c} = workerClient<${P}Worker>(${c}Pool);
144
+
145
+ /** Latest-wins task over a call — { data, pending, settled, elapsedMs }. */
146
+ export const ${c}EchoTask = defineTask((message: string) => ${c}.echo(message));
147
+ `;
148
+ }
149
+ return `import { connectWorker, defineTask } from '@atolljs/core';
150
+ ${memImport}import type { ${P}Worker } from './${name}.worker';
151
+
152
+ /**
153
+ * ${c} — typed client over the worker pool. Spawning is lazy, so importing
154
+ * this module never touches Worker (SSR-safe); the first method call (or
155
+ * ${c}.start()) brings the pool up. Keep the new Worker(new URL(...)) call
156
+ * inline — bundlers detect worker entries by that exact shape.
157
+ */
158
+ export const ${c} = connectWorker<${P}Worker>({
159
+ worker: () =>
160
+ new Worker(new URL('./${name}.worker.ts', import.meta.url), { type: 'module' }),
161
+ ${memLine} poolSize: 'auto',
162
+ });
163
+ export type ${P}Client = typeof ${c};
164
+
165
+ /** Latest-wins task over a call — { data, pending, settled, elapsedMs }. */
166
+ export const ${c}EchoTask = defineTask((message: string) => ${c}.echo(message));
167
+ `;
168
+ }
169
+
170
+ /* ── islands — worker entry per framework ───────────────────────────────── */
171
+
172
+ const ISLAND_EXT: Record<string, string> = {
173
+ react: 'tsx',
174
+ vue: 'ts',
175
+ solid: 'tsx',
176
+ svelte: 'ts',
177
+ angular: 'ts',
178
+ };
179
+
180
+ /** Worker-entry files for `add island`. Emits the component + worker. */
181
+ export function islandFiles(name: string, fw: Framework, dir = 'src/islands'): OutFile[] {
182
+ const P = pascal(name);
183
+ switch (fw) {
184
+ case 'react':
185
+ return [
186
+ {
187
+ path: `${dir}/${name}.worker.tsx`,
188
+ content: `import { useState } from 'react';
189
+ import { defineReactMonoWorker } from '@atolljs/react-island/worker';
190
+ import { emit } from '@atolljs/islands/worker';
191
+
192
+ /**
193
+ * ${P} — a real React component rendered INSIDE the worker. Mutations
194
+ * serialize to ops; the shell driver replays them into real DOM.
195
+ * Handlers receive the plain wire payload, not a SyntheticEvent.
196
+ */
197
+ function ${P}({ label = '${name}' }: { label?: string }) {
198
+ const [count, setCount] = useState(0);
199
+ return (
200
+ <div>
201
+ <span>
202
+ {label}: {count}
203
+ </span>
204
+ <button
205
+ onClick={() => {
206
+ const n = count + 1;
207
+ setCount(n);
208
+ emit('incremented', { count: n });
209
+ }}
210
+ >
211
+ increment
212
+ </button>
213
+ </div>
214
+ );
215
+ }
216
+
217
+ export const worker = defineReactMonoWorker(${P});
218
+ `,
219
+ },
220
+ ];
221
+ case 'vue':
222
+ return [
223
+ {
224
+ path: `${dir}/${P}.vue`,
225
+ content: `<script setup lang="ts">
226
+ import { ref } from 'vue';
227
+ import { emit } from '@atolljs/islands/worker';
228
+
229
+ // Rendered inside the worker — DOM ops stream to the shell; emit()
230
+ // sends app events back to it.
231
+ defineProps<{ label?: string }>();
232
+ const count = ref(0);
233
+ function increment() {
234
+ count.value += 1;
235
+ emit('incremented', { count: count.value });
236
+ }
237
+ </script>
238
+
239
+ <template>
240
+ <div>
241
+ <span>{{ label ?? '${name}' }}: {{ count }}</span>
242
+ <button @click="increment">increment</button>
243
+ </div>
244
+ </template>
245
+ `,
246
+ },
247
+ {
248
+ path: `${dir}/${name}.worker.ts`,
249
+ content: `import { defineVueMonoWorker } from '@atolljs/vue-island/worker';
250
+ import ${P} from './${P}.vue';
251
+
252
+ export const worker = defineVueMonoWorker(${P});
253
+ `,
254
+ },
255
+ ];
256
+ case 'solid':
257
+ return [
258
+ {
259
+ path: `${dir}/${name}.worker.tsx`,
260
+ content: `import { createSignal } from 'solid-js';
261
+ import { defineSolidMonoWorker } from '@atolljs/solid-island/worker';
262
+ import { emit } from '@atolljs/islands/worker';
263
+
264
+ /** ${P} — Solid rendering inside the worker; ops stream to the shell. */
265
+ function ${P}(props: { label?: string }) {
266
+ const [count, setCount] = createSignal(0);
267
+ return (
268
+ <div>
269
+ <span>
270
+ {props.label ?? '${name}'}: {count()}
271
+ </span>
272
+ <button
273
+ onClick={() => {
274
+ const n = count() + 1;
275
+ setCount(n);
276
+ emit('incremented', { count: n });
277
+ }}
278
+ >
279
+ increment
280
+ </button>
281
+ </div>
282
+ );
283
+ }
284
+
285
+ export const worker = defineSolidMonoWorker(${P});
286
+ `,
287
+ },
288
+ ];
289
+ case 'svelte':
290
+ return [
291
+ {
292
+ path: `${dir}/${P}.svelte`,
293
+ content: `<script lang="ts">
294
+ import { emit } from '@atolljs/islands/worker';
295
+
296
+ // Rendered inside the worker — DOM ops stream to the shell.
297
+ let { label = '${name}' }: { label?: string } = $props();
298
+ let count = $state(0);
299
+ function increment() {
300
+ count += 1;
301
+ emit('incremented', { count });
302
+ }
303
+ </script>
304
+
305
+ <div>
306
+ <span>{label}: {count}</span>
307
+ <button onclick={increment}>increment</button>
308
+ </div>
309
+ `,
310
+ },
311
+ {
312
+ path: `${dir}/${name}.worker.ts`,
313
+ content: `import { defineSvelteMonoWorker } from '@atolljs/svelte-island/worker';
314
+ import ${P} from './${P}.svelte';
315
+
316
+ export const worker = defineSvelteMonoWorker(${P});
317
+ `,
318
+ },
319
+ ];
320
+ case 'angular':
321
+ return [
322
+ {
323
+ path: `${dir}/${name}.worker.ts`,
324
+ content: `import { Component } from '@angular/core';
325
+ import { defineMonoWorker } from '@atolljs/islands/worker';
326
+ import { angularIslandApp } from '@atolljs/angular-island/worker';
327
+
328
+ /**
329
+ * ${P}Component — rendered inside the worker by Angular's own engine;
330
+ * ops stream to the shell driver on the main thread.
331
+ */
332
+ @Component({
333
+ template: \`
334
+ <div>
335
+ <span>{{ label }}: {{ count }}</span>
336
+ <button (click)="increment()">increment</button>
337
+ </div>
338
+ \`,
339
+ })
340
+ export class ${P}Component {
341
+ label = '${name}';
342
+ count = 0;
343
+ increment() {
344
+ this.count += 1;
345
+ }
346
+ }
347
+
348
+ export const worker = defineMonoWorker(angularIslandApp(${P}Component));
349
+ `,
350
+ },
351
+ ];
352
+ default:
353
+ return [];
354
+ }
355
+ }
356
+
357
+ /** Shell-side usage hint printed after `add island` — one per framework. */
358
+ export function islandUsage(name: string, fw: Framework): string {
359
+ const P = pascal(name);
360
+ const ext = ISLAND_EXT[fw] ?? 'ts';
361
+ const factory = `() => new Worker(new URL('./islands/${name}.worker.${ext}', import.meta.url), { type: 'module' })`;
362
+ switch (fw) {
363
+ case 'react':
364
+ return `// mount it in your shell — worker factory stays inline so the bundler sees it
365
+ import { Island } from '@atolljs/react-island';
366
+
367
+ <Island app="${name}" worker={${factory}} onEvent={(n, p) => console.log(n, p)} />`;
368
+ case 'vue':
369
+ return `<script setup>
370
+ import { AtollIsland } from '@atolljs/vue-island';
371
+ const worker = ${factory};
372
+ </script>
373
+
374
+ <template>
375
+ <AtollIsland app="${name}" :worker="worker" />
376
+ </template>`;
377
+ case 'solid':
378
+ return `import { Island } from '@atolljs/solid-island';
379
+
380
+ <Island app="${name}" worker={${factory}} />`;
381
+ case 'svelte':
382
+ return `<script lang="ts">
383
+ import { island } from '@atolljs/svelte-island';
384
+ const worker = ${factory};
385
+ </script>
386
+
387
+ <div use:island={{ app: '${name}', worker }} />`;
388
+ case 'angular':
389
+ return `import { islandComponent } from '@atolljs/angular-island';
390
+ import type { ${P}Component } from './islands/${name}.worker';
391
+
392
+ export const ${P}Island = islandComponent<${P}Component>({
393
+ app: '${name}',
394
+ worker: ${factory},
395
+ });
396
+ // then use the generated component in a template — see docs/consumer Angular islands`;
397
+ default:
398
+ return '';
399
+ }
400
+ }
401
+
402
+ /* ── `atoll new` — full app scaffolds ───────────────────────────────────── */
403
+
404
+ const HEADERS = ` // SharedArrayBuffer is gated behind cross-origin isolation — these
405
+ // headers unlock it; islands/pools degrade gracefully without them.
406
+ server: {
407
+ headers: {
408
+ 'Cross-Origin-Opener-Policy': 'same-origin',
409
+ 'Cross-Origin-Embedder-Policy': 'require-corp',
410
+ },
411
+ },`;
412
+
413
+ interface ScaffoldSpec {
414
+ deps: Record<string, string>;
415
+ devDeps: Record<string, string>;
416
+ vitePlugin?: { pkg: string; import_: string; call: string };
417
+ }
418
+
419
+ const SCAFFOLDS: Record<string, ScaffoldSpec> = {
420
+ react: {
421
+ deps: { react: '^19.3.0', 'react-dom': '^19.3.0', 'react-reconciler': '^0.34.0', '@atolljs/core': '^0.1.0', '@atolljs/react': '^0.1.0', '@atolljs/react-island': '^0.1.0', zod: '^4.6.5' },
422
+ devDeps: { vite: '^8.3.1', typescript: '^7.0.2', '@vitejs/plugin-react': '^6.1.1', '@types/react': '^19.3.0', '@types/react-dom': '^19.3.0' },
423
+ vitePlugin: { pkg: '@vitejs/plugin-react', import_: `import react from '@vitejs/plugin-react';`, call: 'react()' },
424
+ },
425
+ vue: {
426
+ deps: { vue: '^3.5.43', '@atolljs/core': '^0.1.0', '@atolljs/vue': '^0.1.0', '@atolljs/vue-island': '^0.1.0', zod: '^4.6.5' },
427
+ devDeps: { vite: '^8.3.1', typescript: '^7.0.2', '@vitejs/plugin-vue': '^6.0.9' },
428
+ vitePlugin: { pkg: '@vitejs/plugin-vue', import_: `import vue from '@vitejs/plugin-vue';`, call: 'vue()' },
429
+ },
430
+ solid: {
431
+ deps: { 'solid-js': '^1.9.15', '@atolljs/core': '^0.1.0', '@atolljs/solidjs': '^0.1.0', '@atolljs/solid-island': '^0.1.0', zod: '^4.6.5' },
432
+ devDeps: { vite: '^8.3.1', typescript: '^7.0.2', 'vite-plugin-solid': '^2.11.14' },
433
+ vitePlugin: { pkg: 'vite-plugin-solid', import_: `import solid from 'vite-plugin-solid';`, call: 'solid()' },
434
+ },
435
+ svelte: {
436
+ deps: { svelte: '^5.57.1', '@atolljs/core': '^0.1.0', '@atolljs/svelte': '^0.1.0', '@atolljs/svelte-island': '^0.1.0', zod: '^4.6.5' },
437
+ devDeps: { vite: '^8.3.1', typescript: '^7.0.2', '@sveltejs/vite-plugin-svelte': '^7.3.1' },
438
+ vitePlugin: { pkg: '@sveltejs/vite-plugin-svelte', import_: `import { svelte } from '@sveltejs/vite-plugin-svelte';`, call: 'svelte()' },
439
+ },
440
+ };
441
+
442
+ export const SCAFFOLD_FRAMEWORKS = Object.keys(SCAFFOLDS) as readonly string[];
443
+
444
+ function pkgJson(appName: string, spec: ScaffoldSpec): string {
445
+ return (
446
+ JSON.stringify(
447
+ {
448
+ name: appName,
449
+ private: true,
450
+ type: 'module',
451
+ scripts: { dev: 'vite', build: 'vite build', preview: 'vite preview' },
452
+ dependencies: spec.deps,
453
+ devDependencies: spec.devDeps,
454
+ },
455
+ null,
456
+ 2,
457
+ ) + '\n'
458
+ );
459
+ }
460
+
461
+ const TSCONFIGS: Record<string, object> = {
462
+ react: {
463
+ compilerOptions: {
464
+ target: 'ES2022', module: 'ESNext', moduleResolution: 'bundler',
465
+ lib: ['ESNext', 'DOM', 'DOM.Iterable'], jsx: 'react-jsx',
466
+ strict: true, noEmit: true, isolatedModules: true, skipLibCheck: true,
467
+ allowImportingTsExtensions: true,
468
+ },
469
+ include: ['src'],
470
+ },
471
+ vue: {
472
+ compilerOptions: {
473
+ target: 'ES2022', module: 'ESNext', moduleResolution: 'bundler',
474
+ lib: ['ESNext', 'DOM', 'DOM.Iterable'],
475
+ strict: true, noEmit: true, isolatedModules: true, skipLibCheck: true,
476
+ allowImportingTsExtensions: true,
477
+ },
478
+ include: ['src'],
479
+ },
480
+ solid: {
481
+ compilerOptions: {
482
+ target: 'ES2022', module: 'ESNext', moduleResolution: 'bundler',
483
+ lib: ['ESNext', 'DOM', 'DOM.Iterable'], jsx: 'preserve', jsxImportSource: 'solid-js',
484
+ strict: true, noEmit: true, isolatedModules: true, skipLibCheck: true,
485
+ allowImportingTsExtensions: true,
486
+ },
487
+ include: ['src'],
488
+ },
489
+ svelte: {
490
+ compilerOptions: {
491
+ target: 'ES2022', module: 'ESNext', moduleResolution: 'bundler',
492
+ lib: ['ESNext', 'DOM', 'DOM.Iterable'],
493
+ strict: true, noEmit: true, isolatedModules: true, skipLibCheck: true,
494
+ allowImportingTsExtensions: true, types: ['vite/client'],
495
+ },
496
+ include: ['src'],
497
+ },
498
+ node: {
499
+ compilerOptions: {
500
+ target: 'ES2022', module: 'ESNext', moduleResolution: 'bundler',
501
+ // DOM+WebWorker libs — @atolljs/node types pool workers against the
502
+ // DOM Worker surface; without them its shipped source won't typecheck.
503
+ lib: ['ESNext', 'DOM', 'WebWorker'], types: ['node'],
504
+ strict: true, noEmit: true, isolatedModules: true, skipLibCheck: true,
505
+ allowImportingTsExtensions: true,
506
+ },
507
+ include: ['src'],
508
+ },
509
+ };
510
+
511
+ function viteConfig(fw: string): string {
512
+ const spec = SCAFFOLDS[fw];
513
+ const plugin = spec.vitePlugin;
514
+ return `import { defineConfig } from 'vite';
515
+ ${plugin?.import_ ?? ''}
516
+
517
+ export default defineConfig({
518
+ plugins: [${plugin?.call ?? ''}],
519
+ ${
520
+ plugin
521
+ ? ` // Worker bundles run their own rolldown pass — fresh plugin instances
522
+ // let the worker entry compile framework files too (worker-island apps).
523
+ worker: { plugins: () => [${plugin.call}] },
524
+ `
525
+ : ''
526
+ }${HEADERS}
527
+ });
528
+ `;
529
+ }
530
+
531
+ const INDEX_HTML = (title: string, entry: string) => `<!doctype html>
532
+ <html lang="en">
533
+ <head>
534
+ <meta charset="UTF-8" />
535
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
536
+ <title>${title}</title>
537
+ </head>
538
+ <body>
539
+ <div id="root"></div>
540
+ <script type="module" src="/${entry}"></script>
541
+ </body>
542
+ </html>
543
+ `;
544
+
545
+ /** All files for `atoll new <dir> --framework <fw>` (browser vite apps). */
546
+ export function scaffoldFiles(appName: string, fw: Framework): OutFile[] {
547
+ const spec = SCAFFOLDS[fw];
548
+ if (!spec) return [];
549
+ const files: OutFile[] = [
550
+ { path: 'package.json', content: pkgJson(appName, spec) },
551
+ { path: 'tsconfig.json', content: JSON.stringify(TSCONFIGS[fw], null, 2) + '\n' },
552
+ { path: 'vite.config.ts', content: viteConfig(fw) },
553
+ { path: '.npmrc', content: NPMRC },
554
+ { path: '.gitignore', content: GITIGNORE },
555
+ { path: 'index.html', content: INDEX_HTML(appName, `src/main.${ISLAND_EXT[fw]}`) },
556
+ ];
557
+ const counter = islandFiles('counter', fw);
558
+ switch (fw) {
559
+ case 'react':
560
+ files.push(
561
+ ...counter,
562
+ {
563
+ path: 'src/main.tsx',
564
+ content: `import { createRoot } from 'react-dom/client';
565
+ import { App } from './App';
566
+
567
+ createRoot(document.getElementById('root')!).render(<App />);
568
+ `,
569
+ },
570
+ {
571
+ path: 'src/App.tsx',
572
+ content: `import { Island } from '@atolljs/react-island';
573
+
574
+ /**
575
+ * The counter island renders inside a worker — the main thread only replays
576
+ * its DOM ops. New islands: \`npx atoll add island <name>\`.
577
+ */
578
+ export function App() {
579
+ return (
580
+ <main style={{ fontFamily: 'system-ui', padding: '2rem' }}>
581
+ <h1>${appName}</h1>
582
+ <p>This counter renders in a worker thread.</p>
583
+ <Island
584
+ app="counter"
585
+ worker={() =>
586
+ new Worker(new URL('./islands/counter.worker.tsx', import.meta.url), { type: 'module' })
587
+ }
588
+ onEvent={(name, payload) => console.log('island event:', name, payload)}
589
+ />
590
+ </main>
591
+ );
592
+ }
593
+ `,
594
+ },
595
+ );
596
+ break;
597
+ case 'vue':
598
+ files.push(
599
+ ...counter,
600
+ { path: 'src/env.d.ts', content: `declare module '*.vue';\n` },
601
+ {
602
+ path: 'src/main.ts',
603
+ content: `import { createApp } from 'vue';
604
+ import App from './App.vue';
605
+
606
+ createApp(App).mount('#root');
607
+ `,
608
+ },
609
+ {
610
+ path: 'src/App.vue',
611
+ content: `<script setup lang="ts">
612
+ import { AtollIsland } from '@atolljs/vue-island';
613
+
614
+ // The counter island renders inside a worker — the main thread replays ops.
615
+ const worker = () =>
616
+ new Worker(new URL('./islands/counter.worker.ts', import.meta.url), { type: 'module' });
617
+ </script>
618
+
619
+ <template>
620
+ <main style="font-family: system-ui; padding: 2rem">
621
+ <h1>${appName}</h1>
622
+ <p>This counter renders in a worker thread.</p>
623
+ <AtollIsland app="counter" :worker="worker" />
624
+ </main>
625
+ </template>
626
+ `,
627
+ },
628
+ );
629
+ break;
630
+ case 'solid':
631
+ files.push(
632
+ ...counter,
633
+ {
634
+ path: 'src/main.tsx',
635
+ content: `import { render } from 'solid-js/web';
636
+ import { App } from './App';
637
+
638
+ render(() => <App />, document.getElementById('root')!);
639
+ `,
640
+ },
641
+ {
642
+ path: 'src/App.tsx',
643
+ content: `import { Island } from '@atolljs/solid-island';
644
+
645
+ /** The counter island renders inside a worker — the main thread replays ops. */
646
+ export function App() {
647
+ return (
648
+ <main style={{ 'font-family': 'system-ui', padding: '2rem' }}>
649
+ <h1>${appName}</h1>
650
+ <p>This counter renders in a worker thread.</p>
651
+ <Island
652
+ app="counter"
653
+ worker={() =>
654
+ new Worker(new URL('./islands/counter.worker.tsx', import.meta.url), { type: 'module' })
655
+ }
656
+ />
657
+ </main>
658
+ );
659
+ }
660
+ `,
661
+ },
662
+ );
663
+ break;
664
+ case 'svelte':
665
+ files.push(
666
+ ...counter,
667
+ {
668
+ path: 'src/main.ts',
669
+ content: `import { mount } from 'svelte';
670
+ import App from './App.svelte';
671
+
672
+ mount(App, { target: document.getElementById('root')! });
673
+ `,
674
+ },
675
+ {
676
+ path: 'src/App.svelte',
677
+ content: `<script lang="ts">
678
+ import { island } from '@atolljs/svelte-island';
679
+
680
+ // The counter island renders inside a worker — the main thread replays ops.
681
+ const worker = () =>
682
+ new Worker(new URL('./islands/counter.worker.ts', import.meta.url), { type: 'module' });
683
+ </script>
684
+
685
+ <main style="font-family: system-ui; padding: 2rem">
686
+ <h1>${appName}</h1>
687
+ <p>This counter renders in a worker thread.</p>
688
+ <div use:island={{ app: 'counter', worker }} />
689
+ </main>
690
+ `,
691
+ },
692
+ );
693
+ break;
694
+ }
695
+ return files;
696
+ }
697
+
698
+ /** `atoll new --framework node` — a tsx service with a worker pool. */
699
+ export function nodeScaffoldFiles(appName: string): OutFile[] {
700
+ return [
701
+ {
702
+ path: 'package.json',
703
+ content:
704
+ JSON.stringify(
705
+ {
706
+ name: appName,
707
+ private: true,
708
+ type: 'module',
709
+ scripts: {
710
+ dev: 'tsx watch src/main.ts',
711
+ start: 'tsx src/main.ts',
712
+ bundle:
713
+ 'esbuild src/atoll/tasks.worker.ts --bundle --platform=node --format=esm --packages=external --outfile=dist/tasks.worker.js',
714
+ },
715
+ dependencies: { '@atolljs/core': '^0.1.0', '@atolljs/node': '^0.1.0', zod: '^4.6.5' },
716
+ devDependencies: { esbuild: '^0.28.2', tsx: '^4.23.15', typescript: '^7.0.2', '@types/node': '^26.6.3' },
717
+ },
718
+ null,
719
+ 2,
720
+ ) + '\n',
721
+ },
722
+ { path: 'tsconfig.json', content: JSON.stringify(TSCONFIGS.node, null, 2) + '\n' },
723
+ { path: '.npmrc', content: NPMRC },
724
+ { path: '.gitignore', content: GITIGNORE },
725
+ {
726
+ path: 'src/atoll/tasks.memory.ts',
727
+ content: memoryContract('tasks'),
728
+ },
729
+ {
730
+ path: 'src/atoll/tasks.worker.ts',
731
+ content: workerEntry('tasks', {
732
+ host: 'node',
733
+ memoryImport: './tasks.memory',
734
+ memoryName: 'tasksMemory',
735
+ demoMemoryWrite: true,
736
+ }),
737
+ },
738
+ {
739
+ path: 'src/atoll/tasks.ts',
740
+ content: workerClientFile('tasks', {
741
+ host: 'node',
742
+ memoryImport: './tasks.memory',
743
+ memoryName: 'tasksMemory',
744
+ distUrl: '../../dist/tasks.worker.js',
745
+ }),
746
+ },
747
+ {
748
+ path: 'src/main.ts',
749
+ content: `import { tasks, tasksPool } from './atoll/tasks';
750
+ import { tasksMemory } from './atoll/tasks.memory';
751
+
752
+ /**
753
+ * ${appName} — a worker pool behind a typed client. The echo task runs off
754
+ * the main thread; shared memory is readable here with zero dispatch.
755
+ */
756
+ const heard = await tasks.echo('hello from main');
757
+ console.log(heard);
758
+ console.log('summary (shared memory):', tasksMemory.state.summary.read());
759
+
760
+ await tasksPool.close();
761
+ `,
762
+ },
763
+ ];
764
+ }
765
+
766
+ /* ── framework bindings over a pool client (`add <fw> worker <name>`) ────── */
767
+
768
+ interface MemRef {
769
+ /** Import specifier relative to the generated file. */
770
+ import_: string;
771
+ /** Contract export name, e.g. `appMemory`. */
772
+ export_: string;
773
+ }
774
+
775
+ const UI_BINDINGS: readonly Framework[] = [
776
+ 'react',
777
+ 'nextjs',
778
+ 'vue',
779
+ 'solid',
780
+ 'svelte',
781
+ 'angular',
782
+ ];
783
+
784
+ export const hasWorkerBindings = (fw: Framework): boolean => UI_BINDINGS.includes(fw);
785
+
786
+ /**
787
+ * The framework-facing half of `add worker` — a hooks/signals adapter module
788
+ * beside the pool client, so components never touch the client directly.
789
+ * Returns null for frameworks without a binding idiom (node, vanilla…).
790
+ */
791
+ export function workerBindingsFile(
792
+ name: string,
793
+ fw: Framework,
794
+ dir: string,
795
+ mem: MemRef | null,
796
+ ): OutFile | null {
797
+ const c = camel(name);
798
+ const P = pascal(name);
799
+ const memImport = mem ? `import { ${mem.export_} } from '${mem.import_}';\n` : '';
800
+ // 'signals.progress' exists on CLI-generated contracts — emitted as a
801
+ // commented example so a hand-authored contract never breaks the build.
802
+ const watchLine = (hook: string, decl = 'const') =>
803
+ mem ? `\n // ${decl} progress = ${hook}(${mem.export_}, 'signals.progress');` : '';
804
+ switch (fw) {
805
+ case 'react':
806
+ return {
807
+ path: `${dir}/use${P}.ts`,
808
+ content: `import { useTask${mem ? ', useSharedValue' : ''} } from '@atolljs/react';
809
+ import { ${c} } from './${name}';
810
+ ${memImport}
811
+ /**
812
+ * React bindings for the ${name} pool — call inside components. useTask
813
+ * wraps a call in { data, pending, settled, elapsedMs, error } with
814
+ * latest-wins run()/runOnce(); useSharedValue parks on Atomics.
815
+ */
816
+ export function use${P}() {
817
+ const echo = useTask(${c}.echo);${watchLine('useSharedValue')}
818
+ return { echo };
819
+ }
820
+ `,
821
+ };
822
+ case 'nextjs':
823
+ return {
824
+ path: `${dir}/${name}.tsx`,
825
+ content: `'use client';
826
+ import { useTask${mem ? ', useSharedValue' : ''} } from '@atolljs/nextjs';
827
+ import { ${c} } from './${name}';
828
+ ${memImport}
829
+ /**
830
+ * Client component over the ${name} pool — 'use client' keeps the hooks
831
+ * browser-side; SSR-safe because the pool spawns lazily on first call.
832
+ */
833
+ export function ${P}() {
834
+ const echo = useTask(${c}.echo);${watchLine('useSharedValue')}
835
+ return (
836
+ <button onClick={() => echo.run('ping')}>
837
+ {echo.data ?? (echo.pending ? 'working\\u2026' : '${name}')}
838
+ </button>
839
+ );
840
+ }
841
+ `,
842
+ };
843
+ case 'vue':
844
+ return {
845
+ path: `${dir}/use${P}.ts`,
846
+ content: `import { useTask${mem ? ', useSharedValue' : ''} } from '@atolljs/vue';
847
+ import { ${c} } from './${name}';
848
+ ${memImport}
849
+ /** Vue composable over the ${name} pool — refs, ready for <script setup>. */
850
+ export function use${P}() {
851
+ const echo = useTask(${c}.echo);${watchLine('useSharedValue')}
852
+ return { echo };
853
+ }
854
+ `,
855
+ };
856
+ case 'solid':
857
+ return {
858
+ path: `${dir}/use${P}.ts`,
859
+ content: `import { createTask${mem ? ', createSharedValue' : ''} } from '@atolljs/solidjs';
860
+ import { ${c} } from './${name}';
861
+ ${memImport}
862
+ /** Solid bindings over the ${name} pool — accessors, call inside setup. */
863
+ export function create${P}() {
864
+ const echo = createTask(${c}.echo);${watchLine('createSharedValue')}
865
+ return { echo };
866
+ }
867
+ `,
868
+ };
869
+ case 'svelte':
870
+ // *.svelte.ts — runes ($effect inside taskState/sharedValue) only
871
+ // compile in svelte-module files.
872
+ return {
873
+ path: `${dir}/${name}.svelte.ts`,
874
+ content: `import { taskState${mem ? ', sharedValue' : ''} } from '@atolljs/svelte';
875
+ import { ${c} } from './${name}';
876
+ ${memImport}
877
+ /** Svelte bindings over the ${name} pool — call inside components. */
878
+ export function ${c}State() {
879
+ const echo = taskState(${c}.echo);${watchLine('sharedValue')}
880
+ return { echo };
881
+ }
882
+ `,
883
+ };
884
+ case 'angular':
885
+ return {
886
+ path: `${dir}/${name}.facade.ts`,
887
+ content: `import { Injectable } from '@angular/core';
888
+ import { taskState${mem ? ', sharedValue' : ''} } from '@atolljs/angular';
889
+ import { ${c} } from './${name}';
890
+ ${memImport}
891
+ /**
892
+ * Angular facade over the ${name} pool — field initializers run in the
893
+ * injection context, so the signal subscription unbinds on destroy.
894
+ */
895
+ @Injectable({ providedIn: 'root' })
896
+ export class ${P}Facade {
897
+ readonly echo = taskState(${c}.echo);${watchLine('sharedValue', 'readonly')}
898
+ readonly run = (message: string) => this.echo.run(message);
899
+ }
900
+ `,
901
+ };
902
+ default:
903
+ return null;
904
+ }
905
+ }
906
+
907
+ /* ── islands — facade variant: contract module + lazy import ────────────── */
908
+
909
+ /**
910
+ * `add island <name> facade` — the contract-module pattern: a shell-safe
911
+ * `${name}.island.ts` exports { app, worker }; the lazy facade imports it,
912
+ * so the worker entry (and the app's dependency graph) only loads on mount.
913
+ */
914
+ export function islandFacadeFiles(name: string, fw: Framework, dir = 'src/islands'): OutFile[] {
915
+ const P = pascal(name);
916
+ const ext = ISLAND_EXT[fw] ?? 'ts';
917
+ const contract = (appLine: string): string => `/**
918
+ * ${name} island contract — shell-safe: the app reference + the worker
919
+ * factory. Never import ./${name}.worker.${ext} here — definePolyWorker
920
+ * installs worker-side globals on evaluation.
921
+ */
922
+ ${appLine}
923
+ export const worker = () =>
924
+ new Worker(new URL('./${name}.worker.${ext}', import.meta.url), { type: 'module' });
925
+ `;
926
+ switch (fw) {
927
+ case 'react':
928
+ case 'solid': {
929
+ const isReact = fw === 'react';
930
+ const sig = isReact ? 'useState' : 'createSignal';
931
+ const poly = isReact ? 'defineReactPolyWorker' : 'defineSolidPolyWorker';
932
+ const pkg = `@atolljs/${fw}-island/worker`;
933
+ return [
934
+ {
935
+ path: `${dir}/${name}.app.tsx`,
936
+ content: `import { ${sig} } from '${isReact ? 'react' : 'solid-js'}';
937
+ import { islandApp } from '@atolljs/islands/worker';
938
+ import { emit } from '${pkg}';
939
+
940
+ /**
941
+ * ${P}App — a real ${isReact ? 'React' : 'Solid'} component rendered INSIDE
942
+ * the worker. islandApp stamps the registry key (minification-proof); state
943
+ * never leaves the worker, DOM ops stream to the shell driver.
944
+ */
945
+ export const ${P}App = islandApp('${name}', function ${P}(${
946
+ isReact ? `{ label = '${name}' }: { label?: string }` : `props: { label?: string }`
947
+ }) {
948
+ ${isReact ? 'const [count, setCount] = useState(0);' : 'const [count, setCount] = createSignal(0);'}
949
+ return (
950
+ <button
951
+ onClick={() => {
952
+ const n = ${isReact ? 'count' : 'count()'} + 1;
953
+ setCount(n);
954
+ emit('incremented', { count: n });
955
+ }}
956
+ >
957
+ ${isReact ? `{label}: {count}` : `{props.label ?? '${name}'}: {count()}`}
958
+ </button>
959
+ );
960
+ });
961
+ `,
962
+ },
963
+ {
964
+ path: `${dir}/${name}.worker.${ext}`,
965
+ content: `import { ${poly} } from '${pkg}';
966
+ import { ${P}App } from './${name}.app';
967
+
968
+ /** One worker, many islands — add apps to the registry. */
969
+ export const worker = ${poly}({ apps: { ${name}: ${P}App } });
970
+ `,
971
+ },
972
+ {
973
+ path: `${dir}/${name}.island.ts`,
974
+ content: contract(`export { ${P}App as app } from './${name}.app';`),
975
+ },
976
+ ];
977
+ }
978
+ case 'vue': {
979
+ const [component] = islandFiles(name, 'vue', dir); // reuse the SFC
980
+ return [
981
+ component,
982
+ {
983
+ path: `${dir}/${name}.worker.ts`,
984
+ content: `import { defineVuePolyWorker } from '@atolljs/vue-island/worker';
985
+ import ${P} from './${P}.vue';
986
+
987
+ /** One worker, many islands — add apps to the registry. */
988
+ export const worker = defineVuePolyWorker({ apps: { ${name}: ${P} } });
989
+ `,
990
+ },
991
+ {
992
+ path: `${dir}/${name}.island.ts`,
993
+ content: contract(`export const app = '${name}';`),
994
+ },
995
+ ];
996
+ }
997
+ case 'svelte': {
998
+ const [component] = islandFiles(name, 'svelte', dir); // reuse the SFC
999
+ return [
1000
+ component,
1001
+ {
1002
+ path: `${dir}/${name}.worker.ts`,
1003
+ content: `import { defineSveltePolyWorker } from '@atolljs/svelte-island/worker';
1004
+ import ${P} from './${P}.svelte';
1005
+
1006
+ /** One worker, many islands — add apps to the registry. */
1007
+ export const worker = defineSveltePolyWorker({ apps: { ${name}: ${P} } });
1008
+ `,
1009
+ },
1010
+ {
1011
+ path: `${dir}/${name}.island.ts`,
1012
+ content: contract(`export const app = '${name}';`),
1013
+ },
1014
+ {
1015
+ path: `${dir}/${P}Island.svelte`,
1016
+ content: `<script lang="ts">
1017
+ import { island } from '@atolljs/svelte-island';
1018
+ import { app, worker } from './${name}.island';
1019
+
1020
+ // The facade component — attrs serialize to worker props on mount.
1021
+ let { label = '${name}' }: { label?: string } = $props();
1022
+ </script>
1023
+
1024
+ <div use:island={{ app, worker, props: { label } }} />
1025
+ `,
1026
+ },
1027
+ ];
1028
+ }
1029
+ case 'angular':
1030
+ return [
1031
+ {
1032
+ path: `${dir}/${name}.component.ts`,
1033
+ content: `import { Component, input, output } from '@angular/core';
1034
+ import { AngularIsland } from '@atolljs/angular-island/worker';
1035
+
1036
+ /**
1037
+ * ${P}Component — rendered inside the worker by Angular's own engine.
1038
+ * @AngularIsland stamps the registry key and registers the component, so
1039
+ * defineAngularPolyWorker() collects it with no hand-kept apps map.
1040
+ */
1041
+ @AngularIsland('${name}')
1042
+ @Component({
1043
+ standalone: true,
1044
+ template: \`
1045
+ <button (click)="increment()">{{ label() }}: {{ count }}</button>
1046
+ \`,
1047
+ })
1048
+ export class ${P}Component {
1049
+ label = input('${name}');
1050
+ /** Root output() fields bridge to the shell's island event channel. */
1051
+ incremented = output<{ count: number }>();
1052
+ count = 0;
1053
+ increment() {
1054
+ this.count += 1;
1055
+ this.incremented.emit({ count: this.count });
1056
+ }
1057
+ }
1058
+ `,
1059
+ },
1060
+ {
1061
+ path: `${dir}/${name}.worker.ts`,
1062
+ content: `import { defineAngularPolyWorker } from '@atolljs/angular-island/worker';
1063
+ import './${name}.component'; // registers via @AngularIsland
1064
+
1065
+ /** One worker, many islands — decorated components self-register. */
1066
+ export const worker = defineAngularPolyWorker();
1067
+ `,
1068
+ },
1069
+ {
1070
+ path: `${dir}/${name}.island.ts`,
1071
+ content: `import { islandComponent } from '@atolljs/angular-island';
1072
+ import type { ${P}Component } from './${name}.component';
1073
+
1074
+ /**
1075
+ * ${name} island facade — a standalone component typed off the worker
1076
+ * component's input()/output() contract. Use <${name}-island> in templates.
1077
+ */
1078
+ export const ${P}Island = islandComponent<${P}Component>({
1079
+ app: '${name}',
1080
+ selector: '${name}-island',
1081
+ worker: () =>
1082
+ new Worker(new URL('./${name}.worker.ts', import.meta.url), { type: 'module' }),
1083
+ });
1084
+ `,
1085
+ },
1086
+ ];
1087
+ default:
1088
+ return [];
1089
+ }
1090
+ }
1091
+
1092
+ /** Shell-side usage hint for the facade variant. */
1093
+ export function islandFacadeUsage(name: string, fw: Framework): string {
1094
+ const P = pascal(name);
1095
+ switch (fw) {
1096
+ case 'react':
1097
+ return `import { Suspense } from 'react';
1098
+ import { lazyIsland } from '@atolljs/react-island';
1099
+
1100
+ const ${P}Island = lazyIsland(() => import('./islands/${name}.island'));
1101
+
1102
+ <Suspense fallback={null}>
1103
+ <${P}Island label="${name}" onEvent={(n, p) => console.log(n, p)} />
1104
+ </Suspense>`;
1105
+ case 'vue':
1106
+ return `<script setup>
1107
+ import { lazyIsland } from '@atolljs/vue-island';
1108
+ const ${P}Island = lazyIsland(() => import('./islands/${name}.island'));
1109
+ </script>
1110
+
1111
+ <template>
1112
+ <${P}Island label="${name}" />
1113
+ </template>`;
1114
+ case 'solid':
1115
+ return `import { lazyIsland } from '@atolljs/solid-island';
1116
+
1117
+ const ${P}Island = lazyIsland(() => import('./islands/${name}.island'));
1118
+
1119
+ <${P}Island label="${name}" />`;
1120
+ case 'svelte':
1121
+ return `<script lang="ts">
1122
+ import ${P}Island from './islands/${P}Island.svelte';
1123
+ </script>
1124
+
1125
+ <${P}Island label="${name}" />`;
1126
+ case 'angular':
1127
+ return `import { ${P}Island } from './islands/${name}.island';
1128
+ // add ${P}Island to the host component's imports, then:
1129
+ // <${name}-island [props]="{ label: '${name}' }" (incremented)="onIncremented($event)" />`;
1130
+ default:
1131
+ return '';
1132
+ }
1133
+ }
1134
+
1135
+ /* ── nestjs (`atoll add nestjs <level> <name>`) ─────────────────────────── */
1136
+
1137
+ /**
1138
+ * The pool boundary module + worker entry — shared by `service` (class
1139
+ * facade), `module` (boundary only), and referenced by `method`.
1140
+ * The module is symmetric: the app imports it for DI, each worker boots it
1141
+ * via runAtollWorker.
1142
+ */
1143
+ export function nestjsModuleFiles(
1144
+ name: string,
1145
+ dir: string,
1146
+ opts: { service?: string; mem?: MemRef | null },
1147
+ ): OutFile[] {
1148
+ const P = pascal(name);
1149
+ const memImport = opts.mem ? `import { ${opts.mem.export_} } from '${opts.mem.import_}';\n` : '';
1150
+ const serviceImport = opts.service
1151
+ ? `import { ${opts.service} } from './${name}.service';\n`
1152
+ : '';
1153
+ const memLine = opts.mem
1154
+ ? ` sharedMemory: ${opts.mem.export_},\n`
1155
+ : ` // message-only — add sharedMemory: <contract> to give the pool a buffer\n`;
1156
+ const providers = opts.service
1157
+ ? ` providers: [${opts.service}],\n exports: [${opts.service}, AtollModule],\n`
1158
+ : '';
1159
+ return [
1160
+ {
1161
+ path: `${dir}/${name}.module.ts`,
1162
+ content: `import { Module } from '@nestjs/common';
1163
+ import { Worker } from 'node:worker_threads';
1164
+ import { AtollModule } from '@atolljs/nestjs';
1165
+ ${serviceImport}${memImport}
1166
+ /**
1167
+ * ${name} pool boundary — the feature module owns its worker domain.
1168
+ * Imported by AppModule for the API thread (registerPool spawns workers)
1169
+ * and by the worker entry (runAtollWorker boots the same module in-thread).
1170
+ */
1171
+ @Module({
1172
+ imports: [
1173
+ AtollModule.registerPool({
1174
+ name: '${name}',
1175
+ // webpack emits the worker entry as its own chunk — keep the URL inline
1176
+ worker: () => new Worker(new URL('./${name}.worker.ts', import.meta.url)),
1177
+ ${memLine} poolSize: 'auto',
1178
+ }),
1179
+ ],
1180
+ ${providers}})
1181
+ export class ${P}Module {}
1182
+ `,
1183
+ },
1184
+ {
1185
+ path: `${dir}/${name}.worker.ts`,
1186
+ content: `// Worker entry — boots a Nest application context inside the worker and
1187
+ // binds every @AtollService/@AtollTask provider in ${P}Module.
1188
+ import { runAtollWorker } from '@atolljs/nestjs/worker';
1189
+ import { ${P}Module } from './${name}.module';
1190
+
1191
+ void runAtollWorker(${P}Module);
1192
+ `,
1193
+ },
1194
+ ];
1195
+ }
1196
+
1197
+ /**
1198
+ * `add nestjs service <name>` — the @AtollService facade: every method
1199
+ * dispatches to the pool, consumers inject it with zero Atoll imports.
1200
+ * Emits the service plus the pool module + worker entry it runs in.
1201
+ */
1202
+ export function nestjsServiceFiles(
1203
+ name: string,
1204
+ dir: string,
1205
+ mem: MemRef | null,
1206
+ ): OutFile[] {
1207
+ const P = pascal(name);
1208
+ const memImport = mem ? `import { ${mem.export_} } from '${mem.import_}';\n` : '';
1209
+ const memWrite = mem
1210
+ ? `\n // shared memory crosses without a message — pick a real field:\n // ${mem.export_}.signals.progress.write(100);`
1211
+ : '';
1212
+ return [
1213
+ {
1214
+ path: `${dir}/${name}.service.ts`,
1215
+ content: `import { Injectable } from '@nestjs/common';
1216
+ import { threadId } from 'node:worker_threads';
1217
+ import { AtollService } from '@atolljs/nestjs/decorators';
1218
+ ${memImport}
1219
+ /**
1220
+ * Service-level facade — the class IS the interop surface. Consumers inject
1221
+ * it like any provider; every call dispatches to the '${name}' pool as a
1222
+ * ${P}Service.method task and the body runs on the worker's DI'd instance.
1223
+ */
1224
+ @Injectable()
1225
+ @AtollService({ pool: '${name}' })
1226
+ export class ${P}Service {
1227
+ /** Runs inside the worker — the return value crosses postMessage. */
1228
+ async echo(input: string) {${memWrite}
1229
+ return { heard: input, worker: threadId };
1230
+ }
1231
+ }
1232
+ `,
1233
+ },
1234
+ ...nestjsModuleFiles(name, dir, { service: `${P}Service`, mem }),
1235
+ ];
1236
+ }
1237
+
1238
+ /**
1239
+ * `add nestjs method <name>` — method-level offload via @AtollTask: only
1240
+ * decorated methods dispatch; the rest of the class runs on the API thread.
1241
+ */
1242
+ export function nestjsMethodFiles(name: string, dir: string): OutFile[] {
1243
+ const P = pascal(name);
1244
+ return [
1245
+ {
1246
+ path: `${dir}/${name}.service.ts`,
1247
+ content: `import { Injectable } from '@nestjs/common';
1248
+ import { threadId } from 'node:worker_threads';
1249
+ import { AtollTask } from '@atolljs/nestjs/decorators';
1250
+
1251
+ /**
1252
+ * Method-level offload — only decorated methods dispatch to the pool; the
1253
+ * rest of the class stays on the API thread. The body executes inside the
1254
+ * worker's own Nest context on the DI-resolved instance.
1255
+ */
1256
+ @Injectable()
1257
+ export class ${P}Service {
1258
+ @AtollTask({ pool: '${name}' })
1259
+ async run(input: string) {
1260
+ return { heard: input, worker: threadId };
1261
+ }
1262
+
1263
+ /** Not decorated — plain main-thread method. */
1264
+ describe() {
1265
+ return '${name} service, partial offload';
1266
+ }
1267
+ }
1268
+ `,
1269
+ },
1270
+ ];
1271
+ }
1272
+
1273
+ /**
1274
+ * `add nestjs housed <name>` — a route subtree that exists ONLY inside pool
1275
+ * workers: dedicated HTTP worker (NestFactory + serveHttp), a worker-side
1276
+ * API module, a controller, and the main-side pool registration.
1277
+ */
1278
+ export function nestjsHousedFiles(name: string, dir: string): OutFile[] {
1279
+ const P = pascal(name);
1280
+ return [
1281
+ {
1282
+ path: `${dir}/${name}-atoll.module.ts`,
1283
+ content: `// Main-side boundary — registers the '${name}' pool the housed API
1284
+ // runs in. Message-only by default; to share another pool's buffer, wrap
1285
+ // the factory:
1286
+ // worker: withSharedBuffer(
1287
+ // () => new Worker(new URL('./${name}.worker.ts', import.meta.url)),
1288
+ // () => getAtollPool('<other-pool>')?.sharedBuffer,
1289
+ // )
1290
+ import { Module } from '@nestjs/common';
1291
+ import { Worker } from 'node:worker_threads';
1292
+ import { AtollModule } from '@atolljs/nestjs';
1293
+
1294
+ @Module({
1295
+ imports: [
1296
+ AtollModule.registerPool({
1297
+ name: '${name}',
1298
+ worker: () => new Worker(new URL('./${name}.worker.ts', import.meta.url)),
1299
+ poolSize: 2,
1300
+ }),
1301
+ ],
1302
+ })
1303
+ export class ${P}AtollModule {}
1304
+ `,
1305
+ },
1306
+ {
1307
+ path: `${dir}/${name}.worker.ts`,
1308
+ content: `// Dedicated HTTP worker — NOT a task worker: it serves requests, so no
1309
+ // runAtollWorker. serveHttp binds an internal 127.0.0.1 port and announces
1310
+ // it via the HTTP_PORT handshake; a respawned worker re-announces.
1311
+ import 'reflect-metadata';
1312
+ import { NestFactory } from '@nestjs/core';
1313
+ import { serveHttp } from '@atolljs/node/http';
1314
+ import { ${P}ApiModule } from './${name}-api.module';
1315
+
1316
+ void (async () => {
1317
+ const app = await NestFactory.create(${P}ApiModule, { logger: ['warn', 'error'] });
1318
+ await app.init();
1319
+ serveHttp(app.getHttpServer(), { listen: 0 });
1320
+ })();
1321
+ `,
1322
+ },
1323
+ {
1324
+ path: `${dir}/${name}-api.module.ts`,
1325
+ content: `// Worker-side module — pure HTTP surface (controllers/providers only).
1326
+ // No registerPool import: pools only exist on the main thread.
1327
+ import { Module } from '@nestjs/common';
1328
+ import { ${P}Controller } from './${name}.controller';
1329
+
1330
+ @Module({ controllers: [${P}Controller] })
1331
+ export class ${P}ApiModule {}
1332
+ `,
1333
+ },
1334
+ {
1335
+ path: `${dir}/${name}.controller.ts`,
1336
+ content: `import { Controller, Get } from '@nestjs/common';
1337
+ import { threadId } from 'node:worker_threads';
1338
+
1339
+ /**
1340
+ * Housed controller — exists ONLY inside '${name}' pool workers. The main
1341
+ * app never sees these routes: it proxies /api/${name}/* into the workers'
1342
+ * internal listeners, so decorators, DI, and guards execute off-thread.
1343
+ */
1344
+ @Controller('api/${name}')
1345
+ export class ${P}Controller {
1346
+ /** Which pool worker owns this request. */
1347
+ @Get('whoami')
1348
+ whoami() {
1349
+ return { worker: threadId };
1350
+ }
1351
+ }
1352
+ `,
1353
+ },
1354
+ ];
1355
+ }
1356
+
1357
+ /** Gateway wiring printed after `add nestjs housed` — belongs in main.ts. */
1358
+ export function nestjsHousedWiring(name: string): string {
1359
+ return `main.ts — proxy the prefix into the pool:
1360
+
1361
+ import { getAtollPool } from '@atolljs/nestjs';
1362
+ import { proxyToWorker, workerHttpPorts } from '@atolljs/node/http';
1363
+
1364
+ const pool = getAtollPool('${name}');
1365
+ if (pool) {
1366
+ const tracker = workerHttpPorts(pool);
1367
+ app.use('/api/${name}', proxyToWorker({
1368
+ pool,
1369
+ tracker,
1370
+ to: '/api/${name}', // express strips the mount — restore it in-worker
1371
+ }));
1372
+ }`;
1373
+ }
1374
+
1375
+ /* ── nextjs (`atoll add nextjs <kind> <name>`) ──────────────────────────── */
1376
+
1377
+ /**
1378
+ * `add nextjs route <name> [task|client]` — a Node-runtime route handler
1379
+ * backed by a node:worker_threads pool. `task` = TaskRegistry contract
1380
+ * dispatch; `client` = defineWorker + typed workerClient.
1381
+ */
1382
+ export function nextjsRouteFiles(
1383
+ name: string,
1384
+ dir: string,
1385
+ variant: 'task' | 'client',
1386
+ ): OutFile[] {
1387
+ const c = camel(name);
1388
+ const P = pascal(name);
1389
+ const workerFactory = `createNodeWorker(
1390
+ new Worker(new URL('./${name}.worker.ts', import.meta.url)),
1391
+ )`;
1392
+ if (variant === 'task') {
1393
+ return [
1394
+ {
1395
+ path: `${dir}/${name}.contract.ts`,
1396
+ content: `import { z } from 'zod';
1397
+ import { defineSharedMemory, field, type TaskContract } from '@atolljs/core';
1398
+
1399
+ /** Pool-owned shared state — the route handler reads it with zero dispatch. */
1400
+ export const ${c}Memory = defineSharedMemory({
1401
+ jobsDone: field.number(),
1402
+ });
1403
+
1404
+ const result = z.object({
1405
+ echo: z.string(),
1406
+ ms: z.number(),
1407
+ jobsDone: z.number(),
1408
+ });
1409
+
1410
+ /** Executed inside a worker via TaskRegistry. */
1411
+ export const ${P}Job: TaskContract<[input?: string], z.infer<typeof result>> = {
1412
+ taskId: '${name}.run',
1413
+ resultSchema: result,
1414
+ };
1415
+ `,
1416
+ },
1417
+ {
1418
+ path: `${dir}/${name}.worker.ts`,
1419
+ content: `// Worker entry — the shim MUST be first: it binds self = parentPort
1420
+ // before workerBootstrap wires INIT_MEMORY / EXECUTE_TASK.
1421
+ import '@atolljs/node/shim';
1422
+ import '@atolljs/core/worker/workerBootstrap';
1423
+ import { TaskRegistry } from '@atolljs/core';
1424
+ import { ${c}Memory, ${P}Job } from './${name}.contract';
1425
+
1426
+ TaskRegistry.register(${P}Job, (input = '${name}') => {
1427
+ const t0 = performance.now();
1428
+ const jobs = ${c}Memory.jobsDone.read() + 1;
1429
+ ${c}Memory.jobsDone.write(jobs);
1430
+ return { echo: \`worker heard: \${input}\`, ms: performance.now() - t0, jobsDone: jobs };
1431
+ });
1432
+ `,
1433
+ },
1434
+ {
1435
+ path: `${dir}/pool.ts`,
1436
+ content: `import { Worker } from 'node:worker_threads';
1437
+ import { createNodePool, createNodeWorker } from '@atolljs/node';
1438
+ import { ${c}Memory, ${P}Job } from './${name}.contract';
1439
+
1440
+ const create = () => {
1441
+ const pool = createNodePool({
1442
+ sharedMemory: ${c}Memory,
1443
+ poolSize: 'auto',
1444
+ tasks: { run: ${P}Job },
1445
+ createWorker: () =>
1446
+ ${workerFactory},
1447
+ });
1448
+ // Read memory via this getter — Next compiles module graphs per route
1449
+ // layer, so a fresh contract import can be a second, unbound instance.
1450
+ return { pool, memory: ${c}Memory };
1451
+ };
1452
+
1453
+ // Module scope = process scope; globalThis survives dev-mode HMR reloads.
1454
+ export const get${P} = (): ReturnType<typeof create> => {
1455
+ const g = globalThis as { __atoll${P}?: ReturnType<typeof create> };
1456
+ return (g.__atoll${P} ??= create());
1457
+ };
1458
+ `,
1459
+ },
1460
+ {
1461
+ path: `${dir}/route.ts`,
1462
+ content: `import { NextResponse } from 'next/server';
1463
+ import { get${P} } from './pool';
1464
+
1465
+ // worker_threads need the Node.js runtime — Edge cannot spawn them.
1466
+ export const runtime = 'nodejs';
1467
+ export const dynamic = 'force-dynamic';
1468
+
1469
+ /** POST /api/${name} {"input": "..."} — dispatch the task into the pool. */
1470
+ export async function POST(req: Request) {
1471
+ const body = await req.json().catch(() => ({}));
1472
+ return NextResponse.json(await get${P}().pool.run(body?.input));
1473
+ }
1474
+
1475
+ /** GET /api/${name} — read shared state on this thread, no dispatch. */
1476
+ export async function GET() {
1477
+ return NextResponse.json({ jobsDone: get${P}().memory.jobsDone.read() });
1478
+ }
1479
+ `,
1480
+ },
1481
+ ];
1482
+ }
1483
+ // 'client' variant — defineWorker + typed workerClient
1484
+ return [
1485
+ {
1486
+ path: `${dir}/${name}.contract.ts`,
1487
+ content: `import { defineSharedMemory, field } from '@atolljs/core';
1488
+
1489
+ /** Pool-owned shared state — the route handler reads it with zero dispatch. */
1490
+ export const ${c}Memory = defineSharedMemory({
1491
+ jobsDone: field.number(),
1492
+ });
1493
+ `,
1494
+ },
1495
+ {
1496
+ path: `${dir}/${name}.worker.ts`,
1497
+ content: `// Worker entry — shim first, then defineWorker installs the message loop.
1498
+ import '@atolljs/node/shim';
1499
+ import { defineWorker, serviceMethod } from '@atolljs/core';
1500
+ import { z } from 'zod';
1501
+ import { ${c}Memory } from './${name}.contract';
1502
+
1503
+ const echo = serviceMethod({
1504
+ def: { argsSchema: z.tuple([z.string()]), resultSchema: z.string() },
1505
+ run: (message) => {
1506
+ ${c}Memory.jobsDone.write(${c}Memory.jobsDone.read() + 1);
1507
+ return \`worker heard: \${message}\`;
1508
+ },
1509
+ });
1510
+
1511
+ export const ${c}Worker = defineWorker({
1512
+ sharedMemory: ${c}Memory,
1513
+ methods: { echo },
1514
+ });
1515
+ export type ${P}Worker = typeof ${c}Worker;
1516
+ `,
1517
+ },
1518
+ {
1519
+ path: `${dir}/pool.ts`,
1520
+ content: `import { Worker } from 'node:worker_threads';
1521
+ import { workerClient } from '@atolljs/core';
1522
+ import { createNodePool, createNodeWorker } from '@atolljs/node';
1523
+ import { ${c}Memory } from './${name}.contract';
1524
+ // Type-only — worker code must not leak into the server bundle.
1525
+ import type { ${P}Worker } from './${name}.worker';
1526
+
1527
+ const create = () => {
1528
+ const pool = createNodePool({
1529
+ sharedMemory: ${c}Memory,
1530
+ poolSize: 'auto',
1531
+ createWorker: () =>
1532
+ ${workerFactory},
1533
+ });
1534
+ // Read memory via this getter — Next compiles module graphs per route
1535
+ // layer, so a fresh contract import can be a second, unbound instance.
1536
+ return { pool, client: workerClient<${P}Worker>(pool), memory: ${c}Memory };
1537
+ };
1538
+
1539
+ // Module scope = process scope; globalThis survives dev-mode HMR reloads.
1540
+ export const get${P} = (): ReturnType<typeof create> => {
1541
+ const g = globalThis as { __atoll${P}?: ReturnType<typeof create> };
1542
+ return (g.__atoll${P} ??= create());
1543
+ };
1544
+ `,
1545
+ },
1546
+ {
1547
+ path: `${dir}/route.ts`,
1548
+ content: `import { NextResponse } from 'next/server';
1549
+ import { get${P} } from './pool';
1550
+
1551
+ // worker_threads need the Node.js runtime — Edge cannot spawn them.
1552
+ export const runtime = 'nodejs';
1553
+ export const dynamic = 'force-dynamic';
1554
+
1555
+ /** POST /api/${name} {"input": "..."} — a typed client call into the pool. */
1556
+ export async function POST(req: Request) {
1557
+ const body = await req.json().catch(() => ({}));
1558
+ return NextResponse.json({
1559
+ echo: await get${P}().client.echo(String(body?.input ?? '${name}')),
1560
+ });
1561
+ }
1562
+
1563
+ /** GET /api/${name} — read shared state on this thread, no dispatch. */
1564
+ export async function GET() {
1565
+ return NextResponse.json({ jobsDone: get${P}().memory.jobsDone.read() });
1566
+ }
1567
+ `,
1568
+ },
1569
+ ];
1570
+ }
1571
+
1572
+ /**
1573
+ * `add nextjs component <name>` — a 'use client' component bound to the
1574
+ * ${name} pool via useTask/useSharedValue. Expects the client + contract
1575
+ * from `add nextjs worker <name>` beside it.
1576
+ */
1577
+ export function nextjsComponentFiles(
1578
+ name: string,
1579
+ dir: string,
1580
+ mem: MemRef | null,
1581
+ ): OutFile[] {
1582
+ const c = camel(name);
1583
+ const P = pascal(name);
1584
+ return [
1585
+ {
1586
+ path: `${dir}/${name}.tsx`,
1587
+ content: `'use client';
1588
+ import { useTask${mem ? ', useSharedValue' : ''} } from '@atolljs/nextjs';
1589
+ import { ${c} } from './${name}';
1590
+ ${mem ? `import { ${mem.export_} } from '${mem.import_}';\n` : ''}
1591
+ /**
1592
+ * ${P} — a client component over the ${name} pool. Field reads return
1593
+ * undefined until the contract binds on the client (SSR-safe).
1594
+ */
1595
+ export function ${P}() {
1596
+ const echo = useTask(${c}.echo);${
1597
+ mem
1598
+ ? `\n const progress = useSharedValue(${mem.export_}, 'signals.progress');`
1599
+ : ''
1600
+ }
1601
+ return (
1602
+ <button onClick={() => echo.run('ping')}>
1603
+ {echo.data ?? (echo.pending ? 'working\\u2026' : '${name}')}
1604
+ </button>
1605
+ );
1606
+ }
1607
+ `,
1608
+ },
1609
+ ];
1610
+ }
1611
+
1612
+ /**
1613
+ * `add nextjs instrumentation <name>` — the server bootstrap hook: warms the
1614
+ * named route pool at boot instead of inside the first unlucky request.
1615
+ */
1616
+ export function nextjsInstrumentationFile(name: string, dir = 'src'): OutFile {
1617
+ const P = pascal(name);
1618
+ return {
1619
+ path: `${dir}/instrumentation.ts`,
1620
+ content: `/**
1621
+ * Next.js server bootstrap — runs once when the Node.js runtime boots.
1622
+ * Dynamic imports keep node:worker_threads out of the edge/browser bundles.
1623
+ */
1624
+ export async function register() {
1625
+ if (process.env.NEXT_RUNTIME !== 'nodejs') return;
1626
+ const { get${P} } = await import('./app/api/${name}/pool');
1627
+ get${P}(); // spawn workers + bind the buffer during startup, not per request
1628
+ }
1629
+ `,
1630
+ };
1631
+ }
1632
+