@motujs/cli 0.0.0-stage → 0.1.1

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.
Files changed (106) hide show
  1. package/LICENSE +21 -0
  2. package/bundled/github/agents/archipelago-build.agent.md +298 -0
  3. package/bundled/github/agents/island-create.agent.md +261 -0
  4. package/bundled/github/agents/island-extract.agent.md +360 -0
  5. package/bundled/github/agents/island-locate.agent.md +208 -0
  6. package/bundled/github/agents/motu-checks.agent.md +158 -0
  7. package/bundled/github/agents/motu-evidence.agent.md +267 -0
  8. package/bundled/github/agents/motu-regions.agent.md +304 -0
  9. package/bundled/github/host-rules.md +43 -0
  10. package/bundled/github/prompts/archipelago-build.prompt.md +19 -0
  11. package/bundled/github/prompts/island-create.prompt.md +37 -0
  12. package/bundled/github/prompts/island-extract.prompt.md +59 -0
  13. package/bundled/github/prompts/island-locate.prompt.md +43 -0
  14. package/bundled/packages/adapters/angularjs/vite.mjs +43 -0
  15. package/bundled/packages/adapters/next/lagoon/next-stubs.tsx +108 -0
  16. package/bundled/packages/adapters/next/vite.mjs +121 -0
  17. package/bundled/packages/adapters/vite/vite.mjs +179 -0
  18. package/package.json +64 -3
  19. package/src/alias-loader.mjs +63 -0
  20. package/src/alias-register.mjs +28 -0
  21. package/src/asset-stub.cjs +3 -0
  22. package/src/asset-stub.mjs +5 -0
  23. package/src/cli.mjs +78 -0
  24. package/src/commands/adopt-root.mjs +194 -0
  25. package/src/commands/archipelago-init.mjs +314 -0
  26. package/src/commands/archipelago.mjs +138 -0
  27. package/src/commands/assurance.mjs +40 -0
  28. package/src/commands/check.mjs +346 -0
  29. package/src/commands/codegen.mjs +29 -0
  30. package/src/commands/coverage.mjs +358 -0
  31. package/src/commands/create.mjs +277 -0
  32. package/src/commands/defaults.mjs +129 -0
  33. package/src/commands/diff.mjs +595 -0
  34. package/src/commands/fixtures.mjs +153 -0
  35. package/src/commands/init.mjs +473 -0
  36. package/src/commands/integrate.mjs +208 -0
  37. package/src/commands/integration.mjs +1349 -0
  38. package/src/commands/lagoon-try.mjs +272 -0
  39. package/src/commands/lagoon.mjs +1419 -0
  40. package/src/commands/link.mjs +110 -0
  41. package/src/commands/region-coverage.mjs +604 -0
  42. package/src/commands/removal-check.mjs +878 -0
  43. package/src/commands/skills.mjs +156 -0
  44. package/src/commands/snapshot.mjs +456 -0
  45. package/src/commands/sync.mjs +38 -0
  46. package/src/commands/verify/island-runtime.mjs +581 -0
  47. package/src/commands/verify/island-static.mjs +586 -0
  48. package/src/commands/verify/island.mjs +135 -0
  49. package/src/commands/verify/reach.mjs +426 -0
  50. package/src/commands/verify/region-composition.mjs +391 -0
  51. package/src/commands/verify/region-config.mjs +330 -0
  52. package/src/commands/verify/region-flows.mjs +760 -0
  53. package/src/commands/verify/region-sources.mjs +516 -0
  54. package/src/commands/verify/region.mjs +271 -0
  55. package/src/commands/verify/report.mjs +338 -0
  56. package/src/commands/verify/screens-runtime.mjs +123 -0
  57. package/src/commands/verify.mjs +18 -0
  58. package/src/lagoon-build.mjs +25 -0
  59. package/src/lagoon-dev.mjs +32 -0
  60. package/src/lib/alias-core.mjs +49 -0
  61. package/src/lib/archipelagos.mjs +111 -0
  62. package/src/lib/assurance.mjs +144 -0
  63. package/src/lib/baselines.mjs +107 -0
  64. package/src/lib/bundlability.mjs +123 -0
  65. package/src/lib/changed.mjs +157 -0
  66. package/src/lib/component-props.mjs +145 -0
  67. package/src/lib/config.mjs +451 -0
  68. package/src/lib/contracts.mjs +244 -0
  69. package/src/lib/data-flow.mjs +47 -0
  70. package/src/lib/declared-sources.mjs +101 -0
  71. package/src/lib/dock-inject.mjs +47 -0
  72. package/src/lib/effects.mjs +97 -0
  73. package/src/lib/eject.mjs +715 -0
  74. package/src/lib/evidence-json.mjs +74 -0
  75. package/src/lib/expect-calls.mjs +53 -0
  76. package/src/lib/finding-memory.mjs +0 -0
  77. package/src/lib/flow-calls.mjs +53 -0
  78. package/src/lib/framework.mjs +98 -0
  79. package/src/lib/host-rules.mjs +55 -0
  80. package/src/lib/host-sources.mjs +121 -0
  81. package/src/lib/island-placement.mjs +64 -0
  82. package/src/lib/island-render.mjs +105 -0
  83. package/src/lib/islands.mjs +112 -0
  84. package/src/lib/lagoon-bundle.mjs +138 -0
  85. package/src/lib/lagoon-declares.mjs +553 -0
  86. package/src/lib/lagoon-materialize.mjs +217 -0
  87. package/src/lib/lagoon-vite.mjs +788 -0
  88. package/src/lib/live-url.mjs +53 -0
  89. package/src/lib/node-aliases.mjs +73 -0
  90. package/src/lib/operations.mjs +145 -0
  91. package/src/lib/provenance-plugin.mjs +74 -0
  92. package/src/lib/region-refs.mjs +40 -0
  93. package/src/lib/remote.mjs +171 -0
  94. package/src/lib/scaffold.mjs +606 -0
  95. package/src/lib/screens.mjs +266 -0
  96. package/src/lib/snapshots.mjs +85 -0
  97. package/src/lib/stubs.mjs +114 -0
  98. package/src/lib/ts-project.mjs +42 -0
  99. package/src/lib/util.mjs +626 -0
  100. package/src/lib/worlds.mjs +236 -0
  101. package/src/lib/write-if-changed.mjs +35 -0
  102. package/src/lib/writes-coverage.mjs +32 -0
  103. package/src/playwright-lagoon.mjs +2305 -0
  104. package/src/run.mjs +385 -0
  105. package/src/runtime-harness.mjs +225 -0
  106. package/README.md +0 -3
@@ -0,0 +1,606 @@
1
+ // Scaffold templates for `motu init`. These exist so a fresh project gets a WORKING loop, not just
2
+ // a layout: `motu island verify` boots the lagoon root through Vite, so a project without one has
3
+ // static checks and no loop at all — which is the half of motu that matters.
4
+ //
5
+ // Templates are plain strings with {{placeholders}} (not JS template literals) so the generated TS —
6
+ // which is full of backticks and ${} of its own — passes through untouched.
7
+
8
+ /** Substitute {{key}} placeholders. An unknown placeholder is left alone (so it shows up in review). */
9
+ export function render(tpl, vars) {
10
+ // TO A FIXPOINT, because a var's VALUE may itself be a template: `envShim` expands to
11
+ // `import '{{envImport}}';`, and `String.replace` never re-scans what it inserted. One pass left
12
+ // that placeholder verbatim in the generated entry files — valid TypeScript, unresolvable module.
13
+ let out = tpl;
14
+ for (let pass = 0; pass < 5; pass++) {
15
+ const next = out.replace(/\{\{(\w+)\}\}/g, (m, k) => (k in vars ? String(vars[k]) : m));
16
+ if (next === out) return out;
17
+ out = next;
18
+ }
19
+ return out;
20
+ }
21
+
22
+ // ---------------------------------------------------------------------------------------------
23
+ // Project root files (the layout declaration + the registries islands slot into)
24
+ // ---------------------------------------------------------------------------------------------
25
+
26
+ export const ISLANDS_REGISTRY = `// GENERATED by \`motu sync\` from the islands on disk — do not edit by hand. \`motu island create\`
27
+ // adds one import + one row here; the framework turns this into custom-element registrations.
28
+ import type { ElementSpec } from '@motujs/react';
29
+
30
+ export const ELEMENT_REGISTRY: ElementSpec[] = [];
31
+ `;
32
+
33
+ export const ARCHIPELAGOS_REGISTRY = `// Registry of the project's archipelagos by id, so a composition root (or the lagoon / CLI) can
34
+ // resolve one by name. \`motu archipelago init\` adds one import + one row here.
35
+ import type { ArchipelagoConfig } from '@motujs/core';
36
+
37
+ export const ARCHIPELAGOS: Record<string, ArchipelagoConfig> = {};
38
+
39
+ /** Resolve an archipelago config by id (undefined if unknown). */
40
+ export function getArchipelago(id: string): ArchipelagoConfig | undefined {
41
+ return ARCHIPELAGOS[id];
42
+ }
43
+ `;
44
+
45
+ export const BARREL = `// Public surface of the project: the element registry and the archipelago configs + resolver.
46
+ // Composition roots (and the motu lagoon / CLI harness) import everything they need from here.
47
+ //
48
+ // NO \`.js\` ON THESE SPECIFIERS. Turbopack has no \`extensionAlias\`, so \`./islands/registry.js\` is
49
+ // simply an unresolved module and \`next build\` stops on this file. motu's own packages had their
50
+ // \`.js\` specifiers dropped for exactly this reason — \`moduleResolution: "Bundler"\` makes the
51
+ // extension optional, which fixes every bundler instead of one mapping per host — and the fix did
52
+ // not reach the files motu GENERATES. It does now.
53
+ export { ELEMENT_REGISTRY } from './islands/registry';
54
+ export { ARCHIPELAGOS, getArchipelago } from './archipelagos/registry';
55
+ `;
56
+
57
+ /** The app package manifest — the lagoon imports the project by NAME, so the name must resolve. */
58
+ export const APP_PACKAGE_JSON = `{
59
+ "name": "{{appPackage}}",
60
+ "version": "0.1.0",
61
+ "private": true,
62
+ "type": "module",
63
+ "main": "src/index.ts",
64
+ "types": "src/index.ts",
65
+ "exports": {
66
+ ".": "./src/index.ts",
67
+ "./styles.css": "./src/shared/styles.css"
68
+ },
69
+ {{motuDependencies}} "peerDependencies": {
70
+ "react": ">=18",
71
+ "react-dom": ">=18"
72
+ }
73
+ }
74
+ `;
75
+
76
+ /** The one shared island stylesheet. `motu archipelago verify` lints this, so it ships dual-mode. */
77
+ export const SHARED_STYLES = `/* Styles for every motu island. In shadow isolation this sheet is adopted into the shadow root; in
78
+ light isolation it is injected once globally and the island root carries a \`.motu-root\` marker.
79
+ That is why every host-scoped rule targets \`:where(:host, .motu-root)\` — a bare \`:host\` is inert
80
+ in light DOM. One sheet, both modes.
81
+
82
+ Colours come from --x-* tokens (host-overridable) with --_* locals holding the fallback, so a host
83
+ can reskin islands without either side reopening isolation.
84
+
85
+ THE LOCALS ARE ALSO DECLARED ON :root, AND THAT IS NOT BELT-AND-BRACES.
86
+ \`.motu-root\` is added by packages/core/src/island.ts when an island mounts AS A CUSTOM ELEMENT.
87
+ Two very common surfaces never do that: a React host that renders the component directly inside its
88
+ region's wrap form, and the lagoon's own view. On both, the whole block below is inert and every
89
+ --_* resolves to nothing — which silently drops border, background and every colour while leaving
90
+ padding and font working, so it renders a plausible, WRONG screen that no check can see. It was
91
+ found by opening the page, which is the only thing that finds it.
92
+
93
+ A project styling its islands with Tailwind never notices, because it never asks these locals for
94
+ anything. One that uses this sheet as written hits it on its first island.
95
+
96
+ Splitting the rule is what makes it safe: the LOCALS go global — they are only fallback
97
+ definitions, and any :host / .motu-root still overrides them — while the presentational rules stay
98
+ scoped, because \`display: block\` and a font on :root would restyle the whole document. */
99
+ :root,
100
+ :where(:host, .motu-root) {
101
+ --_text: var(--x-color-text, inherit);
102
+ --_muted: var(--x-color-muted, #6b7280);
103
+ --_border: var(--x-border, #e5e7eb);
104
+ --_radius: var(--x-radius, 6px);
105
+ --_surface: var(--x-color-surface, #ffffff);
106
+ }
107
+
108
+ /* SCOPED, unlike the locals above: these would restyle the document from :root. */
109
+ :where(:host, .motu-root) {
110
+ display: block;
111
+ color: var(--_text);
112
+ font-family: var(--x-font, inherit);
113
+ }
114
+ `;
115
+
116
+ /** Build output + local installs. `.motu/` holds published lagoon artifacts, which are rebuilt on
117
+ * demand and can be large; the lagoon's node_modules is its own build toolchain. */
118
+ export const GITIGNORE = `node_modules/
119
+ dist/
120
+ .motu/
121
+ `;
122
+
123
+ // ---------------------------------------------------------------------------------------------
124
+ // Lagoon root — the part `motu island verify` actually boots
125
+ // ---------------------------------------------------------------------------------------------
126
+
127
+ /** The lagoon's OWN dependencies only.
128
+ *
129
+ * Deliberately does not list @motujs/* or the app package: vite.config resolves those by path (they
130
+ * are unpublished, raw-TS sources), so declaring them here would just be an install that fails.
131
+ * React is also absent on purpose — it resolves upward to the host application's copy, and a second
132
+ * React installed here would give the host two of them and break hooks the moment an island renders
133
+ * a component from the host's own library. */
134
+ export const LAGOON_PACKAGE_JSON = `{
135
+ "name": "{{appPackage}}-lagoon",
136
+ "version": "0.1.0",
137
+ "private": true,
138
+ "type": "module",
139
+ "scripts": {
140
+ "dev": "vite",
141
+ "focus": "vite --open /lagoon.html",
142
+ "build": "vite build"
143
+ },
144
+ "devDependencies": {
145
+ "@vitejs/plugin-react": "^4.3.3",
146
+ "vite": "^5.4.10"{{lagoonExtraDevDeps}}
147
+ }
148
+ }
149
+ `;
150
+
151
+ /**
152
+ * WHAT A PAGE SAYS WHEN ITS OWN BUNDLE NEVER RAN.
153
+ *
154
+ * motu already refuses loudly when a state NAME resolves to nothing: banner, console error, and
155
+ * `__motuLagoonState.ok: false`, because being handed a page you did not ask for is the failure worth
156
+ * engineering against. A module-eval crash had none of that. `startLagoon` never runs, so nothing is
157
+ * left to report it, and the page is WHITE — indistinguishable from a region that renders nothing.
158
+ *
159
+ * That cost an afternoon on a real project: a circular import made one archipelago evaluate after the
160
+ * composition root that used it, the whole single-chunk lagoon died on the ReferenceError, and every
161
+ * region went blank. Auth, the proxy and the host were all suspected first, because a blank page
162
+ * accuses nothing. The console had the answer the entire time.
163
+ *
164
+ * A CLASSIC SCRIPT IN THE HEAD, before the module. Module scripts are deferred, so this is installed
165
+ * and listening before the first line of the bundle is evaluated — which is the only way to catch a
166
+ * crash that happens during evaluation.
167
+ *
168
+ * IT ONLY SPEAKS WHEN THE LAGOON NEVER BOOTED. `__motuLagoonStates` is published by `startLagoon`, so
169
+ * its absence AFTER load is exactly "the bundle did not finish". An error with the catalogue present
170
+ * is an ordinary runtime error in somebody's component, which is theirs to see in the console and not
171
+ * something to paint over the page.
172
+ */
173
+ export const LAGOON_BOOT_GUARD = `<script>
174
+ (function () {
175
+ var first = null;
176
+ var note = function (e) { if (first === null && e) first = e; };
177
+ addEventListener('error', function (e) { note(e.error || e.message); }, true);
178
+ addEventListener('unhandledrejection', function (e) { note(e.reason); });
179
+ function verdict() {
180
+ if (window.__motuLagoonStates || first === null) return;
181
+ var text = (first && (first.stack || first.message)) || String(first);
182
+ var line = String(text).split('\\n')[0];
183
+ window.__motuLagoonState = { ok: false, kind: 'boot', target: 'lagoon', error: line };
184
+ console.error('motu: the lagoon bundle did not boot - nothing on this page is the application.\\n' + text);
185
+ var el = document.createElement('div');
186
+ el.setAttribute('data-motu-boot-error', '');
187
+ el.style.cssText = 'position:fixed;left:0;right:0;top:0;z-index:2147483647;padding:12px 16px;' +
188
+ 'background:#7f1d1d;color:#fff;font:500 13px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;' +
189
+ 'box-shadow:0 2px 12px rgba(0,0,0,.35);white-space:pre-wrap';
190
+ el.textContent = 'motu: this lagoon did not boot - ' + line +
191
+ '\\nNothing below is the application. The whole bundle is one module graph, so one module ' +
192
+ 'throwing takes every region with it.';
193
+ (document.body || document.documentElement).appendChild(el);
194
+ }
195
+ addEventListener('load', function () { setTimeout(verdict, 250); });
196
+ })();
197
+ </script>`;
198
+
199
+ export const LAGOON_INDEX_HTML = `<!doctype html>
200
+ <html lang="en">
201
+ <head>
202
+ <meta charset="UTF-8" />
203
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
204
+ <title>motu lagoon — {{appPackage}}</title>
205
+ ${LAGOON_BOOT_GUARD}
206
+ <style>
207
+ * { box-sizing: border-box; }
208
+ body {
209
+ margin: 0;
210
+ background: #f6f4f0;
211
+ color: #22302c;
212
+ font-family: ui-sans-serif, system-ui, "Segoe UI", Roboto, sans-serif;
213
+ }
214
+ </style>
215
+ </head>
216
+ <body>
217
+ <div id="lagoon-root"></div>
218
+ <script type="module" src="{{galleryEntrySrc}}"></script>
219
+ </body>
220
+ </html>
221
+ `;
222
+
223
+ export const LAGOON_FOCUS_HTML = `<!doctype html>
224
+ <html lang="en">
225
+ <head>
226
+ <meta charset="UTF-8" />
227
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
228
+ <title>motu lagoon — single target (isolated)</title>
229
+ ${LAGOON_BOOT_GUARD}
230
+ <style>
231
+ * { box-sizing: border-box; }
232
+ body {
233
+ margin: 0;
234
+ background: #f6f4f0;
235
+ color: #22302c;
236
+ font-family: ui-sans-serif, system-ui, "Segoe UI", Roboto, sans-serif;
237
+ }
238
+ #lagoon { max-width: 1440px; margin: 22px auto; padding: 0 20px; }
239
+ </style>
240
+ </head>
241
+ <body>
242
+ <div id="lagoon"></div>
243
+ <script type="module" src="{{focusEntrySrc}}"></script>
244
+ </body>
245
+ </html>
246
+ `;
247
+
248
+ /**
249
+ * A `process.env` for the browser, installed before any application module is evaluated.
250
+ *
251
+ * Host modules read process.env at IMPORT time — a Supabase client that throws when its keys are
252
+ * missing takes the whole lagoon down before anything renders, with an error ("process is not
253
+ * defined") that says nothing about why. Vite's `define` does not reach files served from outside the
254
+ * lagoon root, which is exactly where the host application's modules live, so the values are installed
255
+ * at runtime instead. ES modules evaluate in import order, so importing this first is enough.
256
+ */
257
+ export const ENV_SHIM = `// Installs process.env before any host module reads it. MUST be imported first — see below.
258
+ import config from '{{lagoonConfigImport}}';
259
+
260
+ const g = globalThis as unknown as { process?: { env: Record<string, string> } };
261
+ g.process = g.process ?? { env: {} };
262
+ // Declared under "env" in lagoon.config.json. The lagoon has no backend, so these only have to EXIST;
263
+ // never put a real secret here — a published lagoon is a static page anyone with the link can read.
264
+ // \`env\` is OPTIONAL in lagoon.config.json and the scaffolded one does not declare it — so with
265
+ // resolveJsonModule TypeScript infers the literal shape and \`config.env\` is a compile error on a
266
+ // freshly generated project. Read it through a widening cast: the key may or may not be there.
267
+ const declared = (config as { env?: Record<string, string> }).env ?? {};
268
+ Object.assign(g.process.env, { NODE_ENV: 'development', ...declared });
269
+ `;
270
+
271
+ /** Fixture aggregation by glob, so adding an island needs no edit here (the demo app's hand-maintained
272
+ * fixtures.ts drifts by construction — its own header admits the manual step). */
273
+ export const LAGOON_FIXTURES = `// Every island's lagoon EVIDENCE, gathered by glob so adding an island is not also an edit here.
274
+ // A \`fixtures.mock.ts\` that exports \`fixtures\` / \`roles\` is picked up automatically.
275
+ //
276
+ // The same modules carry the project's declared STATES, which used to be dropped here: this file read
277
+ // \`fixtures\` and \`roles\` off each evidence module and ignored \`scenarios\`, so every state the project
278
+ // had written down existed in the node-side checks and nowhere a browser could reach. Passing them to
279
+ // the entry is what makes \`?scenario=\` / \`?flow=\` an address (see @motujs/react's lagoon-states).
280
+ import type { Fixture, RegionScenario, Scenario } from '@motujs/runtime/mock';
281
+
282
+ type FixtureModule = { fixtures?: Fixture[]; roles?: string[]; scenarios?: Scenario[] };
283
+
284
+ const modules = import.meta.glob<FixtureModule>({{fixturesGlob}}, { eager: true });
285
+
286
+ export const ALL_FIXTURES: Fixture[] = Object.values(modules).flatMap((m) => m.fixtures ?? []);
287
+
288
+ export const ALL_ROLES: string[] = [...new Set(Object.values(modules).flatMap((m) => m.roles ?? []))];
289
+
290
+ const EVIDENCE_SUFFIX = '.evidence.ts';
291
+
292
+ /** \`…/week-actions.evidence.ts\` -> \`week-actions\`; \`…/week-actions/fixtures.mock.ts\` -> \`week-actions\`.
293
+ * Derived from the PATH rather than declared: both evidence layouts are named after the island, and a
294
+ * second declaration of which island a file belongs to is a second thing that can be wrong. */
295
+ function ownerOf(path: string): string {
296
+ const parts = path.split('/');
297
+ const file = parts[parts.length - 1] ?? '';
298
+ return file.endsWith(EVIDENCE_SUFFIX) ? file.slice(0, -EVIDENCE_SUFFIX.length) : (parts[parts.length - 2] ?? '');
299
+ }
300
+
301
+ /** Island scenarios by element tag — the states \`?scenario=\` addresses. */
302
+ export const ALL_SCENARIOS: Record<string, Scenario[]> = Object.fromEntries(
303
+ Object.entries(modules)
304
+ .map(([path, m]) => ['{{tagPrefix}}' + ownerOf(path), m.scenarios ?? []] as const)
305
+ .filter(([, list]) => list.length),
306
+ );
307
+
308
+ // A REGION's evidence, which is NOT in the fixtures glob above and must not be: its \`fixtures\` would
309
+ // join the mock transport's corpus and change what every existing check replays. Only the flows are
310
+ // read from it.
311
+ const regions = import.meta.glob<{ scenarios?: RegionScenario[] }>({{flowsGlob}}, { eager: true });
312
+
313
+ /** Region flows by archipelago id — the states \`?flow=\` addresses. */
314
+ export const ALL_FLOWS: Record<string, RegionScenario[]> = Object.fromEntries(
315
+ Object.entries(regions)
316
+ .map(([path, m]) => [ownerOf(path), m.scenarios ?? []] as const)
317
+ .filter(([, list]) => list.length),
318
+ );
319
+ `;
320
+
321
+ /** The focused entry — this is what `motu island verify` drives via MOTU_TARGET. */
322
+ export const LAGOON_FOCUS_ENTRY = `{{envShim}}// Thin lagoon entry: reads the vite-injected target/fit and hands the generic lagoon harness
323
+ // (@motujs/react -> bootstrapLagoon) the project's element registry, fixtures and archipelago resolver.
324
+ // All harness logic is framework-side; this file only supplies app-specific inputs.
325
+ import { bootstrapLagoon } from '@motujs/react';
326
+ import { setDefaultIsolation } from '@motujs/core';
327
+ import { ELEMENT_REGISTRY, ARCHIPELAGOS, getArchipelago } from '{{appPackage}}';
328
+ import css from '{{appPackage}}/styles.css?inline';
329
+ import { ALL_FIXTURES, ALL_ROLES, ALL_SCENARIOS, ALL_FLOWS } from '{{fixturesImport}}';
330
+ import config from '{{lagoonConfigImport}}';
331
+ // The SAME overrides the gallery entry uses. Without them the focused lagoon — the one
332
+ // \`motu island verify\` drives — ran with no region seed, so the checks asserted against defaults
333
+ // while the gallery showed seeded state.
334
+ import * as overrides from '{{overridesImport}}';
335
+ {{hostImport}}{{hostGlobalCss}}
336
+ // Injected by vite.config from the MOTU_* env (see vite.config.ts).
337
+ declare const __MOTU_TARGET__: string; // "island:{{tagPrefix}}some-tag" | "archipelago:some-id"
338
+ declare const __MOTU_FIT__: string; // "native" | "legacy"
339
+ declare const __MOTU_FORCE_ERROR__: string; // "" | "500" | "403" — verify's error-resilience mount
340
+ declare const __MOTU_ISOLATION__: 'shadow' | 'light';
341
+ declare const __MOTU_LEGACY_FIT__: boolean;
342
+
343
+ // Set BEFORE bootstrapLagoon so single-target verify exercises the project's real isolation posture.
344
+ setDefaultIsolation(__MOTU_ISOLATION__);
345
+
346
+ // The target can come from the URL (\`?target=island:x-thing\`), from the build (\`MOTU_TARGET\`), or
347
+ // from nowhere at all. The URL wins because it is what lets ONE dev server answer for every island:
348
+ // the checks navigate instead of booting a server each. Opening lagoon.html by hand passes neither,
349
+ // so fall back to the project's first archipelago rather than to a name the framework guessed.
350
+ const params = typeof location !== 'undefined' ? new URLSearchParams(location.search) : new URLSearchParams();
351
+ const targetFromUrl = params.get('target');
352
+ const target =
353
+ targetFromUrl ||
354
+ (typeof __MOTU_TARGET__ === 'string' && __MOTU_TARGET__) ||
355
+ (Object.keys(ARCHIPELAGOS)[0] ? 'archipelago:' + Object.keys(ARCHIPELAGOS)[0] : '');
356
+
357
+ bootstrapLagoon({
358
+ elements: ELEMENT_REGISTRY,
359
+ css,
360
+ fixtures: ALL_FIXTURES,
361
+ roles: ALL_ROLES,
362
+ resolveArchipelago: getArchipelago,
363
+ // The project's declared states, so this entry can be opened ON one (\`?scenario=\` / \`?flow=\`).
364
+ evidence: { scenarios: ALL_SCENARIOS, flows: ALL_FLOWS },
365
+ // Region seeds/channels, resolved from the target — including an \`island:\` target, whose seed
366
+ // belongs to the region that declares it.
367
+ overrides,
368
+ archipelagos: ARCHIPELAGOS,
369
+ // motu's chrome wears this application's colours. Shared with the gallery through
370
+ // lagoon.config.json so the two entries cannot disagree — override it right here when you want a
371
+ // value the JSON should not carry, e.g. a deliberately darker shade of the brand:
372
+ // chrome: { ...config.chrome, primary: 'color-mix(in srgb, hsl(var(--primary)) 70%, #000)' },
373
+ chrome: config.chrome,
374
+ // Must match how the host application mounts islands, or this verifies a path you do not ship.
375
+ mount: config.mount,
376
+ {{hostOption}} target,
377
+ fit: params.get('fit') || (typeof __MOTU_FIT__ === 'string' ? __MOTU_FIT__ : ''),
378
+ // From the URL, like the target: the wiring/flow checks ask for 'mountpoints' so every declared
379
+ // slot mounts regardless of where the page's own arrangement would put it.
380
+ view: params.get('view') === 'mountpoints' ? 'mountpoints' : undefined,
381
+ // Also from the URL, and for the same reason as the target: the error-resilience check wants ONE
382
+ // island mounted against a failing transport, not a second dev server that differs by an env var.
383
+ forceErrorStatus: (() => {
384
+ const forced = params.get('forceError') || (typeof __MOTU_FORCE_ERROR__ === 'string' ? __MOTU_FORCE_ERROR__ : '');
385
+ return forced ? Number(forced) : undefined;
386
+ })(),
387
+ });
388
+ `;
389
+
390
+ /** `lagoon.config.json` — the project's lagoon, declared. Behaviour lives in @motujs/react. */
391
+ export const LAGOON_CONFIG = `{
392
+ "about": "The {{appPackage}} lagoon: this project's islands rendered against fixtures, with no backend, no session and no host application around them.",
393
+ "transport": "mock",
394
+ "defaultTheme": "motu",
395
+ {{lagoonMount}}{{lagoonChrome}} "stations": {}
396
+ }
397
+ `;
398
+
399
+ /** For a host with shadcn-style tokens, point motu's chrome at the app's own primary by REFERENCE —
400
+ * no value copied here, so a rebrand in the app moves the tooling with it. */
401
+ export const LAGOON_CHROME_SHADCN = ` "chrome": {
402
+ "primary": "hsl(var(--primary))",
403
+ "onPrimary": "hsl(var(--primary-foreground))"
404
+ },
405
+ `;
406
+
407
+ /** `src/lagoon.ts` — the escape hatch, empty by default. */
408
+ export const LAGOON_OVERRIDES = `// Lagoon overrides: what a JSON declaration cannot hold.
409
+ //
410
+ // Everything the lagoon DOES lives in @motujs/react (\`startLagoon\`), and everything it can be TOLD is in
411
+ // ../lagoon.config.json. This file is for the rest — functions and objects — and staying empty is the
412
+ // normal case. Each export below is optional; delete what you do not use.
413
+ import type { LagoonOverrides } from '@motujs/react';
414
+
415
+ // A host bridge for outward intents. The default logs them, which is usually what you want here:
416
+ // seeing "[host] navigate /x" in the console is the proof that an island emitted an intent instead of
417
+ // navigating by itself.
418
+ // export const host: LagoonOverrides['host'] = { navigate: (p) => {}, action: (n, d) => {} };
419
+
420
+ // Everything about one region, in ONE entry: its seed, its arrangement, its inbound channels, the
421
+ // providers its islands cannot render without, and the props the page passes on an island element.
422
+ // Keep each region's own seed and frame in a module beside it and reference them here, so adding a
423
+ // region touches this file on one line.
424
+ // export const regions: LagoonOverrides['regions'] = {
425
+ // someArchipelago: { seed: { criteria: {} }, layout: (island) => <SomePageFrame island={island} /> },
426
+ // };
427
+
428
+ // The same fields may also be given kind-first, keyed by archipelago id — the older shape, still
429
+ // honoured. Where both name a field for one region, \`regions\` wins.
430
+ // export const seed: LagoonOverrides['seed'] = { someArchipelago: { criteria: {} } };
431
+
432
+ // Runs before anything mounts — e.g. standing up a fake host for a foreign-framework island.
433
+ // export const setup: LagoonOverrides['setup'] = () => {};
434
+
435
+ export {};
436
+ `;
437
+
438
+ /** The gallery shim. Deliberately tiny and stable: it is wiring Vite requires in the app, nothing more. */
439
+ export const LAGOON_GALLERY_ENTRY = `{{envShim}}// The lagoon gallery entry.
440
+ //
441
+ // There is no logic here on purpose. The gallery — tide line, seam lens, archipelago switching, the
442
+ // recorded callsite frames — lives in @motujs/react (\`startLagoon\`), so improvements to it arrive with
443
+ // the framework instead of needing this file regenerated. Declare the lagoon in ../lagoon.config.json;
444
+ // override behaviour in ./lagoon.ts.
445
+ //
446
+ // What stays here is only what Vite requires in the app: the build-time defines, \`import.meta.glob\`,
447
+ // the project's own registry, and the debug overlay — which @motujs/react must not depend on, so the app
448
+ // hands it in.
449
+ import { startLagoon } from '@motujs/react';
450
+ import { mountDebugOverlay, toggleDebugOverlay, isDebugOverlayOpen, subscribeDebugOverlay, mountFindings, currentFindings, currentSheet, currentSeams, currentIslands, currentCoupling, currentCoverage, watchSeams, toggleRecording, recordingState, setPicking, setCoupling, couplingOn, onIslandPicked } from '@motujs/debug-overlay';
451
+ import { ELEMENT_REGISTRY, ARCHIPELAGOS } from '{{appPackage}}';
452
+ import css from '{{appPackage}}/styles.css?inline';
453
+ import { ALL_FIXTURES, ALL_ROLES, ALL_SCENARIOS, ALL_FLOWS } from '{{fixturesImport}}';
454
+ import config from '{{lagoonConfigImport}}';
455
+ import * as overrides from '{{overridesImport}}';
456
+ {{hostImport}}{{hostGlobalCss}}
457
+ declare const __MOTU_ISOLATION__: 'shadow' | 'light';
458
+ declare const __MOTU_LEGACY_FIT__: boolean;
459
+ declare const __MOTU_TRANSPORT__: string;
460
+ // "archipelago:<id>" when the artifact was built for one region; '' for the whole gallery.
461
+ declare const __MOTU_TARGET__: string;
462
+ // Present in the lagoon by default (this is the sandbox); MOTU_DEBUG=0 strips the lens entirely.
463
+ declare const __MOTU_DEBUG__: boolean;
464
+
465
+ startLagoon({
466
+ elements: ELEMENT_REGISTRY,
467
+ archipelagos: ARCHIPELAGOS,
468
+ fixtures: ALL_FIXTURES,
469
+ roles: ALL_ROLES,
470
+ css,
471
+ config,
472
+ overrides,
473
+ isolation: __MOTU_ISOLATION__,
474
+ // Whether this host HAS a legacy skin — the fit chip is a choice with one option without it.
475
+ legacyFit: __MOTU_LEGACY_FIT__,
476
+ transport: __MOTU_TRANSPORT__,
477
+ target: __MOTU_TARGET__,
478
+ debug: __MOTU_DEBUG__,
479
+ // Must match the host application (and the focused entry), or the lagoon shows a mount path the
480
+ // project does not ship.
481
+ mount: config.mount,
482
+ lens: {
483
+ mount: mountDebugOverlay,
484
+ toggle: toggleDebugOverlay,
485
+ isOpen: isDebugOverlayOpen,
486
+ subscribe: subscribeDebugOverlay,
487
+ mountFindings,
488
+ findings: currentFindings,
489
+ sheet: currentSheet,
490
+ seams: currentSeams,
491
+ islands: currentIslands,
492
+ coupling: currentCoupling,
493
+ coverage: currentCoverage,
494
+ toggleRecording,
495
+ recordingState,
496
+ setPicking,
497
+ // What the crosshair chose, so the dock can scope the lagoon to it.
498
+ onPicked: onIslandPicked,
499
+ setCoupling,
500
+ couplingOn,
501
+ watch: watchSeams,
502
+ },
503
+ // Callsite frame stylesheets (\`roots/lagoon/src/frames/*.css\`). The glob has to be written here:
504
+ // Vite resolves it statically, at this file's location.
505
+ // BOTH, and the scenarios are not optional here. This is the entry \`lagoon serve\` and
506
+ // \`lagoon publish\` build, so an address it cannot resolve is an address that works nowhere a human
507
+ // looks — which is what \`?target=island:…&scenario=…\` was until this line carried scenarios too.
508
+ evidence: { scenarios: ALL_SCENARIOS, flows: ALL_FLOWS },
509
+ frames: import.meta.glob('{{framesGlob}}', { query: '?inline', import: 'default', eager: true }),
510
+ });
511
+ `;
512
+
513
+ /** The lagoon's Tailwind config: the host's, re-anchored so it works from a different directory. */
514
+ export const NEXT_TAILWIND_CONFIG = `// The lagoon's Tailwind setup — the HOST's config, with one thing changed.
515
+ //
516
+ // Tailwind resolves \`content\` globs against the current working directory, and here that is the lagoon
517
+ // rather than the app. The host's own './components/**' globs would therefore match nothing, Tailwind
518
+ // would emit no utilities at all, and every island would render unstyled — a preview that quietly lies
519
+ // about how the component looks in the app. Re-anchoring the globs on the host root fixes that, and the
520
+ // island sources are added so a class used ONLY inside an island still gets generated.
521
+ //
522
+ // Everything else (theme, plugins, dark mode, breakpoints) is inherited, so the lagoon and the app
523
+ // cannot drift apart on design tokens.
524
+ import { resolve } from 'node:path';
525
+ import hostConfig from '{{hostRootFromLagoon}}/tailwind.config';
526
+
527
+ const HOST_ROOT = resolve(__dirname, '{{hostRootFromLagoon}}');
528
+
529
+ const hostContent = Array.isArray(hostConfig.content) ? hostConfig.content : (hostConfig.content?.files ?? []);
530
+
531
+ export default {
532
+ ...hostConfig,
533
+ content: [
534
+ ...hostContent.map((glob) => (typeof glob === 'string' ? resolve(HOST_ROOT, glob) : glob)),
535
+ resolve(__dirname, '../../src/**/*.{ts,tsx}'),
536
+ ],
537
+ };
538
+ `;
539
+
540
+ // --- host: next -------------------------------------------------------------------------------
541
+
542
+ /** Next's own modules have no meaning in a plain Vite SPA — the lagoon stubs them so a client
543
+ * component that links or routes still mounts. Kept deliberately small: anything an island needs
544
+ * beyond this is a sign it is reaching for the host instead of emitting an intent. */
545
+ export const NEXT_STUBS = `// Vite aliases point next/* here: the lagoon is a plain SPA with no Next runtime, but an island is
546
+ // still allowed to render a link or read the router. Navigation intents belong to the host bridge
547
+ // (@motujs/adapter-next), so these stubs are inert on purpose — they render and no-op, they never
548
+ // navigate. If an island needs more of Next than this, it is coupling to the host too tightly.
549
+ import { createElement, forwardRef, type ComponentProps } from 'react';
550
+
551
+ export const Link = forwardRef<HTMLAnchorElement, ComponentProps<'a'> & { href?: unknown }>(
552
+ function Link({ href, children, ...rest }, ref) {
553
+ return createElement('a', { ...rest, ref, href: typeof href === 'string' ? href : '#' }, children);
554
+ },
555
+ );
556
+
557
+ export const Image = forwardRef<HTMLImageElement, ComponentProps<'img'>>(function Image(props, ref) {
558
+ return createElement('img', { ...props, ref });
559
+ });
560
+
561
+ const noop = () => {};
562
+
563
+ export function useRouter() {
564
+ return { push: noop, replace: noop, back: noop, forward: noop, refresh: noop, prefetch: noop };
565
+ }
566
+
567
+ export function usePathname() {
568
+ return '/';
569
+ }
570
+
571
+ export function useSearchParams() {
572
+ return new URLSearchParams();
573
+ }
574
+
575
+ export function useParams() {
576
+ return {} as Record<string, string>;
577
+ }
578
+
579
+ export default Link;
580
+ `;
581
+
582
+ export const NEXT_VITE_IMPORTS = `import tailwindcss from 'tailwindcss';
583
+ import autoprefixer from 'autoprefixer';
584
+ `;
585
+
586
+ /** Alias fragment for a Next host: the app's own '@/…' alias plus inert next/* stubs, so the lagoon
587
+ * can mount components written against the host's conventions. */
588
+ export const NEXT_VITE_ALIASES = ` // The lagoon mounts components written for the Next app, so it speaks the same module language:
589
+ // the host's '@/…' alias, and next/* mapped to inert stubs (see src/next-stubs.tsx).
590
+ { find: /^@\\//, replacement: resolve(__dirname, '{{hostRootFromLagoon}}') + '/' },
591
+ { find: /^next\\/link$/, replacement: resolve(__dirname, 'src/next-stubs.tsx') },
592
+ { find: /^next\\/image$/, replacement: resolve(__dirname, 'src/next-stubs.tsx') },
593
+ { find: /^next\\/navigation$/, replacement: resolve(__dirname, 'src/next-stubs.tsx') },
594
+ `;
595
+
596
+ /** Tailwind is the HOST's, not motu's: point postcss at the host config so an island's utility
597
+ * classes render in the lagoon exactly as they do in the app. */
598
+ export const NEXT_VITE_CSS = ` // Tailwind is the HOST's. It is loaded through the lagoon's own tailwind.config.ts (which extends the
599
+ // app's) rather than imported here, because Tailwind's loader handles a config that \`require()\`s CJS
600
+ // plugins and vite's ESM config bundling does not.
601
+ css: {
602
+ postcss: {
603
+ plugins: [tailwindcss({ config: resolve(__dirname, 'tailwind.config.ts') }), autoprefixer()],
604
+ },
605
+ },
606
+ `;