@atolljs/cli 0.1.4 → 0.1.6

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 CHANGED
@@ -20,10 +20,15 @@ builds it automatically before staging.
20
20
  ## Commands
21
21
 
22
22
  ```bash
23
- atoll new <dir> [--framework react|vue|solid|svelte|node] [--pm npm]
24
- Scaffold a fresh app: vite shell with COOP/COEP headers, a
25
- worker-rendered counter island, hardened .npmrc — or a tsx Node
26
- service with a pooled worker.
23
+ atoll new <dir> [--framework react|vue|solid|svelte|node] [--mfe] [--pm npm]
24
+ Scaffold a fresh app: vite shell with COOP/COEP headers, the
25
+ @atolljs/vite plugin (worker entries are dev-bundled — no
26
+ fast-refresh in workers), a worker-rendered counter island,
27
+ hardened .npmrc — or a tsx Node service with a pooled worker.
28
+ --mfe instead scaffolds a publishable micro-frontend package:
29
+ contract + worker entry + vite.mfe.config.ts (emits a
30
+ self-contained remote bundle) + a dev harness mounting the
31
+ island through the contract.
27
32
 
28
33
  atoll init
29
34
  Wire Atoll into the current project: .npmrc, installs the @atolljs/*
@@ -33,6 +38,10 @@ atoll init
33
38
  atoll add worker <name> pooled worker + typed client (browser or node)
34
39
  atoll add memory <name> shared-memory contract module
35
40
  atoll add island <name> worker-rendered component for your framework
41
+ atoll add mfe <name> publishable micro-frontend — framework-free
42
+ contract (defineIslandContract) + worker entry
43
+ attaching it + vite.mfe.config.ts publish build
44
+ → dist-mfe/<name>.worker.js behind CORS
36
45
 
37
46
  atoll doctor [--fix] verify deps, lockfile, .npmrc policy,
38
47
  worker-entry detectability, COOP/COEP headers
package/dist/cli.js CHANGED
@@ -577,22 +577,27 @@ var HEADERS = ` // SharedArrayBuffer is gated behind cross-origin isolation \u2
577
577
  var SCAFFOLDS = {
578
578
  react: {
579
579
  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" },
580
- 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" },
581
- vitePlugin: { pkg: "@vitejs/plugin-react", import_: `import react from '@vitejs/plugin-react';`, call: "react()" }
580
+ devDeps: { vite: "^8.3.1", typescript: "^7.0.2", "@vitejs/plugin-react": "^6.1.1", "@atolljs/vite": "^0.1.0", "@types/react": "^19.3.0", "@types/react-dom": "^19.3.0" },
581
+ // `exclude` replaces the plugin's default /node_modules/ — keep it, and
582
+ // add *.worker.*: fast-refresh injects `/@react-refresh` (which reads
583
+ // `window` unguarded) into every transformed module, including worker
584
+ // entries served as ?worker_file — that crashes the worker in dev.
585
+ vitePlugin: { pkg: "@vitejs/plugin-react", import_: `import react from '@vitejs/plugin-react';`, call: "react({ exclude: [/\\/node_modules\\//, /\\.worker\\./] })" }
582
586
  },
583
587
  vue: {
584
588
  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" },
585
- devDeps: { vite: "^8.3.1", typescript: "^7.0.2", "@vitejs/plugin-vue": "^6.0.9" },
589
+ devDeps: { vite: "^8.3.1", typescript: "^7.0.2", "@vitejs/plugin-vue": "^6.0.9", "@atolljs/vite": "^0.1.0" },
586
590
  vitePlugin: { pkg: "@vitejs/plugin-vue", import_: `import vue from '@vitejs/plugin-vue';`, call: "vue()" }
587
591
  },
588
592
  solid: {
589
593
  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" },
590
- devDeps: { vite: "^8.3.1", typescript: "^7.0.2", "vite-plugin-solid": "^2.11.14" },
594
+ devDeps: { vite: "^8.3.1", typescript: "^7.0.2", "vite-plugin-solid": "^2.11.14", "@atolljs/vite": "^0.1.0" },
595
+ atollCall: `atoll({ jsxImportSource: 'solid-js' })`,
591
596
  vitePlugin: { pkg: "vite-plugin-solid", import_: `import solid from 'vite-plugin-solid';`, call: "solid()" }
592
597
  },
593
598
  svelte: {
594
599
  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" },
595
- devDeps: { vite: "^8.3.1", typescript: "^7.0.2", "@sveltejs/vite-plugin-svelte": "^7.3.1" },
600
+ devDeps: { vite: "^8.3.1", typescript: "^7.0.2", "@sveltejs/vite-plugin-svelte": "^7.3.1", "@atolljs/vite": "^0.1.0" },
596
601
  vitePlugin: { pkg: "@sveltejs/vite-plugin-svelte", import_: `import { svelte } from '@sveltejs/vite-plugin-svelte';`, call: "svelte()" }
597
602
  }
598
603
  };
@@ -695,9 +700,13 @@ function viteConfig(fw) {
695
700
  const plugin = spec.vitePlugin;
696
701
  return `import { defineConfig } from 'vite';
697
702
  ${plugin?.import_ ?? ""}
703
+ import atoll from '@atolljs/vite';
698
704
 
699
705
  export default defineConfig({
700
- plugins: [${plugin?.call ?? ""}],
706
+ // atoll() serves worker entries as esbuild bundles (rebuild + respawn on
707
+ // change) \u2014 browser-plugin transforms like react fast-refresh can't leak
708
+ // into worker code.
709
+ plugins: [${plugin?.call ?? ""}, ${spec.atollCall ?? "atoll()"}],
701
710
  ${plugin ? ` // Worker bundles run their own rolldown pass \u2014 fresh plugin instances
702
711
  // let the worker entry compile framework files too (worker-island apps).
703
712
  worker: { plugins: () => [${plugin.call}] },
@@ -1257,6 +1266,501 @@ const ${P}Island = lazyIsland(() => import('./islands/${name}.island'));
1257
1266
  return "";
1258
1267
  }
1259
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
+ }
1260
1764
  function nestjsModuleFiles(name, dir, opts) {
1261
1765
  const P = pascal(name);
1262
1766
  const memImport = opts.mem ? `import { ${opts.mem.export_} } from '${opts.mem.import_}';
@@ -1781,6 +2285,7 @@ var ADD_KINDS = [
1781
2285
  "worker",
1782
2286
  "memory",
1783
2287
  "island",
2288
+ "mfe",
1784
2289
  "service",
1785
2290
  "method",
1786
2291
  "module",
@@ -1793,6 +2298,7 @@ var KIND_SCOPE = {
1793
2298
  worker: null,
1794
2299
  memory: null,
1795
2300
  island: ISLAND_FRAMEWORKS,
2301
+ mfe: ISLAND_FRAMEWORKS,
1796
2302
  service: ["nestjs"],
1797
2303
  method: ["nestjs"],
1798
2304
  module: ["nestjs"],
@@ -1965,6 +2471,29 @@ bind it: pass ${ctx.io.fmt.accent(`${c}Memory`)} as ${ctx.io.fmt.accent("sharedM
1965
2471
  }
1966
2472
  return 0;
1967
2473
  }
2474
+ case "mfe": {
2475
+ const dir = flag(ctx.args, "dir") ?? rel(join3(project.srcDir, "mfe"));
2476
+ const files = mfeFiles(name, framework, dir);
2477
+ const publishCfg = rel(join3(project.root, "vite.mfe.config.ts"));
2478
+ const dirFromRoot = relative(project.root, join3(ctx.cwd, dir)).replaceAll("\\", "/");
2479
+ await writeTree(
2480
+ ctx.io,
2481
+ ctx.cwd,
2482
+ [
2483
+ ...files,
2484
+ { path: publishCfg, content: mfePublishConfig(name, framework, dirFromRoot) }
2485
+ ],
2486
+ force
2487
+ );
2488
+ if (missingDeps(project.pkg, ["vite"]).length) {
2489
+ ctx.io.warn(
2490
+ `vite isn't installed \u2014 the publish build needs it: ${ctx.io.fmt.accent(pmInstallCmd(project.pm, ["-D", "vite"]))}`
2491
+ );
2492
+ }
2493
+ ctx.io.print(ctx.io.fmt.strong("\nwire it:"));
2494
+ ctx.io.print(mfeUsage(name, framework, dir));
2495
+ return 0;
2496
+ }
1968
2497
  case "service": {
1969
2498
  const dir = atollDir;
1970
2499
  mkdirSync2(join3(ctx.cwd, dir), { recursive: true });
@@ -2078,7 +2607,12 @@ async function runCreate(ctx) {
2078
2607
  return 1;
2079
2608
  }
2080
2609
  mkdirSync2(target, { recursive: true });
2081
- const files = fw === "node" ? nodeScaffoldFiles(dirName) : scaffoldFiles(dirName, fw);
2610
+ const mfe = hasFlag(ctx.args, "mfe");
2611
+ if (mfe && fw === "node") {
2612
+ ctx.io.error("--mfe is a browser-island shape \u2014 pick react|vue|solid|svelte");
2613
+ return 1;
2614
+ }
2615
+ const files = fw === "node" ? nodeScaffoldFiles(dirName) : mfe ? mfeScaffoldFiles(dirName, fw) : scaffoldFiles(dirName, fw);
2082
2616
  await writeTree(ctx.io, target, files, { force: true });
2083
2617
  const written = JSON.parse(readFileSync2(join3(target, "package.json"), "utf8"));
2084
2618
  const allDeps = [
@@ -2095,6 +2629,14 @@ async function runCreate(ctx) {
2095
2629
  );
2096
2630
  ctx.io.print("");
2097
2631
  ctx.io.print(`done \u2014 ${ctx.io.fmt.accent(`cd ${dirName} && ${pm} run dev`)}`);
2632
+ if (mfe) {
2633
+ ctx.io.print(
2634
+ ` ${ctx.io.fmt.accent("npm run build:mfe")} \u2192 dist-mfe/${dirName}.worker.js \u2014 publish behind CORS + a versioned URL`
2635
+ );
2636
+ ctx.io.print(
2637
+ ` ${ctx.io.fmt.accent("npm run preview:mfe")} serves it cross-origin; shells mount the contract`
2638
+ );
2639
+ }
2098
2640
  return 0;
2099
2641
  }
2100
2642
  function runDoctor(ctx) {
@@ -2186,9 +2728,10 @@ function runDoctor(ctx) {
2186
2728
  // src/cli.ts
2187
2729
  var HELP = (fmt) => `${fmt.strong("atoll")} \u2014 bootstrap multithreading ${fmt.dim("(experimental)")}
2188
2730
 
2189
- ${fmt.accent("atoll new <dir>")} [--framework react|vue|solid|svelte|node] [--pm npm]
2731
+ ${fmt.accent("atoll new <dir>")} [--framework react|vue|solid|svelte|node] [--mfe] [--pm npm]
2190
2732
  Scaffold a fresh app \u2014 vite shell + a worker-rendered island,
2191
- or a tsx Node service with a pooled worker.
2733
+ or a tsx Node service with a pooled worker. --mfe scaffolds a
2734
+ publishable MFE package (contract + worker + bundle build).
2192
2735
 
2193
2736
  ${fmt.accent("atoll init")} [--name app] [--force]
2194
2737
  Wire Atoll into the current project: hardened .npmrc, package
@@ -2198,6 +2741,8 @@ var HELP = (fmt) => `${fmt.strong("atoll")} \u2014 bootstrap multithreading ${fm
2198
2741
  worker pool + typed client (+ framework bindings on UI projects)
2199
2742
  memory shared-memory contract
2200
2743
  island worker-rendered UI \u2014 facade (contract + lazy import) | mono
2744
+ mfe publishable micro-frontend \u2014 island contract + worker +
2745
+ vite.mfe.config.ts (build a self-contained remote bundle)
2201
2746
  service nestjs: @AtollService class facade + pool module + worker
2202
2747
  method nestjs: @AtollTask method-level offload
2203
2748
  module nestjs: registerPool boundary module + worker entry
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atolljs/cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
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",