@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/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' ? T.nodeScaffoldFiles(dirName) : T.scaffoldFiles(dirName, fw);
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
@@ -414,27 +414,34 @@ interface ScaffoldSpec {
414
414
  deps: Record<string, string>;
415
415
  devDeps: Record<string, string>;
416
416
  vitePlugin?: { pkg: string; import_: string; call: string };
417
+ /** `atoll(…)` call args for the emitted config — e.g. solid needs its jsx runtime. */
418
+ atollCall?: string;
417
419
  }
418
420
 
419
421
  const SCAFFOLDS: Record<string, ScaffoldSpec> = {
420
422
  react: {
421
423
  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' },
422
- 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' },
423
- vitePlugin: { pkg: '@vitejs/plugin-react', import_: `import react from '@vitejs/plugin-react';`, call: 'react()' },
424
+ 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' },
425
+ // `exclude` replaces the plugin's default /node_modules/ — keep it, and
426
+ // add *.worker.*: fast-refresh injects `/@react-refresh` (which reads
427
+ // `window` unguarded) into every transformed module, including worker
428
+ // entries served as ?worker_file — that crashes the worker in dev.
429
+ vitePlugin: { pkg: '@vitejs/plugin-react', import_: `import react from '@vitejs/plugin-react';`, call: 'react({ exclude: [/\\/node_modules\\//, /\\.worker\\./] })' },
424
430
  },
425
431
  vue: {
426
432
  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' },
427
- devDeps: { vite: '^8.3.1', typescript: '^7.0.2', '@vitejs/plugin-vue': '^6.0.9' },
433
+ devDeps: { vite: '^8.3.1', typescript: '^7.0.2', '@vitejs/plugin-vue': '^6.0.9', '@atolljs/vite': '^0.1.0' },
428
434
  vitePlugin: { pkg: '@vitejs/plugin-vue', import_: `import vue from '@vitejs/plugin-vue';`, call: 'vue()' },
429
435
  },
430
436
  solid: {
431
437
  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' },
432
- devDeps: { vite: '^8.3.1', typescript: '^7.0.2', 'vite-plugin-solid': '^2.11.14' },
438
+ devDeps: { vite: '^8.3.1', typescript: '^7.0.2', 'vite-plugin-solid': '^2.11.14', '@atolljs/vite': '^0.1.0' },
439
+ atollCall: `atoll({ jsxImportSource: 'solid-js' })`,
433
440
  vitePlugin: { pkg: 'vite-plugin-solid', import_: `import solid from 'vite-plugin-solid';`, call: 'solid()' },
434
441
  },
435
442
  svelte: {
436
443
  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' },
437
- devDeps: { vite: '^8.3.1', typescript: '^7.0.2', '@sveltejs/vite-plugin-svelte': '^7.3.1' },
444
+ devDeps: { vite: '^8.3.1', typescript: '^7.0.2', '@sveltejs/vite-plugin-svelte': '^7.3.1', '@atolljs/vite': '^0.1.0' },
438
445
  vitePlugin: { pkg: '@sveltejs/vite-plugin-svelte', import_: `import { svelte } from '@sveltejs/vite-plugin-svelte';`, call: 'svelte()' },
439
446
  },
440
447
  };
@@ -513,9 +520,13 @@ function viteConfig(fw: string): string {
513
520
  const plugin = spec.vitePlugin;
514
521
  return `import { defineConfig } from 'vite';
515
522
  ${plugin?.import_ ?? ''}
523
+ import atoll from '@atolljs/vite';
516
524
 
517
525
  export default defineConfig({
518
- plugins: [${plugin?.call ?? ''}],
526
+ // atoll() serves worker entries as esbuild bundles (rebuild + respawn on
527
+ // change) — browser-plugin transforms like react fast-refresh can't leak
528
+ // into worker code.
529
+ plugins: [${plugin?.call ?? ''}, ${spec.atollCall ?? 'atoll()'}],
519
530
  ${
520
531
  plugin
521
532
  ? ` // Worker bundles run their own rolldown pass — fresh plugin instances
@@ -1132,6 +1143,528 @@ const ${P}Island = lazyIsland(() => import('./islands/${name}.island'));
1132
1143
  }
1133
1144
  }
1134
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
+
1135
1668
  /* ── nestjs (`atoll add nestjs <level> <name>`) ─────────────────────────── */
1136
1669
 
1137
1670
  /**