@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/dist/cli.js
CHANGED
|
@@ -1266,6 +1266,501 @@ const ${P}Island = lazyIsland(() => import('./islands/${name}.island'));
|
|
|
1266
1266
|
return "";
|
|
1267
1267
|
}
|
|
1268
1268
|
}
|
|
1269
|
+
function mfeContractFile(name, fw) {
|
|
1270
|
+
const c = camel(name);
|
|
1271
|
+
const ext = ISLAND_EXT[fw] ?? "ts";
|
|
1272
|
+
return {
|
|
1273
|
+
path: `${name}.contract.ts`,
|
|
1274
|
+
content: `import { z } from '@atolljs/core';
|
|
1275
|
+
import { defineIslandContract } from '@atolljs/islands';
|
|
1276
|
+
|
|
1277
|
+
/**
|
|
1278
|
+
* '${name}' \u2014 the wire contract for this micro-frontend. Shells import THIS
|
|
1279
|
+
* module only: it names the app, pins the prop/event payload shapes, and
|
|
1280
|
+
* carries the worker factory. The worker attaches the same module via
|
|
1281
|
+
* { contract } so both sides validate one shape \u2014 drift fails loudly at
|
|
1282
|
+
* mount or emit.
|
|
1283
|
+
*
|
|
1284
|
+
* The factory below is bundler-detectable (\`new URL(..., import.meta.url)\`)
|
|
1285
|
+
* so local dev builds the worker entry automatically. Deployment changes
|
|
1286
|
+
* only this field \u2014 see docs/islands-remote.md for the serving rules:
|
|
1287
|
+
*
|
|
1288
|
+
* // CDN deploy \u2014 worker SCRIPT urls must stay same-origin (a cross-origin
|
|
1289
|
+
* // new Worker() throws SecurityError regardless of CORS), so the remote
|
|
1290
|
+
* // bundle loads through a same-origin blob: shim; CORS covers the
|
|
1291
|
+
* // import fetch inside it:
|
|
1292
|
+
* worker: () => {
|
|
1293
|
+
* const shim = \`import \${JSON.stringify(\`\${import.meta.env.VITE_MFE_ORIGIN}/${name}.worker.js\`)};\`;
|
|
1294
|
+
* return new Worker(
|
|
1295
|
+
* URL.createObjectURL(new Blob([shim], { type: 'text/javascript' })),
|
|
1296
|
+
* { type: 'module' },
|
|
1297
|
+
* );
|
|
1298
|
+
* },
|
|
1299
|
+
*
|
|
1300
|
+
* // Or ship dist-mfe/ INSIDE the npm package \u2014 resolve the bundle
|
|
1301
|
+
* // package-relative as a plain ASSET (hoisted new URL \u2014 inline inside
|
|
1302
|
+
* // new Worker would make the consumer's bundler re-emit it as a worker
|
|
1303
|
+
* // entry instead of copying it verbatim):
|
|
1304
|
+
* const bundled = new URL('../../dist-mfe/${name}.worker.js', import.meta.url);
|
|
1305
|
+
* worker: () => new Worker(bundled, { type: 'module' }),
|
|
1306
|
+
*/
|
|
1307
|
+
export const ${c}Contract = defineIslandContract({
|
|
1308
|
+
app: '${name}',
|
|
1309
|
+
props: z.object({ label: z.string().optional() }),
|
|
1310
|
+
events: {
|
|
1311
|
+
incremented: z.object({ count: z.number(), label: z.string() }),
|
|
1312
|
+
},
|
|
1313
|
+
worker: () =>
|
|
1314
|
+
new Worker(new URL('./${name}.worker.${ext}', import.meta.url), { type: 'module' }),
|
|
1315
|
+
});
|
|
1316
|
+
|
|
1317
|
+
export default ${c}Contract;
|
|
1318
|
+
`
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
function mfeWorkerFiles(name, fw) {
|
|
1322
|
+
const P = pascal(name);
|
|
1323
|
+
const c = camel(name);
|
|
1324
|
+
switch (fw) {
|
|
1325
|
+
case "react":
|
|
1326
|
+
return [
|
|
1327
|
+
{
|
|
1328
|
+
path: `${name}.worker.tsx`,
|
|
1329
|
+
content: `import { useState } from 'react';
|
|
1330
|
+
import { defineReactMonoWorker, emit } from '@atolljs/react-island/worker';
|
|
1331
|
+
import ${c}Contract from './${name}.contract';
|
|
1332
|
+
|
|
1333
|
+
/**
|
|
1334
|
+
* ${P} \u2014 a publishable React micro-frontend rendered INSIDE its own worker.
|
|
1335
|
+
* The bundle is self-contained; deploy it behind CORS + a versioned URL.
|
|
1336
|
+
*/
|
|
1337
|
+
function ${P}({ label = '${name}' }: { label?: string }) {
|
|
1338
|
+
const [count, setCount] = useState(0);
|
|
1339
|
+
return (
|
|
1340
|
+
<button
|
|
1341
|
+
onClick={() => {
|
|
1342
|
+
const n = count + 1;
|
|
1343
|
+
setCount(n);
|
|
1344
|
+
emit('incremented', { count: n, label }); // validates against the contract
|
|
1345
|
+
}}
|
|
1346
|
+
>
|
|
1347
|
+
{label}: {count}
|
|
1348
|
+
</button>
|
|
1349
|
+
);
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
export const worker = defineReactMonoWorker(${P}, { contract: ${c}Contract });
|
|
1353
|
+
`
|
|
1354
|
+
}
|
|
1355
|
+
];
|
|
1356
|
+
case "vue":
|
|
1357
|
+
return [
|
|
1358
|
+
{
|
|
1359
|
+
path: `${P}.vue`,
|
|
1360
|
+
content: `<script setup lang="ts">
|
|
1361
|
+
import { ref } from 'vue';
|
|
1362
|
+
import { emit } from '@atolljs/islands/worker';
|
|
1363
|
+
|
|
1364
|
+
// Rendered inside the worker \u2014 ops stream to the shell; emit() validates
|
|
1365
|
+
// the payload against the contract before it leaves the worker.
|
|
1366
|
+
const props = defineProps<{ label?: string }>();
|
|
1367
|
+
const count = ref(0);
|
|
1368
|
+
function increment() {
|
|
1369
|
+
count.value += 1;
|
|
1370
|
+
emit('incremented', { count: count.value, label: props.label ?? '${name}' });
|
|
1371
|
+
}
|
|
1372
|
+
</script>
|
|
1373
|
+
|
|
1374
|
+
<template>
|
|
1375
|
+
<button @click="increment">{{ label ?? '${name}' }}: {{ count }}</button>
|
|
1376
|
+
</template>
|
|
1377
|
+
`
|
|
1378
|
+
},
|
|
1379
|
+
{
|
|
1380
|
+
path: `${name}.worker.ts`,
|
|
1381
|
+
content: `import { defineVueMonoWorker } from '@atolljs/vue-island/worker';
|
|
1382
|
+
import ${P} from './${P}.vue';
|
|
1383
|
+
import ${c}Contract from './${name}.contract';
|
|
1384
|
+
|
|
1385
|
+
/** '${name}' \u2014 publishable Vue MFE, one app per worker. */
|
|
1386
|
+
export const worker = defineVueMonoWorker(${P}, { contract: ${c}Contract });
|
|
1387
|
+
`
|
|
1388
|
+
}
|
|
1389
|
+
];
|
|
1390
|
+
case "solid":
|
|
1391
|
+
return [
|
|
1392
|
+
{
|
|
1393
|
+
path: `${name}.worker.tsx`,
|
|
1394
|
+
content: `import { createSignal } from 'solid-js';
|
|
1395
|
+
import { defineSolidMonoWorker, emit } from '@atolljs/solid-island/worker';
|
|
1396
|
+
import ${c}Contract from './${name}.contract';
|
|
1397
|
+
|
|
1398
|
+
/** ${P} \u2014 a publishable Solid MFE rendered inside its own worker. */
|
|
1399
|
+
function ${P}(props: { label?: string }) {
|
|
1400
|
+
const [count, setCount] = createSignal(0);
|
|
1401
|
+
return (
|
|
1402
|
+
<button
|
|
1403
|
+
onClick={() => {
|
|
1404
|
+
const n = count() + 1;
|
|
1405
|
+
setCount(n);
|
|
1406
|
+
emit('incremented', { count: n, label: props.label ?? '${name}' });
|
|
1407
|
+
}}
|
|
1408
|
+
>
|
|
1409
|
+
{props.label ?? '${name}'}: {count()}
|
|
1410
|
+
</button>
|
|
1411
|
+
);
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
export const worker = defineSolidMonoWorker(${P}, { contract: ${c}Contract });
|
|
1415
|
+
`
|
|
1416
|
+
}
|
|
1417
|
+
];
|
|
1418
|
+
case "svelte":
|
|
1419
|
+
return [
|
|
1420
|
+
{
|
|
1421
|
+
path: `${P}.svelte`,
|
|
1422
|
+
content: `<script lang="ts">
|
|
1423
|
+
import { emit } from '@atolljs/islands/worker';
|
|
1424
|
+
|
|
1425
|
+
// Rendered inside the worker \u2014 emit() validates against the contract.
|
|
1426
|
+
let { label = '${name}' }: { label?: string } = $props();
|
|
1427
|
+
let count = $state(0);
|
|
1428
|
+
function increment() {
|
|
1429
|
+
count += 1;
|
|
1430
|
+
emit('incremented', { count, label });
|
|
1431
|
+
}
|
|
1432
|
+
</script>
|
|
1433
|
+
|
|
1434
|
+
<button onclick={increment}>{label}: {count}</button>
|
|
1435
|
+
`
|
|
1436
|
+
},
|
|
1437
|
+
{
|
|
1438
|
+
path: `${name}.worker.ts`,
|
|
1439
|
+
content: `import { defineSvelteMonoWorker } from '@atolljs/svelte-island/worker';
|
|
1440
|
+
import ${P} from './${P}.svelte';
|
|
1441
|
+
import ${c}Contract from './${name}.contract';
|
|
1442
|
+
|
|
1443
|
+
/** '${name}' \u2014 publishable Svelte MFE, one app per worker. */
|
|
1444
|
+
export const worker = defineSvelteMonoWorker(${P}, { contract: ${c}Contract });
|
|
1445
|
+
`
|
|
1446
|
+
}
|
|
1447
|
+
];
|
|
1448
|
+
case "angular":
|
|
1449
|
+
return [
|
|
1450
|
+
{
|
|
1451
|
+
path: `${name}.worker.ts`,
|
|
1452
|
+
content: `import '@angular/compiler'; // JIT \u2014 compiles the decorator template in the worker
|
|
1453
|
+
import { Component, input, output } from '@angular/core';
|
|
1454
|
+
import { defineAngularMonoWorker } from '@atolljs/angular-island/worker';
|
|
1455
|
+
import ${c}Contract from './${name}.contract';
|
|
1456
|
+
|
|
1457
|
+
/**
|
|
1458
|
+
* ${P}Component \u2014 a publishable Angular MFE. The contract is checked against
|
|
1459
|
+
* this class's input()/output() surface (IslandContract<IslandInputs<C>,
|
|
1460
|
+
* IslandEvents<C>>) \u2014 a contract missing 'label' or 'incremented' fails
|
|
1461
|
+
* typecheck HERE, not at the shell.
|
|
1462
|
+
*/
|
|
1463
|
+
@Component({
|
|
1464
|
+
selector: 'mfe-${name}',
|
|
1465
|
+
template: \`<button (click)="increment()">{{ label() }}: {{ count }}</button>\`,
|
|
1466
|
+
})
|
|
1467
|
+
export class ${P}Component {
|
|
1468
|
+
readonly label = input('${name}');
|
|
1469
|
+
readonly incremented = output<{ count: number; label: string }>();
|
|
1470
|
+
count = 0;
|
|
1471
|
+
increment(): void {
|
|
1472
|
+
this.count += 1;
|
|
1473
|
+
this.incremented.emit({ count: this.count, label: this.label() });
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
export const worker = defineAngularMonoWorker(${P}Component, {
|
|
1478
|
+
contract: ${c}Contract,
|
|
1479
|
+
});
|
|
1480
|
+
`
|
|
1481
|
+
}
|
|
1482
|
+
];
|
|
1483
|
+
default:
|
|
1484
|
+
return [];
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
function mfeFiles(name, fw, dir) {
|
|
1488
|
+
return [
|
|
1489
|
+
{ ...mfeContractFile(name, fw), path: `${dir}/${name}.contract.ts` },
|
|
1490
|
+
...mfeWorkerFiles(name, fw).map((f) => ({ ...f, path: `${dir}/${f.path}` }))
|
|
1491
|
+
];
|
|
1492
|
+
}
|
|
1493
|
+
function mfePublishConfig(name, fw, dir = "src/mfe") {
|
|
1494
|
+
const ext = ISLAND_EXT[fw] ?? "ts";
|
|
1495
|
+
const plugin = SCAFFOLDS[fw]?.vitePlugin;
|
|
1496
|
+
return `import { defineConfig } from 'vite';
|
|
1497
|
+
${plugin?.import_ ?? ""}
|
|
1498
|
+
/**
|
|
1499
|
+
* MFE publish build \u2014 emits ONE self-contained ESM worker bundle at
|
|
1500
|
+
* dist-mfe/${name}.worker.js (worker entry + framework runtime baked in;
|
|
1501
|
+
* every remote import would need its own CORS headers, so ship one file).
|
|
1502
|
+
*
|
|
1503
|
+
* Serve it cross-origin with:
|
|
1504
|
+
* Access-Control-Allow-Origin: <shell origin> // module workers fetch via CORS
|
|
1505
|
+
* Cache-Control: public, max-age=31536000, immutable
|
|
1506
|
+
* and version the URL per release \u2014 the contract's worker factory is the
|
|
1507
|
+
* cache key. docs/islands-remote.md covers the full matrix.
|
|
1508
|
+
*
|
|
1509
|
+
* npm run build:mfe \u2192 dist-mfe/${name}.worker.js
|
|
1510
|
+
* npm run preview:mfe \u2192 serves dist-mfe with the CORS header below \u2014
|
|
1511
|
+
* shells still need a same-origin shim entry
|
|
1512
|
+
* (blob: or a local re-export file) since worker
|
|
1513
|
+
* script URLs can't be cross-origin
|
|
1514
|
+
*/
|
|
1515
|
+
export default defineConfig({
|
|
1516
|
+
plugins: [
|
|
1517
|
+
{
|
|
1518
|
+
// The worker entry imports the contract, so the contract's own worker
|
|
1519
|
+
// factory lands in this bundle too \u2014 and its new URL(...) resolves
|
|
1520
|
+
// the PREVIOUS dist-mfe output, inlining it into its own successor
|
|
1521
|
+
// (the bundle grows every rebuild). The field is dead code here \u2014 a
|
|
1522
|
+
// worker never spawns itself \u2014 so stub the URL before vite's asset
|
|
1523
|
+
// plugin can resolve it.
|
|
1524
|
+
name: 'mfe:stub-worker-url',
|
|
1525
|
+
enforce: 'pre',
|
|
1526
|
+
transform(code: string, id: string) {
|
|
1527
|
+
if (!id.endsWith('${name}.contract.ts')) return null;
|
|
1528
|
+
return code.replace(
|
|
1529
|
+
/new URL\\(\\s*'[^']*'\\s*,\\s*import\\.meta\\.url,?\\s*\\)/,
|
|
1530
|
+
"'about:blank'",
|
|
1531
|
+
);
|
|
1532
|
+
},
|
|
1533
|
+
},
|
|
1534
|
+
${plugin?.call ?? ""}
|
|
1535
|
+
],
|
|
1536
|
+
// App builds define process.env.NODE_ENV automatically; a lib-mode worker
|
|
1537
|
+
// bundle doesn't, and framework dev/prod checks crash on a bare
|
|
1538
|
+
// \`process\` in a browser worker.
|
|
1539
|
+
define: { 'process.env.NODE_ENV': '"production"' },
|
|
1540
|
+
build: {
|
|
1541
|
+
outDir: 'dist-mfe',
|
|
1542
|
+
minify: true,
|
|
1543
|
+
lib: {
|
|
1544
|
+
entry: '${dir}/${name}.worker.${ext}',
|
|
1545
|
+
formats: ['es'],
|
|
1546
|
+
fileName: () => '${name}.worker.js',
|
|
1547
|
+
},
|
|
1548
|
+
},
|
|
1549
|
+
preview: {
|
|
1550
|
+
headers: { 'Access-Control-Allow-Origin': '*' },
|
|
1551
|
+
},
|
|
1552
|
+
});
|
|
1553
|
+
`;
|
|
1554
|
+
}
|
|
1555
|
+
function mfeUsage(name, fw, dir = "src/mfe") {
|
|
1556
|
+
const c = camel(name);
|
|
1557
|
+
const consume = {
|
|
1558
|
+
react: `import { islandComponent } from '@atolljs/react-island';
|
|
1559
|
+
import ${c}Contract from './${dir}/${name}.contract';
|
|
1560
|
+
const ${pascal(name)}Island = islandComponent(${c}Contract);
|
|
1561
|
+
<${pascal(name)}Island label="${name}" onEvent={(n, p) => console.log(n, p)} />`,
|
|
1562
|
+
vue: `import { islandComponent } from '@atolljs/vue-island';
|
|
1563
|
+
import ${c}Contract from './${dir}/${name}.contract';
|
|
1564
|
+
const ${pascal(name)}Island = islandComponent(${c}Contract);
|
|
1565
|
+
<${pascal(name)}Island v-bind="{ label: '${name}', onEvent }" />`,
|
|
1566
|
+
solid: `import { islandComponent } from '@atolljs/solid-island';
|
|
1567
|
+
import ${c}Contract from './${dir}/${name}.contract';
|
|
1568
|
+
const ${pascal(name)}Island = islandComponent(${c}Contract);
|
|
1569
|
+
<${pascal(name)}Island label="${name}" onEvent={(n, p) => console.log(n, p)} />`,
|
|
1570
|
+
svelte: `import { island } from '@atolljs/svelte-island';
|
|
1571
|
+
import ${c}Contract from './${dir}/${name}.contract';
|
|
1572
|
+
<div use:island={{ app: ${c}Contract, props: { label: '${name}' }, onEvent }} />`,
|
|
1573
|
+
angular: `import { islandComponent } from '@atolljs/angular-island';
|
|
1574
|
+
import ${c}Contract from './${dir}/${name}.contract';
|
|
1575
|
+
export const ${pascal(name)}Island = islandComponent({
|
|
1576
|
+
contract: ${c}Contract,
|
|
1577
|
+
selector: '${name}-island',
|
|
1578
|
+
});
|
|
1579
|
+
// <${name}-island [props]="{ label: '${name}' }" [onEvent]="onEvent" />`
|
|
1580
|
+
};
|
|
1581
|
+
return `shell side \u2014 mount through the contract, no framework import:
|
|
1582
|
+
${consume[fw] ?? ""}
|
|
1583
|
+
|
|
1584
|
+
publish \u2014 \`vite build --config vite.mfe.config.ts\` emits dist-mfe/${name}.worker.js
|
|
1585
|
+
deploy it behind Access-Control-Allow-Origin + a versioned URL, then point the
|
|
1586
|
+
contract's worker factory at it (docs/islands-remote.md).`;
|
|
1587
|
+
}
|
|
1588
|
+
function mfeScaffoldFiles(appName, fw) {
|
|
1589
|
+
const spec = SCAFFOLDS[fw];
|
|
1590
|
+
if (!spec) return [];
|
|
1591
|
+
const name = appName;
|
|
1592
|
+
const P = pascal(name);
|
|
1593
|
+
const files = [
|
|
1594
|
+
{
|
|
1595
|
+
path: "package.json",
|
|
1596
|
+
content: JSON.stringify(
|
|
1597
|
+
{
|
|
1598
|
+
name: appName,
|
|
1599
|
+
private: true,
|
|
1600
|
+
type: "module",
|
|
1601
|
+
// The package's PUBLIC surface is the contract module — shells
|
|
1602
|
+
// import it, never the worker entry. The worker ships as a
|
|
1603
|
+
// deployed asset (dist-mfe/) referenced by URL, not an import.
|
|
1604
|
+
exports: { ".": `./src/mfe/${name}.contract.ts` },
|
|
1605
|
+
files: ["src/mfe", "dist-mfe"],
|
|
1606
|
+
scripts: {
|
|
1607
|
+
dev: "vite",
|
|
1608
|
+
"build:mfe": "vite build --config vite.mfe.config.ts",
|
|
1609
|
+
"preview:mfe": "vite preview --config vite.mfe.config.ts"
|
|
1610
|
+
},
|
|
1611
|
+
dependencies: spec.deps,
|
|
1612
|
+
devDependencies: spec.devDeps
|
|
1613
|
+
},
|
|
1614
|
+
null,
|
|
1615
|
+
2
|
|
1616
|
+
) + "\n"
|
|
1617
|
+
},
|
|
1618
|
+
{ path: "tsconfig.json", content: JSON.stringify(TSCONFIGS[fw], null, 2) + "\n" },
|
|
1619
|
+
{ path: "vite.config.ts", content: viteConfig(fw) },
|
|
1620
|
+
{ path: "vite.mfe.config.ts", content: mfePublishConfig(name, fw) },
|
|
1621
|
+
{ path: ".npmrc", content: NPMRC },
|
|
1622
|
+
{ path: ".gitignore", content: GITIGNORE + "dist-mfe/\n" },
|
|
1623
|
+
{ path: "index.html", content: INDEX_HTML(appName, `src/main.${ISLAND_EXT[fw]}`) },
|
|
1624
|
+
...mfeFiles(name, fw, "src/mfe")
|
|
1625
|
+
];
|
|
1626
|
+
switch (fw) {
|
|
1627
|
+
case "react":
|
|
1628
|
+
files.push(
|
|
1629
|
+
{
|
|
1630
|
+
path: "src/main.tsx",
|
|
1631
|
+
content: `import { createRoot } from 'react-dom/client';
|
|
1632
|
+
import { App } from './App';
|
|
1633
|
+
|
|
1634
|
+
createRoot(document.getElementById('root')!).render(<App />);
|
|
1635
|
+
`
|
|
1636
|
+
},
|
|
1637
|
+
{
|
|
1638
|
+
path: "src/App.tsx",
|
|
1639
|
+
content: `import { islandComponent } from '@atolljs/react-island';
|
|
1640
|
+
import ${camel(name)}Contract from './mfe/${name}.contract';
|
|
1641
|
+
|
|
1642
|
+
const ${P}Island = islandComponent(${camel(name)}Contract);
|
|
1643
|
+
|
|
1644
|
+
/** Dev harness \u2014 this page mounts the MFE exactly like a consuming shell. */
|
|
1645
|
+
export function App() {
|
|
1646
|
+
return (
|
|
1647
|
+
<main style={{ fontFamily: 'system-ui', padding: '2rem' }}>
|
|
1648
|
+
<h1>${appName}</h1>
|
|
1649
|
+
<p>
|
|
1650
|
+
This island renders in a worker. Publish with{' '}
|
|
1651
|
+
<code>npm run build:mfe</code> \u2192 <code>dist-mfe/</code>.
|
|
1652
|
+
</p>
|
|
1653
|
+
<${P}Island
|
|
1654
|
+
label="${name}"
|
|
1655
|
+
onEvent={(name, payload) => console.log('mfe event:', name, payload)}
|
|
1656
|
+
/>
|
|
1657
|
+
</main>
|
|
1658
|
+
);
|
|
1659
|
+
}
|
|
1660
|
+
`
|
|
1661
|
+
}
|
|
1662
|
+
);
|
|
1663
|
+
break;
|
|
1664
|
+
case "vue":
|
|
1665
|
+
files.push(
|
|
1666
|
+
{ path: "src/env.d.ts", content: `declare module '*.vue';
|
|
1667
|
+
` },
|
|
1668
|
+
{
|
|
1669
|
+
path: "src/main.ts",
|
|
1670
|
+
content: `import { createApp } from 'vue';
|
|
1671
|
+
import App from './App.vue';
|
|
1672
|
+
|
|
1673
|
+
createApp(App).mount('#root');
|
|
1674
|
+
`
|
|
1675
|
+
},
|
|
1676
|
+
{
|
|
1677
|
+
path: "src/App.vue",
|
|
1678
|
+
content: `<script setup lang="ts">
|
|
1679
|
+
import { islandComponent } from '@atolljs/vue-island';
|
|
1680
|
+
import ${camel(name)}Contract from './mfe/${name}.contract';
|
|
1681
|
+
|
|
1682
|
+
const ${P}Island = islandComponent(${camel(name)}Contract);
|
|
1683
|
+
const onEvent = (name: string, payload: unknown) => console.log('mfe event:', name, payload);
|
|
1684
|
+
</script>
|
|
1685
|
+
|
|
1686
|
+
<template>
|
|
1687
|
+
<main style="font-family: system-ui; padding: 2rem">
|
|
1688
|
+
<h1>${appName}</h1>
|
|
1689
|
+
<p>This island renders in a worker \u2014 <code>npm run build:mfe</code> publishes it.</p>
|
|
1690
|
+
<${P}Island v-bind="{ label: '${name}', onEvent }" />
|
|
1691
|
+
</main>
|
|
1692
|
+
</template>
|
|
1693
|
+
`
|
|
1694
|
+
}
|
|
1695
|
+
);
|
|
1696
|
+
break;
|
|
1697
|
+
case "solid":
|
|
1698
|
+
files.push(
|
|
1699
|
+
{
|
|
1700
|
+
path: "src/main.tsx",
|
|
1701
|
+
content: `import { render } from 'solid-js/web';
|
|
1702
|
+
import { App } from './App';
|
|
1703
|
+
|
|
1704
|
+
render(() => <App />, document.getElementById('root')!);
|
|
1705
|
+
`
|
|
1706
|
+
},
|
|
1707
|
+
{
|
|
1708
|
+
path: "src/App.tsx",
|
|
1709
|
+
content: `import { islandComponent } from '@atolljs/solid-island';
|
|
1710
|
+
import ${camel(name)}Contract from './mfe/${name}.contract';
|
|
1711
|
+
|
|
1712
|
+
const ${P}Island = islandComponent(${camel(name)}Contract);
|
|
1713
|
+
|
|
1714
|
+
/** Dev harness \u2014 mounts the MFE exactly like a consuming shell. */
|
|
1715
|
+
export function App() {
|
|
1716
|
+
return (
|
|
1717
|
+
<main style={{ 'font-family': 'system-ui', padding: '2rem' }}>
|
|
1718
|
+
<h1>${appName}</h1>
|
|
1719
|
+
<p>
|
|
1720
|
+
This island renders in a worker \u2014 <code>npm run build:mfe</code> publishes it.
|
|
1721
|
+
</p>
|
|
1722
|
+
<${P}Island
|
|
1723
|
+
label="${name}"
|
|
1724
|
+
onEvent={(name, payload) => console.log('mfe event:', name, payload)}
|
|
1725
|
+
/>
|
|
1726
|
+
</main>
|
|
1727
|
+
);
|
|
1728
|
+
}
|
|
1729
|
+
`
|
|
1730
|
+
}
|
|
1731
|
+
);
|
|
1732
|
+
break;
|
|
1733
|
+
case "svelte":
|
|
1734
|
+
files.push(
|
|
1735
|
+
{
|
|
1736
|
+
path: "src/main.ts",
|
|
1737
|
+
content: `import { mount } from 'svelte';
|
|
1738
|
+
import App from './App.svelte';
|
|
1739
|
+
|
|
1740
|
+
mount(App, { target: document.getElementById('root')! });
|
|
1741
|
+
`
|
|
1742
|
+
},
|
|
1743
|
+
{
|
|
1744
|
+
path: "src/App.svelte",
|
|
1745
|
+
content: `<script lang="ts">
|
|
1746
|
+
import { island } from '@atolljs/svelte-island';
|
|
1747
|
+
import ${camel(name)}Contract from './mfe/${name}.contract';
|
|
1748
|
+
|
|
1749
|
+
const onEvent = (name: string, payload: unknown) => console.log('mfe event:', name, payload);
|
|
1750
|
+
</script>
|
|
1751
|
+
|
|
1752
|
+
<main style="font-family: system-ui; padding: 2rem">
|
|
1753
|
+
<h1>${appName}</h1>
|
|
1754
|
+
<p>This island renders in a worker \u2014 <code>npm run build:mfe</code> publishes it.</p>
|
|
1755
|
+
<div use:island={{ app: ${camel(name)}Contract, props: { label: '${name}' }, onEvent }} />
|
|
1756
|
+
</main>
|
|
1757
|
+
`
|
|
1758
|
+
}
|
|
1759
|
+
);
|
|
1760
|
+
break;
|
|
1761
|
+
}
|
|
1762
|
+
return files;
|
|
1763
|
+
}
|
|
1269
1764
|
function nestjsModuleFiles(name, dir, opts) {
|
|
1270
1765
|
const P = pascal(name);
|
|
1271
1766
|
const memImport = opts.mem ? `import { ${opts.mem.export_} } from '${opts.mem.import_}';
|
|
@@ -1699,6 +2194,57 @@ export async function register() {
|
|
|
1699
2194
|
`
|
|
1700
2195
|
};
|
|
1701
2196
|
}
|
|
2197
|
+
function devtoolsSetup(name, host, wsUrl) {
|
|
2198
|
+
if (host === "node") {
|
|
2199
|
+
return `import { initDevtools } from '@atolljs/devtools/node';
|
|
2200
|
+
|
|
2201
|
+
/**
|
|
2202
|
+
* Devtools sink \u2014 imported first from the entry so it's installed before any
|
|
2203
|
+
* pool spawns (worker INIT carries the flag at spawn time; late installs
|
|
2204
|
+
* leave existing workers silent).
|
|
2205
|
+
*
|
|
2206
|
+
* No-op unless the process was started with ATOLL_DEVTOOLS=1 \u2014 then events
|
|
2207
|
+
* stream over WebSocket to the standalone dashboard (\`atoll devtools\`,
|
|
2208
|
+
* http://127.0.0.1:4780). Node has no BroadcastChannel page context, so the
|
|
2209
|
+
* aggregate server is the transport.
|
|
2210
|
+
*/
|
|
2211
|
+
initDevtools({ session: { name: '${name}' } });
|
|
2212
|
+
`;
|
|
2213
|
+
}
|
|
2214
|
+
if (wsUrl) {
|
|
2215
|
+
return `import { initDevtools } from '@atolljs/devtools';
|
|
2216
|
+
|
|
2217
|
+
/**
|
|
2218
|
+
* Devtools sink \u2014 imported first from the app entry so it's installed before
|
|
2219
|
+
* any pool spawns (worker INIT carries the flag at spawn time; late installs
|
|
2220
|
+
* leave existing workers silent).
|
|
2221
|
+
*
|
|
2222
|
+
* No-op unless the page URL carries ?__atoll_devtools \u2014 then events stream
|
|
2223
|
+
* over WebSocket to the standalone aggregate dashboard (\`atoll devtools\`,
|
|
2224
|
+
* http://127.0.0.1:4780). This app's dev server doesn't mount /__atoll/, so
|
|
2225
|
+
* the overlay is off and the aggregate server is the viewer.
|
|
2226
|
+
*/
|
|
2227
|
+
initDevtools({
|
|
2228
|
+
session: { name: '${name}' },
|
|
2229
|
+
overlay: false,
|
|
2230
|
+
url: '${wsUrl}',
|
|
2231
|
+
});
|
|
2232
|
+
`;
|
|
2233
|
+
}
|
|
2234
|
+
return `import { initDevtools } from '@atolljs/devtools';
|
|
2235
|
+
|
|
2236
|
+
/**
|
|
2237
|
+
* Devtools sink \u2014 imported first from the app entry so it's installed before
|
|
2238
|
+
* any pool spawns (worker INIT carries the flag at spawn time; late installs
|
|
2239
|
+
* leave existing workers silent).
|
|
2240
|
+
*
|
|
2241
|
+
* No-op unless the page URL carries ?__atoll_devtools \u2014 then events stream
|
|
2242
|
+
* over BroadcastChannel on the app's own origin and a flyout overlay mounts.
|
|
2243
|
+
* /__atoll/ itself is served by the @atolljs/vite plugin (plugins: [atoll()]).
|
|
2244
|
+
*/
|
|
2245
|
+
initDevtools({ session: { name: '${name}' } });
|
|
2246
|
+
`;
|
|
2247
|
+
}
|
|
1702
2248
|
|
|
1703
2249
|
// src/commands.ts
|
|
1704
2250
|
var NODE_HOSTS = ["node", "nestjs"];
|
|
@@ -1790,25 +2336,29 @@ var ADD_KINDS = [
|
|
|
1790
2336
|
"worker",
|
|
1791
2337
|
"memory",
|
|
1792
2338
|
"island",
|
|
2339
|
+
"mfe",
|
|
1793
2340
|
"service",
|
|
1794
2341
|
"method",
|
|
1795
2342
|
"module",
|
|
1796
2343
|
"housed",
|
|
1797
2344
|
"route",
|
|
1798
2345
|
"component",
|
|
1799
|
-
"instrumentation"
|
|
2346
|
+
"instrumentation",
|
|
2347
|
+
"devtools"
|
|
1800
2348
|
];
|
|
1801
2349
|
var KIND_SCOPE = {
|
|
1802
2350
|
worker: null,
|
|
1803
2351
|
memory: null,
|
|
1804
2352
|
island: ISLAND_FRAMEWORKS,
|
|
2353
|
+
mfe: ISLAND_FRAMEWORKS,
|
|
1805
2354
|
service: ["nestjs"],
|
|
1806
2355
|
method: ["nestjs"],
|
|
1807
2356
|
module: ["nestjs"],
|
|
1808
2357
|
housed: ["nestjs"],
|
|
1809
2358
|
route: ["nextjs"],
|
|
1810
2359
|
component: ["nextjs"],
|
|
1811
|
-
instrumentation: ["nextjs"]
|
|
2360
|
+
instrumentation: ["nextjs"],
|
|
2361
|
+
devtools: null
|
|
1812
2362
|
};
|
|
1813
2363
|
var KIND_VARIANTS = {
|
|
1814
2364
|
island: ["facade", "mono"],
|
|
@@ -1902,7 +2452,7 @@ async function runAdd(ctx) {
|
|
|
1902
2452
|
} else {
|
|
1903
2453
|
nameArg = rest[1];
|
|
1904
2454
|
}
|
|
1905
|
-
const name = await pickName(ctx, kind, nameArg);
|
|
2455
|
+
const name = kind === "devtools" ? nameArg ?? flag(ctx.args, "name") ?? project.pkg?.name?.split("/").pop() ?? "app" : await pickName(ctx, kind, nameArg);
|
|
1906
2456
|
if (!name) return 1;
|
|
1907
2457
|
const force = { force: hasFlag(ctx.args, "force") };
|
|
1908
2458
|
const atollDir = flag(ctx.args, "dir") ?? rel(join3(project.srcDir, "atoll"));
|
|
@@ -1974,6 +2524,29 @@ bind it: pass ${ctx.io.fmt.accent(`${c}Memory`)} as ${ctx.io.fmt.accent("sharedM
|
|
|
1974
2524
|
}
|
|
1975
2525
|
return 0;
|
|
1976
2526
|
}
|
|
2527
|
+
case "mfe": {
|
|
2528
|
+
const dir = flag(ctx.args, "dir") ?? rel(join3(project.srcDir, "mfe"));
|
|
2529
|
+
const files = mfeFiles(name, framework, dir);
|
|
2530
|
+
const publishCfg = rel(join3(project.root, "vite.mfe.config.ts"));
|
|
2531
|
+
const dirFromRoot = relative(project.root, join3(ctx.cwd, dir)).replaceAll("\\", "/");
|
|
2532
|
+
await writeTree(
|
|
2533
|
+
ctx.io,
|
|
2534
|
+
ctx.cwd,
|
|
2535
|
+
[
|
|
2536
|
+
...files,
|
|
2537
|
+
{ path: publishCfg, content: mfePublishConfig(name, framework, dirFromRoot) }
|
|
2538
|
+
],
|
|
2539
|
+
force
|
|
2540
|
+
);
|
|
2541
|
+
if (missingDeps(project.pkg, ["vite"]).length) {
|
|
2542
|
+
ctx.io.warn(
|
|
2543
|
+
`vite isn't installed \u2014 the publish build needs it: ${ctx.io.fmt.accent(pmInstallCmd(project.pm, ["-D", "vite"]))}`
|
|
2544
|
+
);
|
|
2545
|
+
}
|
|
2546
|
+
ctx.io.print(ctx.io.fmt.strong("\nwire it:"));
|
|
2547
|
+
ctx.io.print(mfeUsage(name, framework, dir));
|
|
2548
|
+
return 0;
|
|
2549
|
+
}
|
|
1977
2550
|
case "service": {
|
|
1978
2551
|
const dir = atollDir;
|
|
1979
2552
|
mkdirSync2(join3(ctx.cwd, dir), { recursive: true });
|
|
@@ -2058,6 +2631,45 @@ pools warm at boot \u2014 extend register() as more routes appear.`
|
|
|
2058
2631
|
);
|
|
2059
2632
|
return 0;
|
|
2060
2633
|
}
|
|
2634
|
+
case "devtools": {
|
|
2635
|
+
const host = hostOf(framework);
|
|
2636
|
+
const wsUrl = host === "browser" && framework === "angular" ? "ws://127.0.0.1:4780/events" : void 0;
|
|
2637
|
+
const file = `${atollDir}/devtools.ts`;
|
|
2638
|
+
await writeTree(
|
|
2639
|
+
ctx.io,
|
|
2640
|
+
ctx.cwd,
|
|
2641
|
+
[{ path: file, content: devtoolsSetup(name, host, wsUrl) }],
|
|
2642
|
+
force
|
|
2643
|
+
);
|
|
2644
|
+
if (missingDeps(project.pkg, ["@atolljs/devtools"]).length) {
|
|
2645
|
+
await offerInstall(ctx, project, ["-D", "@atolljs/devtools"]);
|
|
2646
|
+
}
|
|
2647
|
+
const srcRel = rel(project.srcDir);
|
|
2648
|
+
const spec = file.startsWith(`${srcRel}/`) ? `./${file.slice(srcRel.length + 1).replace(/\.ts$/, "")}` : `./${file.replace(/\.ts$/, "")}`;
|
|
2649
|
+
ctx.io.print("");
|
|
2650
|
+
ctx.io.print(ctx.io.fmt.strong("wire it:"));
|
|
2651
|
+
ctx.io.print(` import the module first in your entry \u2014 before any pool spawns:`);
|
|
2652
|
+
ctx.io.print(ctx.io.fmt.accent(` import '${spec}';`));
|
|
2653
|
+
if (host === "node") {
|
|
2654
|
+
ctx.io.print(
|
|
2655
|
+
` ${ctx.io.fmt.accent("ATOLL_DEVTOOLS=1")} starts streaming \xB7 ${ctx.io.fmt.accent("atoll devtools")} serves the dashboard`
|
|
2656
|
+
);
|
|
2657
|
+
} else if (wsUrl) {
|
|
2658
|
+
ctx.io.print(
|
|
2659
|
+
` ${ctx.io.fmt.accent("atoll devtools")} serves the aggregate dashboard \xB7 open the app with ${ctx.io.fmt.accent("?__atoll_devtools")}`
|
|
2660
|
+
);
|
|
2661
|
+
} else {
|
|
2662
|
+
ctx.io.print(
|
|
2663
|
+
` open the app with ${ctx.io.fmt.accent("?__atoll_devtools")} \u2014 the flyout mounts and /__atoll/ serves the dashboard`
|
|
2664
|
+
);
|
|
2665
|
+
ctx.io.print(
|
|
2666
|
+
ctx.io.fmt.dim(
|
|
2667
|
+
` /__atoll/ needs the @atolljs/vite plugin (plugins: [atoll()]) \u2014 no plugin? run \`atoll devtools\` + pass transport: 'websocket'`
|
|
2668
|
+
)
|
|
2669
|
+
);
|
|
2670
|
+
}
|
|
2671
|
+
return 0;
|
|
2672
|
+
}
|
|
2061
2673
|
default:
|
|
2062
2674
|
ctx.io.error(`unhandled add kind ${kind}`);
|
|
2063
2675
|
return 1;
|
|
@@ -2087,7 +2699,12 @@ async function runCreate(ctx) {
|
|
|
2087
2699
|
return 1;
|
|
2088
2700
|
}
|
|
2089
2701
|
mkdirSync2(target, { recursive: true });
|
|
2090
|
-
const
|
|
2702
|
+
const mfe = hasFlag(ctx.args, "mfe");
|
|
2703
|
+
if (mfe && fw === "node") {
|
|
2704
|
+
ctx.io.error("--mfe is a browser-island shape \u2014 pick react|vue|solid|svelte");
|
|
2705
|
+
return 1;
|
|
2706
|
+
}
|
|
2707
|
+
const files = fw === "node" ? nodeScaffoldFiles(dirName) : mfe ? mfeScaffoldFiles(dirName, fw) : scaffoldFiles(dirName, fw);
|
|
2091
2708
|
await writeTree(ctx.io, target, files, { force: true });
|
|
2092
2709
|
const written = JSON.parse(readFileSync2(join3(target, "package.json"), "utf8"));
|
|
2093
2710
|
const allDeps = [
|
|
@@ -2104,8 +2721,36 @@ async function runCreate(ctx) {
|
|
|
2104
2721
|
);
|
|
2105
2722
|
ctx.io.print("");
|
|
2106
2723
|
ctx.io.print(`done \u2014 ${ctx.io.fmt.accent(`cd ${dirName} && ${pm} run dev`)}`);
|
|
2724
|
+
if (mfe) {
|
|
2725
|
+
ctx.io.print(
|
|
2726
|
+
` ${ctx.io.fmt.accent("npm run build:mfe")} \u2192 dist-mfe/${dirName}.worker.js \u2014 publish behind CORS + a versioned URL`
|
|
2727
|
+
);
|
|
2728
|
+
ctx.io.print(
|
|
2729
|
+
` ${ctx.io.fmt.accent("npm run preview:mfe")} serves it cross-origin; shells mount the contract`
|
|
2730
|
+
);
|
|
2731
|
+
}
|
|
2107
2732
|
return 0;
|
|
2108
2733
|
}
|
|
2734
|
+
async function runDevtools(ctx) {
|
|
2735
|
+
const { io } = ctx;
|
|
2736
|
+
const spec = "@atolljs/devtools/server";
|
|
2737
|
+
const mod = await import(spec).catch(async () => {
|
|
2738
|
+
const local = new URL("../../devtools/dist/server.js", import.meta.url).href;
|
|
2739
|
+
return import(local).catch(() => null);
|
|
2740
|
+
});
|
|
2741
|
+
if (!mod?.createDevtoolsServer) {
|
|
2742
|
+
io.error(`@atolljs/devtools isn't installed \u2014 ${io.fmt.accent("npm i -D @atolljs/devtools")}`);
|
|
2743
|
+
return 1;
|
|
2744
|
+
}
|
|
2745
|
+
const port = Number(flag(ctx.args, "port") ?? 4780);
|
|
2746
|
+
const server = await mod.createDevtoolsServer({ port: Number.isFinite(port) ? port : 4780 });
|
|
2747
|
+
io.print(`atoll devtools \u2192 ${io.fmt.accent(server.url)}`);
|
|
2748
|
+
io.print(io.fmt.dim(" ingest endpoint: " + server.url.replace("http://", "ws://") + "/events"));
|
|
2749
|
+
io.print(io.fmt.dim(" in the app: connectDevtools() from '@atolljs/devtools'"));
|
|
2750
|
+
io.warn("serving \u2014 Ctrl+C to stop");
|
|
2751
|
+
return new Promise(() => {
|
|
2752
|
+
});
|
|
2753
|
+
}
|
|
2109
2754
|
function runDoctor(ctx) {
|
|
2110
2755
|
const project = detectProject(ctx.cwd);
|
|
2111
2756
|
const fix = hasFlag(ctx.args, "fix");
|
|
@@ -2195,9 +2840,10 @@ function runDoctor(ctx) {
|
|
|
2195
2840
|
// src/cli.ts
|
|
2196
2841
|
var HELP = (fmt) => `${fmt.strong("atoll")} \u2014 bootstrap multithreading ${fmt.dim("(experimental)")}
|
|
2197
2842
|
|
|
2198
|
-
${fmt.accent("atoll new <dir>")} [--framework react|vue|solid|svelte|node] [--pm npm]
|
|
2843
|
+
${fmt.accent("atoll new <dir>")} [--framework react|vue|solid|svelte|node] [--mfe] [--pm npm]
|
|
2199
2844
|
Scaffold a fresh app \u2014 vite shell + a worker-rendered island,
|
|
2200
|
-
or a tsx Node service with a pooled worker.
|
|
2845
|
+
or a tsx Node service with a pooled worker. --mfe scaffolds a
|
|
2846
|
+
publishable MFE package (contract + worker + bundle build).
|
|
2201
2847
|
|
|
2202
2848
|
${fmt.accent("atoll init")} [--name app] [--force]
|
|
2203
2849
|
Wire Atoll into the current project: hardened .npmrc, package
|
|
@@ -2207,6 +2853,8 @@ var HELP = (fmt) => `${fmt.strong("atoll")} \u2014 bootstrap multithreading ${fm
|
|
|
2207
2853
|
worker pool + typed client (+ framework bindings on UI projects)
|
|
2208
2854
|
memory shared-memory contract
|
|
2209
2855
|
island worker-rendered UI \u2014 facade (contract + lazy import) | mono
|
|
2856
|
+
mfe publishable micro-frontend \u2014 island contract + worker +
|
|
2857
|
+
vite.mfe.config.ts (build a self-contained remote bundle)
|
|
2210
2858
|
service nestjs: @AtollService class facade + pool module + worker
|
|
2211
2859
|
method nestjs: @AtollTask method-level offload
|
|
2212
2860
|
module nestjs: registerPool boundary module + worker entry
|
|
@@ -2214,6 +2862,8 @@ var HELP = (fmt) => `${fmt.strong("atoll")} \u2014 bootstrap multithreading ${fm
|
|
|
2214
2862
|
route nextjs: api route pool \u2014 task | client variants
|
|
2215
2863
|
component nextjs: 'use client' hooks component over a pool
|
|
2216
2864
|
instrumentation nextjs: server bootstrap that warms route pools
|
|
2865
|
+
devtools observability \u2014 init module + @atolljs/devtools install
|
|
2866
|
+
(browser: ?__atoll_devtools + /__atoll/ \xB7 node: ATOLL_DEVTOOLS=1)
|
|
2217
2867
|
|
|
2218
2868
|
Frameworks: react vue solid svelte angular nestjs nextjs node
|
|
2219
2869
|
[--dir <path>] [--memory <name>|--no-memory] [--variant <v>] [--force]
|
|
@@ -2222,6 +2872,10 @@ var HELP = (fmt) => `${fmt.strong("atoll")} \u2014 bootstrap multithreading ${fm
|
|
|
2222
2872
|
Check the setup \u2014 deps, lockfile, .npmrc policy, bundler-detectable
|
|
2223
2873
|
worker entries, COOP/COEP headers. --fix writes a hardened .npmrc.
|
|
2224
2874
|
|
|
2875
|
+
${fmt.accent("atoll devtools")} [--port 4780]
|
|
2876
|
+
Serve the local devtools dashboard \u2014 pools, task timings, shared-memory
|
|
2877
|
+
writes, island traffic. Apps connect via connectDevtools().
|
|
2878
|
+
|
|
2225
2879
|
${fmt.dim(`flags: --yes/-y accepts defaults, --force overwrites existing files,
|
|
2226
2880
|
--no-install skips dependency installation`)}
|
|
2227
2881
|
`;
|
|
@@ -2242,6 +2896,8 @@ async function runCli(argv, io, cwd) {
|
|
|
2242
2896
|
return await runAdd(ctx);
|
|
2243
2897
|
case "doctor":
|
|
2244
2898
|
return runDoctor(ctx);
|
|
2899
|
+
case "devtools":
|
|
2900
|
+
return await runDevtools(ctx);
|
|
2245
2901
|
case "help":
|
|
2246
2902
|
case void 0:
|
|
2247
2903
|
io.print(HELP(io.fmt));
|