@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/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
|
/**
|
|
@@ -1641,3 +2163,66 @@ export async function register() {
|
|
|
1641
2163
|
};
|
|
1642
2164
|
}
|
|
1643
2165
|
|
|
2166
|
+
|
|
2167
|
+
/**
|
|
2168
|
+
* `add devtools` — the init module, imported first from the app entry so the
|
|
2169
|
+
* sink is installed before any pool spawns (worker INIT carries the flag at
|
|
2170
|
+
* spawn time; late installs leave existing workers silent).
|
|
2171
|
+
*
|
|
2172
|
+
* Three shapes:
|
|
2173
|
+
* - node host → '@atolljs/devtools/node' entry, ATOLL_DEVTOOLS env gate
|
|
2174
|
+
* - browser → '@atolljs/devtools', ?__atoll_devtools URL gate + overlay
|
|
2175
|
+
* - browser + wsUrl → same browser gate but streams to the aggregate server
|
|
2176
|
+
* (angular: nothing mounts /__atoll/)
|
|
2177
|
+
*/
|
|
2178
|
+
export function devtoolsSetup(name: string, host: 'browser' | 'node', wsUrl?: string): string {
|
|
2179
|
+
if (host === 'node') {
|
|
2180
|
+
return `import { initDevtools } from '@atolljs/devtools/node';
|
|
2181
|
+
|
|
2182
|
+
/**
|
|
2183
|
+
* Devtools sink — imported first from the entry so it's installed before any
|
|
2184
|
+
* pool spawns (worker INIT carries the flag at spawn time; late installs
|
|
2185
|
+
* leave existing workers silent).
|
|
2186
|
+
*
|
|
2187
|
+
* No-op unless the process was started with ATOLL_DEVTOOLS=1 — then events
|
|
2188
|
+
* stream over WebSocket to the standalone dashboard (\`atoll devtools\`,
|
|
2189
|
+
* http://127.0.0.1:4780). Node has no BroadcastChannel page context, so the
|
|
2190
|
+
* aggregate server is the transport.
|
|
2191
|
+
*/
|
|
2192
|
+
initDevtools({ session: { name: '${name}' } });
|
|
2193
|
+
`;
|
|
2194
|
+
}
|
|
2195
|
+
if (wsUrl) {
|
|
2196
|
+
return `import { initDevtools } from '@atolljs/devtools';
|
|
2197
|
+
|
|
2198
|
+
/**
|
|
2199
|
+
* Devtools sink — imported first from the app entry so it's installed before
|
|
2200
|
+
* any pool spawns (worker INIT carries the flag at spawn time; late installs
|
|
2201
|
+
* leave existing workers silent).
|
|
2202
|
+
*
|
|
2203
|
+
* No-op unless the page URL carries ?__atoll_devtools — then events stream
|
|
2204
|
+
* over WebSocket to the standalone aggregate dashboard (\`atoll devtools\`,
|
|
2205
|
+
* http://127.0.0.1:4780). This app's dev server doesn't mount /__atoll/, so
|
|
2206
|
+
* the overlay is off and the aggregate server is the viewer.
|
|
2207
|
+
*/
|
|
2208
|
+
initDevtools({
|
|
2209
|
+
session: { name: '${name}' },
|
|
2210
|
+
overlay: false,
|
|
2211
|
+
url: '${wsUrl}',
|
|
2212
|
+
});
|
|
2213
|
+
`;
|
|
2214
|
+
}
|
|
2215
|
+
return `import { initDevtools } from '@atolljs/devtools';
|
|
2216
|
+
|
|
2217
|
+
/**
|
|
2218
|
+
* Devtools sink — 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 — then events stream
|
|
2223
|
+
* over BroadcastChannel on the app's own origin and a flyout overlay mounts.
|
|
2224
|
+
* /__atoll/ itself is served by the @atolljs/vite plugin (plugins: [atoll()]).
|
|
2225
|
+
*/
|
|
2226
|
+
initDevtools({ session: { name: '${name}' } });
|
|
2227
|
+
`;
|
|
2228
|
+
}
|