@atolljs/cli 0.1.5 → 0.1.7
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/README.md +13 -1
- package/dist/cli.js +662 -6
- package/package.json +1 -1
- package/src/cli.ts +14 -3
- package/src/commands.ts +137 -2
- package/src/templates.ts +585 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atolljs/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
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,13 +12,14 @@
|
|
|
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, runDoctor, runInit, type Ctx } from './commands.ts';
|
|
15
|
+
import { runAdd, 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
|
|
|
19
|
-
${fmt.accent('atoll new <dir>')} [--framework react|vue|solid|svelte|node] [--pm npm]
|
|
19
|
+
${fmt.accent('atoll new <dir>')} [--framework react|vue|solid|svelte|node] [--mfe] [--pm npm]
|
|
20
20
|
Scaffold a fresh app — vite shell + a worker-rendered island,
|
|
21
|
-
or a tsx Node service with a pooled worker.
|
|
21
|
+
or a tsx Node service with a pooled worker. --mfe scaffolds a
|
|
22
|
+
publishable MFE package (contract + worker + bundle build).
|
|
22
23
|
|
|
23
24
|
${fmt.accent('atoll init')} [--name app] [--force]
|
|
24
25
|
Wire Atoll into the current project: hardened .npmrc, package
|
|
@@ -28,6 +29,8 @@ const HELP = (fmt: Fmt) => `${fmt.strong('atoll')} — bootstrap multithreading
|
|
|
28
29
|
worker pool + typed client (+ framework bindings on UI projects)
|
|
29
30
|
memory shared-memory contract
|
|
30
31
|
island worker-rendered UI — facade (contract + lazy import) | mono
|
|
32
|
+
mfe publishable micro-frontend — island contract + worker +
|
|
33
|
+
vite.mfe.config.ts (build a self-contained remote bundle)
|
|
31
34
|
service nestjs: @AtollService class facade + pool module + worker
|
|
32
35
|
method nestjs: @AtollTask method-level offload
|
|
33
36
|
module nestjs: registerPool boundary module + worker entry
|
|
@@ -35,6 +38,8 @@ const HELP = (fmt: Fmt) => `${fmt.strong('atoll')} — bootstrap multithreading
|
|
|
35
38
|
route nextjs: api route pool — task | client variants
|
|
36
39
|
component nextjs: 'use client' hooks component over a pool
|
|
37
40
|
instrumentation nextjs: server bootstrap that warms route pools
|
|
41
|
+
devtools observability — init module + @atolljs/devtools install
|
|
42
|
+
(browser: ?__atoll_devtools + /__atoll/ · node: ATOLL_DEVTOOLS=1)
|
|
38
43
|
|
|
39
44
|
Frameworks: react vue solid svelte angular nestjs nextjs node
|
|
40
45
|
[--dir <path>] [--memory <name>|--no-memory] [--variant <v>] [--force]
|
|
@@ -43,6 +48,10 @@ const HELP = (fmt: Fmt) => `${fmt.strong('atoll')} — bootstrap multithreading
|
|
|
43
48
|
Check the setup — deps, lockfile, .npmrc policy, bundler-detectable
|
|
44
49
|
worker entries, COOP/COEP headers. --fix writes a hardened .npmrc.
|
|
45
50
|
|
|
51
|
+
${fmt.accent('atoll devtools')} [--port 4780]
|
|
52
|
+
Serve the local devtools dashboard — pools, task timings, shared-memory
|
|
53
|
+
writes, island traffic. Apps connect via connectDevtools().
|
|
54
|
+
|
|
46
55
|
${fmt.dim(`flags: --yes/-y accepts defaults, --force overwrites existing files,
|
|
47
56
|
--no-install skips dependency installation`)}
|
|
48
57
|
`;
|
|
@@ -69,6 +78,8 @@ export async function runCli(argv: readonly string[], io: Io, cwd: string): Prom
|
|
|
69
78
|
return await runAdd(ctx);
|
|
70
79
|
case 'doctor':
|
|
71
80
|
return runDoctor(ctx);
|
|
81
|
+
case 'devtools':
|
|
82
|
+
return await runDevtools(ctx);
|
|
72
83
|
case 'help':
|
|
73
84
|
case undefined:
|
|
74
85
|
io.print(HELP(io.fmt));
|
package/src/commands.ts
CHANGED
|
@@ -137,6 +137,7 @@ const ADD_KINDS = [
|
|
|
137
137
|
'worker',
|
|
138
138
|
'memory',
|
|
139
139
|
'island',
|
|
140
|
+
'mfe',
|
|
140
141
|
'service',
|
|
141
142
|
'method',
|
|
142
143
|
'module',
|
|
@@ -144,6 +145,7 @@ const ADD_KINDS = [
|
|
|
144
145
|
'route',
|
|
145
146
|
'component',
|
|
146
147
|
'instrumentation',
|
|
148
|
+
'devtools',
|
|
147
149
|
] as const;
|
|
148
150
|
type AddKind = (typeof ADD_KINDS)[number];
|
|
149
151
|
|
|
@@ -152,6 +154,7 @@ const KIND_SCOPE: Record<AddKind, readonly Framework[] | null> = {
|
|
|
152
154
|
worker: null,
|
|
153
155
|
memory: null,
|
|
154
156
|
island: ISLAND_FRAMEWORKS,
|
|
157
|
+
mfe: ISLAND_FRAMEWORKS,
|
|
155
158
|
service: ['nestjs'],
|
|
156
159
|
method: ['nestjs'],
|
|
157
160
|
module: ['nestjs'],
|
|
@@ -159,6 +162,7 @@ const KIND_SCOPE: Record<AddKind, readonly Framework[] | null> = {
|
|
|
159
162
|
route: ['nextjs'],
|
|
160
163
|
component: ['nextjs'],
|
|
161
164
|
instrumentation: ['nextjs'],
|
|
165
|
+
devtools: null,
|
|
162
166
|
};
|
|
163
167
|
|
|
164
168
|
/**
|
|
@@ -281,7 +285,12 @@ export async function runAdd(ctx: Ctx): Promise<number> {
|
|
|
281
285
|
nameArg = rest[1];
|
|
282
286
|
}
|
|
283
287
|
|
|
284
|
-
|
|
288
|
+
// devtools writes a fixed devtools.ts — a positional/--name argument
|
|
289
|
+
// becomes the dashboard session label, not a file name.
|
|
290
|
+
const name =
|
|
291
|
+
kind === 'devtools'
|
|
292
|
+
? (nameArg ?? flag(ctx.args, 'name') ?? project.pkg?.name?.split('/').pop() ?? 'app')
|
|
293
|
+
: await pickName(ctx, kind, nameArg);
|
|
285
294
|
if (!name) return 1;
|
|
286
295
|
const force = { force: hasFlag(ctx.args, 'force') };
|
|
287
296
|
const atollDir = flag(ctx.args, 'dir') ?? rel(join(project.srcDir, 'atoll'));
|
|
@@ -360,6 +369,34 @@ export async function runAdd(ctx: Ctx): Promise<number> {
|
|
|
360
369
|
return 0;
|
|
361
370
|
}
|
|
362
371
|
|
|
372
|
+
case 'mfe': {
|
|
373
|
+
const dir = flag(ctx.args, 'dir') ?? rel(join(project.srcDir, 'mfe'));
|
|
374
|
+
const files = T.mfeFiles(name, framework, dir);
|
|
375
|
+
// The publish build lives at the project root beside vite.config.ts —
|
|
376
|
+
// it exists only for `vite build --config`, never the app dev server.
|
|
377
|
+
const publishCfg = rel(join(project.root!, 'vite.mfe.config.ts'));
|
|
378
|
+
// The config lives at the project root — the lib entry must be
|
|
379
|
+
// root-relative even when `add` ran from a subdirectory or --dir moved.
|
|
380
|
+
const dirFromRoot = relative(project.root!, join(ctx.cwd, dir)).replaceAll('\\', '/');
|
|
381
|
+
await writeTree(
|
|
382
|
+
ctx.io,
|
|
383
|
+
ctx.cwd,
|
|
384
|
+
[
|
|
385
|
+
...files,
|
|
386
|
+
{ path: publishCfg, content: T.mfePublishConfig(name, framework, dirFromRoot) },
|
|
387
|
+
],
|
|
388
|
+
force,
|
|
389
|
+
);
|
|
390
|
+
if (missingDeps(project.pkg, ['vite']).length) {
|
|
391
|
+
ctx.io.warn(
|
|
392
|
+
`vite isn't installed — the publish build needs it: ${ctx.io.fmt.accent(pmInstallCmd(project.pm, ['-D', 'vite']))}`,
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
ctx.io.print(ctx.io.fmt.strong('\nwire it:'));
|
|
396
|
+
ctx.io.print(T.mfeUsage(name, framework, dir));
|
|
397
|
+
return 0;
|
|
398
|
+
}
|
|
399
|
+
|
|
363
400
|
case 'service': {
|
|
364
401
|
const dir = atollDir;
|
|
365
402
|
mkdirSync(join(ctx.cwd, dir), { recursive: true });
|
|
@@ -450,6 +487,54 @@ export async function runAdd(ctx: Ctx): Promise<number> {
|
|
|
450
487
|
return 0;
|
|
451
488
|
}
|
|
452
489
|
|
|
490
|
+
case 'devtools': {
|
|
491
|
+
const host = hostOf(framework);
|
|
492
|
+
// Angular's CLI dev server mounts no /__atoll/ — stream to the
|
|
493
|
+
// aggregate server instead of the BroadcastChannel + flyout path.
|
|
494
|
+
const wsUrl = host === 'browser' && framework === 'angular'
|
|
495
|
+
? 'ws://127.0.0.1:4780/events'
|
|
496
|
+
: undefined;
|
|
497
|
+
const file = `${atollDir}/devtools.ts`;
|
|
498
|
+
await writeTree(
|
|
499
|
+
ctx.io,
|
|
500
|
+
ctx.cwd,
|
|
501
|
+
[{ path: file, content: T.devtoolsSetup(name, host, wsUrl) }],
|
|
502
|
+
force,
|
|
503
|
+
);
|
|
504
|
+
if (missingDeps(project.pkg, ['@atolljs/devtools']).length) {
|
|
505
|
+
await offerInstall(ctx, project, ['-D', '@atolljs/devtools']);
|
|
506
|
+
}
|
|
507
|
+
// Print the specifier relative to srcDir — the import lands in the
|
|
508
|
+
// app entry (src/main.ts style), not in cwd.
|
|
509
|
+
const srcRel = rel(project.srcDir);
|
|
510
|
+
const spec = file.startsWith(`${srcRel}/`)
|
|
511
|
+
? `./${file.slice(srcRel.length + 1).replace(/\.ts$/, '')}`
|
|
512
|
+
: `./${file.replace(/\.ts$/, '')}`;
|
|
513
|
+
ctx.io.print('');
|
|
514
|
+
ctx.io.print(ctx.io.fmt.strong('wire it:'));
|
|
515
|
+
ctx.io.print(` import the module first in your entry — before any pool spawns:`);
|
|
516
|
+
ctx.io.print(ctx.io.fmt.accent(` import '${spec}';`));
|
|
517
|
+
if (host === 'node') {
|
|
518
|
+
ctx.io.print(
|
|
519
|
+
` ${ctx.io.fmt.accent('ATOLL_DEVTOOLS=1')} starts streaming · ${ctx.io.fmt.accent('atoll devtools')} serves the dashboard`,
|
|
520
|
+
);
|
|
521
|
+
} else if (wsUrl) {
|
|
522
|
+
ctx.io.print(
|
|
523
|
+
` ${ctx.io.fmt.accent('atoll devtools')} serves the aggregate dashboard · open the app with ${ctx.io.fmt.accent('?__atoll_devtools')}`,
|
|
524
|
+
);
|
|
525
|
+
} else {
|
|
526
|
+
ctx.io.print(
|
|
527
|
+
` open the app with ${ctx.io.fmt.accent('?__atoll_devtools')} — the flyout mounts and /__atoll/ serves the dashboard`,
|
|
528
|
+
);
|
|
529
|
+
ctx.io.print(
|
|
530
|
+
ctx.io.fmt.dim(
|
|
531
|
+
` /__atoll/ needs the @atolljs/vite plugin (plugins: [atoll()]) — no plugin? run \`atoll devtools\` + pass transport: 'websocket'`,
|
|
532
|
+
),
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
return 0;
|
|
536
|
+
}
|
|
537
|
+
|
|
453
538
|
default:
|
|
454
539
|
ctx.io.error(`unhandled add kind ${kind}`);
|
|
455
540
|
return 1;
|
|
@@ -488,8 +573,17 @@ export async function runCreate(ctx: Ctx): Promise<number> {
|
|
|
488
573
|
}
|
|
489
574
|
mkdirSync(target, { recursive: true });
|
|
490
575
|
|
|
576
|
+
const mfe = hasFlag(ctx.args, 'mfe');
|
|
577
|
+
if (mfe && fw === 'node') {
|
|
578
|
+
ctx.io.error('--mfe is a browser-island shape — pick react|vue|solid|svelte');
|
|
579
|
+
return 1;
|
|
580
|
+
}
|
|
491
581
|
const files =
|
|
492
|
-
fw === 'node'
|
|
582
|
+
fw === 'node'
|
|
583
|
+
? T.nodeScaffoldFiles(dirName)
|
|
584
|
+
: mfe
|
|
585
|
+
? T.mfeScaffoldFiles(dirName, fw)
|
|
586
|
+
: T.scaffoldFiles(dirName, fw);
|
|
493
587
|
await writeTree(ctx.io, target, files, { force: true });
|
|
494
588
|
|
|
495
589
|
const written = JSON.parse(readFileSync(join(target, 'package.json'), 'utf8'));
|
|
@@ -507,9 +601,50 @@ export async function runCreate(ctx: Ctx): Promise<number> {
|
|
|
507
601
|
|
|
508
602
|
ctx.io.print('');
|
|
509
603
|
ctx.io.print(`done — ${ctx.io.fmt.accent(`cd ${dirName} && ${pm} run dev`)}`);
|
|
604
|
+
if (mfe) {
|
|
605
|
+
ctx.io.print(
|
|
606
|
+
` ${ctx.io.fmt.accent('npm run build:mfe')} → dist-mfe/${dirName}.worker.js — publish behind CORS + a versioned URL`,
|
|
607
|
+
);
|
|
608
|
+
ctx.io.print(
|
|
609
|
+
` ${ctx.io.fmt.accent('npm run preview:mfe')} serves it cross-origin; shells mount the contract`,
|
|
610
|
+
);
|
|
611
|
+
}
|
|
510
612
|
return 0;
|
|
511
613
|
}
|
|
512
614
|
|
|
615
|
+
/* ── atoll devtools ─────────────────────────────────────────────────────── */
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* `atoll devtools [--port N]` — boot the local devtools server + dashboard.
|
|
619
|
+
* The server lives in @atolljs/devtools: resolved as an installed package in
|
|
620
|
+
* consumer projects, with a relative fallback so `npm run atoll devtools`
|
|
621
|
+
* works inside the atoll monorepo where the package isn't installed.
|
|
622
|
+
*
|
|
623
|
+
* Never resolves — the process serves until SIGINT.
|
|
624
|
+
*/
|
|
625
|
+
export async function runDevtools(ctx: Ctx): Promise<number> {
|
|
626
|
+
const { io } = ctx;
|
|
627
|
+
const spec = '@atolljs/devtools/server';
|
|
628
|
+
const mod: { createDevtoolsServer?: (o: { port: number }) => Promise<{ url: string }> } | null =
|
|
629
|
+
await import(spec).catch(async () => {
|
|
630
|
+
// In-repo fallback: the workspace package's built dist (extensionless
|
|
631
|
+
// sources can't run under type stripping — `npm run build` there first).
|
|
632
|
+
const local = new URL('../../devtools/dist/server.js', import.meta.url).href;
|
|
633
|
+
return import(local).catch(() => null);
|
|
634
|
+
});
|
|
635
|
+
if (!mod?.createDevtoolsServer) {
|
|
636
|
+
io.error(`@atolljs/devtools isn't installed — ${io.fmt.accent('npm i -D @atolljs/devtools')}`);
|
|
637
|
+
return 1;
|
|
638
|
+
}
|
|
639
|
+
const port = Number(flag(ctx.args, 'port') ?? 4780);
|
|
640
|
+
const server = await mod.createDevtoolsServer({ port: Number.isFinite(port) ? port : 4780 });
|
|
641
|
+
io.print(`atoll devtools → ${io.fmt.accent(server.url)}`);
|
|
642
|
+
io.print(io.fmt.dim(' ingest endpoint: ' + server.url.replace('http://', 'ws://') + '/events'));
|
|
643
|
+
io.print(io.fmt.dim(" in the app: connectDevtools() from '@atolljs/devtools'"));
|
|
644
|
+
io.warn('serving — Ctrl+C to stop');
|
|
645
|
+
return new Promise<number>(() => {});
|
|
646
|
+
}
|
|
647
|
+
|
|
513
648
|
/* ── atoll doctor ───────────────────────────────────────────────────────── */
|
|
514
649
|
|
|
515
650
|
interface Check {
|