@atolljs/cli 0.1.5 → 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 +9 -1
- package/dist/cli.js +539 -3
- package/package.json +1 -1
- package/src/cli.ts +5 -2
- package/src/commands.ts +48 -1
- package/src/templates.ts +522 -0
package/README.md
CHANGED
|
@@ -20,11 +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]
|
|
23
|
+
atoll new <dir> [--framework react|vue|solid|svelte|node] [--mfe] [--pm npm]
|
|
24
24
|
Scaffold a fresh app: vite shell with COOP/COEP headers, the
|
|
25
25
|
@atolljs/vite plugin (worker entries are dev-bundled — no
|
|
26
26
|
fast-refresh in workers), a worker-rendered counter island,
|
|
27
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.
|
|
28
32
|
|
|
29
33
|
atoll init
|
|
30
34
|
Wire Atoll into the current project: .npmrc, installs the @atolljs/*
|
|
@@ -34,6 +38,10 @@ atoll init
|
|
|
34
38
|
atoll add worker <name> pooled worker + typed client (browser or node)
|
|
35
39
|
atoll add memory <name> shared-memory contract module
|
|
36
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
|
|
37
45
|
|
|
38
46
|
atoll doctor [--fix] verify deps, lockfile, .npmrc policy,
|
|
39
47
|
worker-entry detectability, COOP/COEP headers
|
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_}';
|
|
@@ -1790,6 +2285,7 @@ var ADD_KINDS = [
|
|
|
1790
2285
|
"worker",
|
|
1791
2286
|
"memory",
|
|
1792
2287
|
"island",
|
|
2288
|
+
"mfe",
|
|
1793
2289
|
"service",
|
|
1794
2290
|
"method",
|
|
1795
2291
|
"module",
|
|
@@ -1802,6 +2298,7 @@ var KIND_SCOPE = {
|
|
|
1802
2298
|
worker: null,
|
|
1803
2299
|
memory: null,
|
|
1804
2300
|
island: ISLAND_FRAMEWORKS,
|
|
2301
|
+
mfe: ISLAND_FRAMEWORKS,
|
|
1805
2302
|
service: ["nestjs"],
|
|
1806
2303
|
method: ["nestjs"],
|
|
1807
2304
|
module: ["nestjs"],
|
|
@@ -1974,6 +2471,29 @@ bind it: pass ${ctx.io.fmt.accent(`${c}Memory`)} as ${ctx.io.fmt.accent("sharedM
|
|
|
1974
2471
|
}
|
|
1975
2472
|
return 0;
|
|
1976
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
|
+
}
|
|
1977
2497
|
case "service": {
|
|
1978
2498
|
const dir = atollDir;
|
|
1979
2499
|
mkdirSync2(join3(ctx.cwd, dir), { recursive: true });
|
|
@@ -2087,7 +2607,12 @@ async function runCreate(ctx) {
|
|
|
2087
2607
|
return 1;
|
|
2088
2608
|
}
|
|
2089
2609
|
mkdirSync2(target, { recursive: true });
|
|
2090
|
-
const
|
|
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);
|
|
2091
2616
|
await writeTree(ctx.io, target, files, { force: true });
|
|
2092
2617
|
const written = JSON.parse(readFileSync2(join3(target, "package.json"), "utf8"));
|
|
2093
2618
|
const allDeps = [
|
|
@@ -2104,6 +2629,14 @@ async function runCreate(ctx) {
|
|
|
2104
2629
|
);
|
|
2105
2630
|
ctx.io.print("");
|
|
2106
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
|
+
}
|
|
2107
2640
|
return 0;
|
|
2108
2641
|
}
|
|
2109
2642
|
function runDoctor(ctx) {
|
|
@@ -2195,9 +2728,10 @@ function runDoctor(ctx) {
|
|
|
2195
2728
|
// src/cli.ts
|
|
2196
2729
|
var HELP = (fmt) => `${fmt.strong("atoll")} \u2014 bootstrap multithreading ${fmt.dim("(experimental)")}
|
|
2197
2730
|
|
|
2198
|
-
${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]
|
|
2199
2732
|
Scaffold a fresh app \u2014 vite shell + a worker-rendered island,
|
|
2200
|
-
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).
|
|
2201
2735
|
|
|
2202
2736
|
${fmt.accent("atoll init")} [--name app] [--force]
|
|
2203
2737
|
Wire Atoll into the current project: hardened .npmrc, package
|
|
@@ -2207,6 +2741,8 @@ var HELP = (fmt) => `${fmt.strong("atoll")} \u2014 bootstrap multithreading ${fm
|
|
|
2207
2741
|
worker pool + typed client (+ framework bindings on UI projects)
|
|
2208
2742
|
memory shared-memory contract
|
|
2209
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)
|
|
2210
2746
|
service nestjs: @AtollService class facade + pool module + worker
|
|
2211
2747
|
method nestjs: @AtollTask method-level offload
|
|
2212
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.
|
|
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",
|
package/src/cli.ts
CHANGED
|
@@ -16,9 +16,10 @@ import { runAdd, runCreate, 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
|
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',
|
|
@@ -152,6 +153,7 @@ const KIND_SCOPE: Record<AddKind, readonly Framework[] | null> = {
|
|
|
152
153
|
worker: null,
|
|
153
154
|
memory: null,
|
|
154
155
|
island: ISLAND_FRAMEWORKS,
|
|
156
|
+
mfe: ISLAND_FRAMEWORKS,
|
|
155
157
|
service: ['nestjs'],
|
|
156
158
|
method: ['nestjs'],
|
|
157
159
|
module: ['nestjs'],
|
|
@@ -360,6 +362,34 @@ export async function runAdd(ctx: Ctx): Promise<number> {
|
|
|
360
362
|
return 0;
|
|
361
363
|
}
|
|
362
364
|
|
|
365
|
+
case 'mfe': {
|
|
366
|
+
const dir = flag(ctx.args, 'dir') ?? rel(join(project.srcDir, 'mfe'));
|
|
367
|
+
const files = T.mfeFiles(name, framework, dir);
|
|
368
|
+
// The publish build lives at the project root beside vite.config.ts —
|
|
369
|
+
// it exists only for `vite build --config`, never the app dev server.
|
|
370
|
+
const publishCfg = rel(join(project.root!, 'vite.mfe.config.ts'));
|
|
371
|
+
// The config lives at the project root — the lib entry must be
|
|
372
|
+
// root-relative even when `add` ran from a subdirectory or --dir moved.
|
|
373
|
+
const dirFromRoot = relative(project.root!, join(ctx.cwd, dir)).replaceAll('\\', '/');
|
|
374
|
+
await writeTree(
|
|
375
|
+
ctx.io,
|
|
376
|
+
ctx.cwd,
|
|
377
|
+
[
|
|
378
|
+
...files,
|
|
379
|
+
{ path: publishCfg, content: T.mfePublishConfig(name, framework, dirFromRoot) },
|
|
380
|
+
],
|
|
381
|
+
force,
|
|
382
|
+
);
|
|
383
|
+
if (missingDeps(project.pkg, ['vite']).length) {
|
|
384
|
+
ctx.io.warn(
|
|
385
|
+
`vite isn't installed — the publish build needs it: ${ctx.io.fmt.accent(pmInstallCmd(project.pm, ['-D', 'vite']))}`,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
ctx.io.print(ctx.io.fmt.strong('\nwire it:'));
|
|
389
|
+
ctx.io.print(T.mfeUsage(name, framework, dir));
|
|
390
|
+
return 0;
|
|
391
|
+
}
|
|
392
|
+
|
|
363
393
|
case 'service': {
|
|
364
394
|
const dir = atollDir;
|
|
365
395
|
mkdirSync(join(ctx.cwd, dir), { recursive: true });
|
|
@@ -488,8 +518,17 @@ export async function runCreate(ctx: Ctx): Promise<number> {
|
|
|
488
518
|
}
|
|
489
519
|
mkdirSync(target, { recursive: true });
|
|
490
520
|
|
|
521
|
+
const mfe = hasFlag(ctx.args, 'mfe');
|
|
522
|
+
if (mfe && fw === 'node') {
|
|
523
|
+
ctx.io.error('--mfe is a browser-island shape — pick react|vue|solid|svelte');
|
|
524
|
+
return 1;
|
|
525
|
+
}
|
|
491
526
|
const files =
|
|
492
|
-
fw === 'node'
|
|
527
|
+
fw === 'node'
|
|
528
|
+
? T.nodeScaffoldFiles(dirName)
|
|
529
|
+
: mfe
|
|
530
|
+
? T.mfeScaffoldFiles(dirName, fw)
|
|
531
|
+
: T.scaffoldFiles(dirName, fw);
|
|
493
532
|
await writeTree(ctx.io, target, files, { force: true });
|
|
494
533
|
|
|
495
534
|
const written = JSON.parse(readFileSync(join(target, 'package.json'), 'utf8'));
|
|
@@ -507,6 +546,14 @@ export async function runCreate(ctx: Ctx): Promise<number> {
|
|
|
507
546
|
|
|
508
547
|
ctx.io.print('');
|
|
509
548
|
ctx.io.print(`done — ${ctx.io.fmt.accent(`cd ${dirName} && ${pm} run dev`)}`);
|
|
549
|
+
if (mfe) {
|
|
550
|
+
ctx.io.print(
|
|
551
|
+
` ${ctx.io.fmt.accent('npm run build:mfe')} → dist-mfe/${dirName}.worker.js — publish behind CORS + a versioned URL`,
|
|
552
|
+
);
|
|
553
|
+
ctx.io.print(
|
|
554
|
+
` ${ctx.io.fmt.accent('npm run preview:mfe')} serves it cross-origin; shells mount the contract`,
|
|
555
|
+
);
|
|
556
|
+
}
|
|
510
557
|
return 0;
|
|
511
558
|
}
|
|
512
559
|
|
package/src/templates.ts
CHANGED
|
@@ -1143,6 +1143,528 @@ const ${P}Island = lazyIsland(() => import('./islands/${name}.island'));
|
|
|
1143
1143
|
}
|
|
1144
1144
|
}
|
|
1145
1145
|
|
|
1146
|
+
/* ── mfe — publishable micro-frontend (contract + worker + bundle config) ── */
|
|
1147
|
+
|
|
1148
|
+
/**
|
|
1149
|
+
* The framework-free contract — the ONE module both sides import. The
|
|
1150
|
+
* `worker` factory is bundler-detectable for local dev; a comment shows the
|
|
1151
|
+
* remote-URL variant used once the bundle is deployed (islands-remote.md).
|
|
1152
|
+
*/
|
|
1153
|
+
export function mfeContractFile(name: string, fw: Framework): OutFile {
|
|
1154
|
+
const c = camel(name);
|
|
1155
|
+
const ext = ISLAND_EXT[fw] ?? 'ts';
|
|
1156
|
+
return {
|
|
1157
|
+
path: `${name}.contract.ts`,
|
|
1158
|
+
content: `import { z } from '@atolljs/core';
|
|
1159
|
+
import { defineIslandContract } from '@atolljs/islands';
|
|
1160
|
+
|
|
1161
|
+
/**
|
|
1162
|
+
* '${name}' — the wire contract for this micro-frontend. Shells import THIS
|
|
1163
|
+
* module only: it names the app, pins the prop/event payload shapes, and
|
|
1164
|
+
* carries the worker factory. The worker attaches the same module via
|
|
1165
|
+
* { contract } so both sides validate one shape — drift fails loudly at
|
|
1166
|
+
* mount or emit.
|
|
1167
|
+
*
|
|
1168
|
+
* The factory below is bundler-detectable (\`new URL(..., import.meta.url)\`)
|
|
1169
|
+
* so local dev builds the worker entry automatically. Deployment changes
|
|
1170
|
+
* only this field — see docs/islands-remote.md for the serving rules:
|
|
1171
|
+
*
|
|
1172
|
+
* // CDN deploy — worker SCRIPT urls must stay same-origin (a cross-origin
|
|
1173
|
+
* // new Worker() throws SecurityError regardless of CORS), so the remote
|
|
1174
|
+
* // bundle loads through a same-origin blob: shim; CORS covers the
|
|
1175
|
+
* // import fetch inside it:
|
|
1176
|
+
* worker: () => {
|
|
1177
|
+
* const shim = \`import \${JSON.stringify(\`\${import.meta.env.VITE_MFE_ORIGIN}/${name}.worker.js\`)};\`;
|
|
1178
|
+
* return new Worker(
|
|
1179
|
+
* URL.createObjectURL(new Blob([shim], { type: 'text/javascript' })),
|
|
1180
|
+
* { type: 'module' },
|
|
1181
|
+
* );
|
|
1182
|
+
* },
|
|
1183
|
+
*
|
|
1184
|
+
* // Or ship dist-mfe/ INSIDE the npm package — resolve the bundle
|
|
1185
|
+
* // package-relative as a plain ASSET (hoisted new URL — inline inside
|
|
1186
|
+
* // new Worker would make the consumer's bundler re-emit it as a worker
|
|
1187
|
+
* // entry instead of copying it verbatim):
|
|
1188
|
+
* const bundled = new URL('../../dist-mfe/${name}.worker.js', import.meta.url);
|
|
1189
|
+
* worker: () => new Worker(bundled, { type: 'module' }),
|
|
1190
|
+
*/
|
|
1191
|
+
export const ${c}Contract = defineIslandContract({
|
|
1192
|
+
app: '${name}',
|
|
1193
|
+
props: z.object({ label: z.string().optional() }),
|
|
1194
|
+
events: {
|
|
1195
|
+
incremented: z.object({ count: z.number(), label: z.string() }),
|
|
1196
|
+
},
|
|
1197
|
+
worker: () =>
|
|
1198
|
+
new Worker(new URL('./${name}.worker.${ext}', import.meta.url), { type: 'module' }),
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
export default ${c}Contract;
|
|
1202
|
+
`,
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
/**
|
|
1207
|
+
* Worker entry per framework — the MFE implementation, attaching the
|
|
1208
|
+
* contract ({ contract }) so props parse at mount/updateProps and the
|
|
1209
|
+
* declared emit payload validates inside the worker.
|
|
1210
|
+
*/
|
|
1211
|
+
export function mfeWorkerFiles(name: string, fw: Framework): OutFile[] {
|
|
1212
|
+
const P = pascal(name);
|
|
1213
|
+
const c = camel(name);
|
|
1214
|
+
switch (fw) {
|
|
1215
|
+
case 'react':
|
|
1216
|
+
return [
|
|
1217
|
+
{
|
|
1218
|
+
path: `${name}.worker.tsx`,
|
|
1219
|
+
content: `import { useState } from 'react';
|
|
1220
|
+
import { defineReactMonoWorker, emit } from '@atolljs/react-island/worker';
|
|
1221
|
+
import ${c}Contract from './${name}.contract';
|
|
1222
|
+
|
|
1223
|
+
/**
|
|
1224
|
+
* ${P} — a publishable React micro-frontend rendered INSIDE its own worker.
|
|
1225
|
+
* The bundle is self-contained; deploy it behind CORS + a versioned URL.
|
|
1226
|
+
*/
|
|
1227
|
+
function ${P}({ label = '${name}' }: { label?: string }) {
|
|
1228
|
+
const [count, setCount] = useState(0);
|
|
1229
|
+
return (
|
|
1230
|
+
<button
|
|
1231
|
+
onClick={() => {
|
|
1232
|
+
const n = count + 1;
|
|
1233
|
+
setCount(n);
|
|
1234
|
+
emit('incremented', { count: n, label }); // validates against the contract
|
|
1235
|
+
}}
|
|
1236
|
+
>
|
|
1237
|
+
{label}: {count}
|
|
1238
|
+
</button>
|
|
1239
|
+
);
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
export const worker = defineReactMonoWorker(${P}, { contract: ${c}Contract });
|
|
1243
|
+
`,
|
|
1244
|
+
},
|
|
1245
|
+
];
|
|
1246
|
+
case 'vue':
|
|
1247
|
+
return [
|
|
1248
|
+
{
|
|
1249
|
+
path: `${P}.vue`,
|
|
1250
|
+
content: `<script setup lang="ts">
|
|
1251
|
+
import { ref } from 'vue';
|
|
1252
|
+
import { emit } from '@atolljs/islands/worker';
|
|
1253
|
+
|
|
1254
|
+
// Rendered inside the worker — ops stream to the shell; emit() validates
|
|
1255
|
+
// the payload against the contract before it leaves the worker.
|
|
1256
|
+
const props = defineProps<{ label?: string }>();
|
|
1257
|
+
const count = ref(0);
|
|
1258
|
+
function increment() {
|
|
1259
|
+
count.value += 1;
|
|
1260
|
+
emit('incremented', { count: count.value, label: props.label ?? '${name}' });
|
|
1261
|
+
}
|
|
1262
|
+
</script>
|
|
1263
|
+
|
|
1264
|
+
<template>
|
|
1265
|
+
<button @click="increment">{{ label ?? '${name}' }}: {{ count }}</button>
|
|
1266
|
+
</template>
|
|
1267
|
+
`,
|
|
1268
|
+
},
|
|
1269
|
+
{
|
|
1270
|
+
path: `${name}.worker.ts`,
|
|
1271
|
+
content: `import { defineVueMonoWorker } from '@atolljs/vue-island/worker';
|
|
1272
|
+
import ${P} from './${P}.vue';
|
|
1273
|
+
import ${c}Contract from './${name}.contract';
|
|
1274
|
+
|
|
1275
|
+
/** '${name}' — publishable Vue MFE, one app per worker. */
|
|
1276
|
+
export const worker = defineVueMonoWorker(${P}, { contract: ${c}Contract });
|
|
1277
|
+
`,
|
|
1278
|
+
},
|
|
1279
|
+
];
|
|
1280
|
+
case 'solid':
|
|
1281
|
+
return [
|
|
1282
|
+
{
|
|
1283
|
+
path: `${name}.worker.tsx`,
|
|
1284
|
+
content: `import { createSignal } from 'solid-js';
|
|
1285
|
+
import { defineSolidMonoWorker, emit } from '@atolljs/solid-island/worker';
|
|
1286
|
+
import ${c}Contract from './${name}.contract';
|
|
1287
|
+
|
|
1288
|
+
/** ${P} — a publishable Solid MFE rendered inside its own worker. */
|
|
1289
|
+
function ${P}(props: { label?: string }) {
|
|
1290
|
+
const [count, setCount] = createSignal(0);
|
|
1291
|
+
return (
|
|
1292
|
+
<button
|
|
1293
|
+
onClick={() => {
|
|
1294
|
+
const n = count() + 1;
|
|
1295
|
+
setCount(n);
|
|
1296
|
+
emit('incremented', { count: n, label: props.label ?? '${name}' });
|
|
1297
|
+
}}
|
|
1298
|
+
>
|
|
1299
|
+
{props.label ?? '${name}'}: {count()}
|
|
1300
|
+
</button>
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
export const worker = defineSolidMonoWorker(${P}, { contract: ${c}Contract });
|
|
1305
|
+
`,
|
|
1306
|
+
},
|
|
1307
|
+
];
|
|
1308
|
+
case 'svelte':
|
|
1309
|
+
return [
|
|
1310
|
+
{
|
|
1311
|
+
path: `${P}.svelte`,
|
|
1312
|
+
content: `<script lang="ts">
|
|
1313
|
+
import { emit } from '@atolljs/islands/worker';
|
|
1314
|
+
|
|
1315
|
+
// Rendered inside the worker — emit() validates against the contract.
|
|
1316
|
+
let { label = '${name}' }: { label?: string } = $props();
|
|
1317
|
+
let count = $state(0);
|
|
1318
|
+
function increment() {
|
|
1319
|
+
count += 1;
|
|
1320
|
+
emit('incremented', { count, label });
|
|
1321
|
+
}
|
|
1322
|
+
</script>
|
|
1323
|
+
|
|
1324
|
+
<button onclick={increment}>{label}: {count}</button>
|
|
1325
|
+
`,
|
|
1326
|
+
},
|
|
1327
|
+
{
|
|
1328
|
+
path: `${name}.worker.ts`,
|
|
1329
|
+
content: `import { defineSvelteMonoWorker } from '@atolljs/svelte-island/worker';
|
|
1330
|
+
import ${P} from './${P}.svelte';
|
|
1331
|
+
import ${c}Contract from './${name}.contract';
|
|
1332
|
+
|
|
1333
|
+
/** '${name}' — publishable Svelte MFE, one app per worker. */
|
|
1334
|
+
export const worker = defineSvelteMonoWorker(${P}, { contract: ${c}Contract });
|
|
1335
|
+
`,
|
|
1336
|
+
},
|
|
1337
|
+
];
|
|
1338
|
+
case 'angular':
|
|
1339
|
+
return [
|
|
1340
|
+
{
|
|
1341
|
+
path: `${name}.worker.ts`,
|
|
1342
|
+
content: `import '@angular/compiler'; // JIT — compiles the decorator template in the worker
|
|
1343
|
+
import { Component, input, output } from '@angular/core';
|
|
1344
|
+
import { defineAngularMonoWorker } from '@atolljs/angular-island/worker';
|
|
1345
|
+
import ${c}Contract from './${name}.contract';
|
|
1346
|
+
|
|
1347
|
+
/**
|
|
1348
|
+
* ${P}Component — a publishable Angular MFE. The contract is checked against
|
|
1349
|
+
* this class's input()/output() surface (IslandContract<IslandInputs<C>,
|
|
1350
|
+
* IslandEvents<C>>) — a contract missing 'label' or 'incremented' fails
|
|
1351
|
+
* typecheck HERE, not at the shell.
|
|
1352
|
+
*/
|
|
1353
|
+
@Component({
|
|
1354
|
+
selector: 'mfe-${name}',
|
|
1355
|
+
template: \`<button (click)="increment()">{{ label() }}: {{ count }}</button>\`,
|
|
1356
|
+
})
|
|
1357
|
+
export class ${P}Component {
|
|
1358
|
+
readonly label = input('${name}');
|
|
1359
|
+
readonly incremented = output<{ count: number; label: string }>();
|
|
1360
|
+
count = 0;
|
|
1361
|
+
increment(): void {
|
|
1362
|
+
this.count += 1;
|
|
1363
|
+
this.incremented.emit({ count: this.count, label: this.label() });
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
export const worker = defineAngularMonoWorker(${P}Component, {
|
|
1368
|
+
contract: ${c}Contract,
|
|
1369
|
+
});
|
|
1370
|
+
`,
|
|
1371
|
+
},
|
|
1372
|
+
];
|
|
1373
|
+
default:
|
|
1374
|
+
return [];
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
/** All MFE files rooted at `dir` — contract + worker (+ framework SFCs). */
|
|
1379
|
+
export function mfeFiles(name: string, fw: Framework, dir: string): OutFile[] {
|
|
1380
|
+
return [
|
|
1381
|
+
{ ...mfeContractFile(name, fw), path: `${dir}/${name}.contract.ts` },
|
|
1382
|
+
...mfeWorkerFiles(name, fw).map((f) => ({ ...f, path: `${dir}/${f.path}` })),
|
|
1383
|
+
];
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
/**
|
|
1387
|
+
* The publish build — `vite build --config vite.mfe.config.ts` emits ONE
|
|
1388
|
+
* self-contained ESM worker bundle per MFE, deployable to any static host.
|
|
1389
|
+
*/
|
|
1390
|
+
export function mfePublishConfig(name: string, fw: Framework, dir = 'src/mfe'): string {
|
|
1391
|
+
const ext = ISLAND_EXT[fw] ?? 'ts';
|
|
1392
|
+
const plugin = SCAFFOLDS[fw]?.vitePlugin;
|
|
1393
|
+
return `import { defineConfig } from 'vite';
|
|
1394
|
+
${plugin?.import_ ?? ''}
|
|
1395
|
+
/**
|
|
1396
|
+
* MFE publish build — emits ONE self-contained ESM worker bundle at
|
|
1397
|
+
* dist-mfe/${name}.worker.js (worker entry + framework runtime baked in;
|
|
1398
|
+
* every remote import would need its own CORS headers, so ship one file).
|
|
1399
|
+
*
|
|
1400
|
+
* Serve it cross-origin with:
|
|
1401
|
+
* Access-Control-Allow-Origin: <shell origin> // module workers fetch via CORS
|
|
1402
|
+
* Cache-Control: public, max-age=31536000, immutable
|
|
1403
|
+
* and version the URL per release — the contract's worker factory is the
|
|
1404
|
+
* cache key. docs/islands-remote.md covers the full matrix.
|
|
1405
|
+
*
|
|
1406
|
+
* npm run build:mfe → dist-mfe/${name}.worker.js
|
|
1407
|
+
* npm run preview:mfe → serves dist-mfe with the CORS header below —
|
|
1408
|
+
* shells still need a same-origin shim entry
|
|
1409
|
+
* (blob: or a local re-export file) since worker
|
|
1410
|
+
* script URLs can't be cross-origin
|
|
1411
|
+
*/
|
|
1412
|
+
export default defineConfig({
|
|
1413
|
+
plugins: [
|
|
1414
|
+
{
|
|
1415
|
+
// The worker entry imports the contract, so the contract's own worker
|
|
1416
|
+
// factory lands in this bundle too — and its new URL(...) resolves
|
|
1417
|
+
// the PREVIOUS dist-mfe output, inlining it into its own successor
|
|
1418
|
+
// (the bundle grows every rebuild). The field is dead code here — a
|
|
1419
|
+
// worker never spawns itself — so stub the URL before vite's asset
|
|
1420
|
+
// plugin can resolve it.
|
|
1421
|
+
name: 'mfe:stub-worker-url',
|
|
1422
|
+
enforce: 'pre',
|
|
1423
|
+
transform(code: string, id: string) {
|
|
1424
|
+
if (!id.endsWith('${name}.contract.ts')) return null;
|
|
1425
|
+
return code.replace(
|
|
1426
|
+
/new URL\\(\\s*'[^']*'\\s*,\\s*import\\.meta\\.url,?\\s*\\)/,
|
|
1427
|
+
"'about:blank'",
|
|
1428
|
+
);
|
|
1429
|
+
},
|
|
1430
|
+
},
|
|
1431
|
+
${plugin?.call ?? ''}
|
|
1432
|
+
],
|
|
1433
|
+
// App builds define process.env.NODE_ENV automatically; a lib-mode worker
|
|
1434
|
+
// bundle doesn't, and framework dev/prod checks crash on a bare
|
|
1435
|
+
// \`process\` in a browser worker.
|
|
1436
|
+
define: { 'process.env.NODE_ENV': '"production"' },
|
|
1437
|
+
build: {
|
|
1438
|
+
outDir: 'dist-mfe',
|
|
1439
|
+
minify: true,
|
|
1440
|
+
lib: {
|
|
1441
|
+
entry: '${dir}/${name}.worker.${ext}',
|
|
1442
|
+
formats: ['es'],
|
|
1443
|
+
fileName: () => '${name}.worker.js',
|
|
1444
|
+
},
|
|
1445
|
+
},
|
|
1446
|
+
preview: {
|
|
1447
|
+
headers: { 'Access-Control-Allow-Origin': '*' },
|
|
1448
|
+
},
|
|
1449
|
+
});
|
|
1450
|
+
`;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
/** Shell-side consume + publish guidance printed after `add mfe`. */
|
|
1454
|
+
export function mfeUsage(name: string, fw: Framework, dir = 'src/mfe'): string {
|
|
1455
|
+
const c = camel(name);
|
|
1456
|
+
const consume: Record<string, string> = {
|
|
1457
|
+
react: `import { islandComponent } from '@atolljs/react-island';
|
|
1458
|
+
import ${c}Contract from './${dir}/${name}.contract';
|
|
1459
|
+
const ${pascal(name)}Island = islandComponent(${c}Contract);
|
|
1460
|
+
<${pascal(name)}Island label="${name}" onEvent={(n, p) => console.log(n, p)} />`,
|
|
1461
|
+
vue: `import { islandComponent } from '@atolljs/vue-island';
|
|
1462
|
+
import ${c}Contract from './${dir}/${name}.contract';
|
|
1463
|
+
const ${pascal(name)}Island = islandComponent(${c}Contract);
|
|
1464
|
+
<${pascal(name)}Island v-bind="{ label: '${name}', onEvent }" />`,
|
|
1465
|
+
solid: `import { islandComponent } from '@atolljs/solid-island';
|
|
1466
|
+
import ${c}Contract from './${dir}/${name}.contract';
|
|
1467
|
+
const ${pascal(name)}Island = islandComponent(${c}Contract);
|
|
1468
|
+
<${pascal(name)}Island label="${name}" onEvent={(n, p) => console.log(n, p)} />`,
|
|
1469
|
+
svelte: `import { island } from '@atolljs/svelte-island';
|
|
1470
|
+
import ${c}Contract from './${dir}/${name}.contract';
|
|
1471
|
+
<div use:island={{ app: ${c}Contract, props: { label: '${name}' }, onEvent }} />`,
|
|
1472
|
+
angular: `import { islandComponent } from '@atolljs/angular-island';
|
|
1473
|
+
import ${c}Contract from './${dir}/${name}.contract';
|
|
1474
|
+
export const ${pascal(name)}Island = islandComponent({
|
|
1475
|
+
contract: ${c}Contract,
|
|
1476
|
+
selector: '${name}-island',
|
|
1477
|
+
});
|
|
1478
|
+
// <${name}-island [props]="{ label: '${name}' }" [onEvent]="onEvent" />`,
|
|
1479
|
+
};
|
|
1480
|
+
return `shell side — mount through the contract, no framework import:
|
|
1481
|
+
${consume[fw] ?? ''}
|
|
1482
|
+
|
|
1483
|
+
publish — \`vite build --config vite.mfe.config.ts\` emits dist-mfe/${name}.worker.js
|
|
1484
|
+
deploy it behind Access-Control-Allow-Origin + a versioned URL, then point the
|
|
1485
|
+
contract's worker factory at it (docs/islands-remote.md).`;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
/** `atoll new <dir> --mfe` — a standalone publishable-MFE package. */
|
|
1489
|
+
export function mfeScaffoldFiles(appName: string, fw: Framework): OutFile[] {
|
|
1490
|
+
const spec = SCAFFOLDS[fw];
|
|
1491
|
+
if (!spec) return [];
|
|
1492
|
+
const name = appName;
|
|
1493
|
+
const P = pascal(name);
|
|
1494
|
+
const files: OutFile[] = [
|
|
1495
|
+
{
|
|
1496
|
+
path: 'package.json',
|
|
1497
|
+
content:
|
|
1498
|
+
JSON.stringify(
|
|
1499
|
+
{
|
|
1500
|
+
name: appName,
|
|
1501
|
+
private: true,
|
|
1502
|
+
type: 'module',
|
|
1503
|
+
// The package's PUBLIC surface is the contract module — shells
|
|
1504
|
+
// import it, never the worker entry. The worker ships as a
|
|
1505
|
+
// deployed asset (dist-mfe/) referenced by URL, not an import.
|
|
1506
|
+
exports: { '.': `./src/mfe/${name}.contract.ts` },
|
|
1507
|
+
files: ['src/mfe', 'dist-mfe'],
|
|
1508
|
+
scripts: {
|
|
1509
|
+
dev: 'vite',
|
|
1510
|
+
'build:mfe': 'vite build --config vite.mfe.config.ts',
|
|
1511
|
+
'preview:mfe': 'vite preview --config vite.mfe.config.ts',
|
|
1512
|
+
},
|
|
1513
|
+
dependencies: spec.deps,
|
|
1514
|
+
devDependencies: spec.devDeps,
|
|
1515
|
+
},
|
|
1516
|
+
null,
|
|
1517
|
+
2,
|
|
1518
|
+
) + '\n',
|
|
1519
|
+
},
|
|
1520
|
+
{ path: 'tsconfig.json', content: JSON.stringify(TSCONFIGS[fw], null, 2) + '\n' },
|
|
1521
|
+
{ path: 'vite.config.ts', content: viteConfig(fw) },
|
|
1522
|
+
{ path: 'vite.mfe.config.ts', content: mfePublishConfig(name, fw) },
|
|
1523
|
+
{ path: '.npmrc', content: NPMRC },
|
|
1524
|
+
{ path: '.gitignore', content: GITIGNORE + 'dist-mfe/\n' },
|
|
1525
|
+
{ path: 'index.html', content: INDEX_HTML(appName, `src/main.${ISLAND_EXT[fw]}`) },
|
|
1526
|
+
...mfeFiles(name, fw, 'src/mfe'),
|
|
1527
|
+
];
|
|
1528
|
+
// Dev harness — mounts the MFE through its contract so `vite dev` previews
|
|
1529
|
+
// exactly what a consuming shell sees (no worker imports on the page).
|
|
1530
|
+
switch (fw) {
|
|
1531
|
+
case 'react':
|
|
1532
|
+
files.push(
|
|
1533
|
+
{
|
|
1534
|
+
path: 'src/main.tsx',
|
|
1535
|
+
content: `import { createRoot } from 'react-dom/client';
|
|
1536
|
+
import { App } from './App';
|
|
1537
|
+
|
|
1538
|
+
createRoot(document.getElementById('root')!).render(<App />);
|
|
1539
|
+
`,
|
|
1540
|
+
},
|
|
1541
|
+
{
|
|
1542
|
+
path: 'src/App.tsx',
|
|
1543
|
+
content: `import { islandComponent } from '@atolljs/react-island';
|
|
1544
|
+
import ${camel(name)}Contract from './mfe/${name}.contract';
|
|
1545
|
+
|
|
1546
|
+
const ${P}Island = islandComponent(${camel(name)}Contract);
|
|
1547
|
+
|
|
1548
|
+
/** Dev harness — this page mounts the MFE exactly like a consuming shell. */
|
|
1549
|
+
export function App() {
|
|
1550
|
+
return (
|
|
1551
|
+
<main style={{ fontFamily: 'system-ui', padding: '2rem' }}>
|
|
1552
|
+
<h1>${appName}</h1>
|
|
1553
|
+
<p>
|
|
1554
|
+
This island renders in a worker. Publish with{' '}
|
|
1555
|
+
<code>npm run build:mfe</code> → <code>dist-mfe/</code>.
|
|
1556
|
+
</p>
|
|
1557
|
+
<${P}Island
|
|
1558
|
+
label="${name}"
|
|
1559
|
+
onEvent={(name, payload) => console.log('mfe event:', name, payload)}
|
|
1560
|
+
/>
|
|
1561
|
+
</main>
|
|
1562
|
+
);
|
|
1563
|
+
}
|
|
1564
|
+
`,
|
|
1565
|
+
},
|
|
1566
|
+
);
|
|
1567
|
+
break;
|
|
1568
|
+
case 'vue':
|
|
1569
|
+
files.push(
|
|
1570
|
+
{ path: 'src/env.d.ts', content: `declare module '*.vue';\n` },
|
|
1571
|
+
{
|
|
1572
|
+
path: 'src/main.ts',
|
|
1573
|
+
content: `import { createApp } from 'vue';
|
|
1574
|
+
import App from './App.vue';
|
|
1575
|
+
|
|
1576
|
+
createApp(App).mount('#root');
|
|
1577
|
+
`,
|
|
1578
|
+
},
|
|
1579
|
+
{
|
|
1580
|
+
path: 'src/App.vue',
|
|
1581
|
+
content: `<script setup lang="ts">
|
|
1582
|
+
import { islandComponent } from '@atolljs/vue-island';
|
|
1583
|
+
import ${camel(name)}Contract from './mfe/${name}.contract';
|
|
1584
|
+
|
|
1585
|
+
const ${P}Island = islandComponent(${camel(name)}Contract);
|
|
1586
|
+
const onEvent = (name: string, payload: unknown) => console.log('mfe event:', name, payload);
|
|
1587
|
+
</script>
|
|
1588
|
+
|
|
1589
|
+
<template>
|
|
1590
|
+
<main style="font-family: system-ui; padding: 2rem">
|
|
1591
|
+
<h1>${appName}</h1>
|
|
1592
|
+
<p>This island renders in a worker — <code>npm run build:mfe</code> publishes it.</p>
|
|
1593
|
+
<${P}Island v-bind="{ label: '${name}', onEvent }" />
|
|
1594
|
+
</main>
|
|
1595
|
+
</template>
|
|
1596
|
+
`,
|
|
1597
|
+
},
|
|
1598
|
+
);
|
|
1599
|
+
break;
|
|
1600
|
+
case 'solid':
|
|
1601
|
+
files.push(
|
|
1602
|
+
{
|
|
1603
|
+
path: 'src/main.tsx',
|
|
1604
|
+
content: `import { render } from 'solid-js/web';
|
|
1605
|
+
import { App } from './App';
|
|
1606
|
+
|
|
1607
|
+
render(() => <App />, document.getElementById('root')!);
|
|
1608
|
+
`,
|
|
1609
|
+
},
|
|
1610
|
+
{
|
|
1611
|
+
path: 'src/App.tsx',
|
|
1612
|
+
content: `import { islandComponent } from '@atolljs/solid-island';
|
|
1613
|
+
import ${camel(name)}Contract from './mfe/${name}.contract';
|
|
1614
|
+
|
|
1615
|
+
const ${P}Island = islandComponent(${camel(name)}Contract);
|
|
1616
|
+
|
|
1617
|
+
/** Dev harness — mounts the MFE exactly like a consuming shell. */
|
|
1618
|
+
export function App() {
|
|
1619
|
+
return (
|
|
1620
|
+
<main style={{ 'font-family': 'system-ui', padding: '2rem' }}>
|
|
1621
|
+
<h1>${appName}</h1>
|
|
1622
|
+
<p>
|
|
1623
|
+
This island renders in a worker — <code>npm run build:mfe</code> publishes it.
|
|
1624
|
+
</p>
|
|
1625
|
+
<${P}Island
|
|
1626
|
+
label="${name}"
|
|
1627
|
+
onEvent={(name, payload) => console.log('mfe event:', name, payload)}
|
|
1628
|
+
/>
|
|
1629
|
+
</main>
|
|
1630
|
+
);
|
|
1631
|
+
}
|
|
1632
|
+
`,
|
|
1633
|
+
},
|
|
1634
|
+
);
|
|
1635
|
+
break;
|
|
1636
|
+
case 'svelte':
|
|
1637
|
+
files.push(
|
|
1638
|
+
{
|
|
1639
|
+
path: 'src/main.ts',
|
|
1640
|
+
content: `import { mount } from 'svelte';
|
|
1641
|
+
import App from './App.svelte';
|
|
1642
|
+
|
|
1643
|
+
mount(App, { target: document.getElementById('root')! });
|
|
1644
|
+
`,
|
|
1645
|
+
},
|
|
1646
|
+
{
|
|
1647
|
+
path: 'src/App.svelte',
|
|
1648
|
+
content: `<script lang="ts">
|
|
1649
|
+
import { island } from '@atolljs/svelte-island';
|
|
1650
|
+
import ${camel(name)}Contract from './mfe/${name}.contract';
|
|
1651
|
+
|
|
1652
|
+
const onEvent = (name: string, payload: unknown) => console.log('mfe event:', name, payload);
|
|
1653
|
+
</script>
|
|
1654
|
+
|
|
1655
|
+
<main style="font-family: system-ui; padding: 2rem">
|
|
1656
|
+
<h1>${appName}</h1>
|
|
1657
|
+
<p>This island renders in a worker — <code>npm run build:mfe</code> publishes it.</p>
|
|
1658
|
+
<div use:island={{ app: ${camel(name)}Contract, props: { label: '${name}' }, onEvent }} />
|
|
1659
|
+
</main>
|
|
1660
|
+
`,
|
|
1661
|
+
},
|
|
1662
|
+
);
|
|
1663
|
+
break;
|
|
1664
|
+
}
|
|
1665
|
+
return files;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1146
1668
|
/* ── nestjs (`atoll add nestjs <level> <name>`) ─────────────────────────── */
|
|
1147
1669
|
|
|
1148
1670
|
/**
|