@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.
- package/LICENSE +21 -0
- package/bundled/github/agents/archipelago-build.agent.md +298 -0
- package/bundled/github/agents/island-create.agent.md +261 -0
- package/bundled/github/agents/island-extract.agent.md +360 -0
- package/bundled/github/agents/island-locate.agent.md +208 -0
- package/bundled/github/agents/motu-checks.agent.md +158 -0
- package/bundled/github/agents/motu-evidence.agent.md +267 -0
- package/bundled/github/agents/motu-regions.agent.md +304 -0
- package/bundled/github/host-rules.md +43 -0
- package/bundled/github/prompts/archipelago-build.prompt.md +19 -0
- package/bundled/github/prompts/island-create.prompt.md +37 -0
- package/bundled/github/prompts/island-extract.prompt.md +59 -0
- package/bundled/github/prompts/island-locate.prompt.md +43 -0
- package/bundled/packages/adapters/angularjs/vite.mjs +43 -0
- package/bundled/packages/adapters/next/lagoon/next-stubs.tsx +108 -0
- package/bundled/packages/adapters/next/vite.mjs +121 -0
- package/bundled/packages/adapters/vite/vite.mjs +179 -0
- package/package.json +64 -3
- package/src/alias-loader.mjs +63 -0
- package/src/alias-register.mjs +28 -0
- package/src/asset-stub.cjs +3 -0
- package/src/asset-stub.mjs +5 -0
- package/src/cli.mjs +78 -0
- package/src/commands/adopt-root.mjs +194 -0
- package/src/commands/archipelago-init.mjs +314 -0
- package/src/commands/archipelago.mjs +138 -0
- package/src/commands/assurance.mjs +40 -0
- package/src/commands/check.mjs +346 -0
- package/src/commands/codegen.mjs +29 -0
- package/src/commands/coverage.mjs +358 -0
- package/src/commands/create.mjs +277 -0
- package/src/commands/defaults.mjs +129 -0
- package/src/commands/diff.mjs +595 -0
- package/src/commands/fixtures.mjs +153 -0
- package/src/commands/init.mjs +473 -0
- package/src/commands/integrate.mjs +208 -0
- package/src/commands/integration.mjs +1349 -0
- package/src/commands/lagoon-try.mjs +272 -0
- package/src/commands/lagoon.mjs +1419 -0
- package/src/commands/link.mjs +110 -0
- package/src/commands/region-coverage.mjs +604 -0
- package/src/commands/removal-check.mjs +878 -0
- package/src/commands/skills.mjs +156 -0
- package/src/commands/snapshot.mjs +456 -0
- package/src/commands/sync.mjs +38 -0
- package/src/commands/verify/island-runtime.mjs +581 -0
- package/src/commands/verify/island-static.mjs +586 -0
- package/src/commands/verify/island.mjs +135 -0
- package/src/commands/verify/reach.mjs +426 -0
- package/src/commands/verify/region-composition.mjs +391 -0
- package/src/commands/verify/region-config.mjs +330 -0
- package/src/commands/verify/region-flows.mjs +760 -0
- package/src/commands/verify/region-sources.mjs +516 -0
- package/src/commands/verify/region.mjs +271 -0
- package/src/commands/verify/report.mjs +338 -0
- package/src/commands/verify/screens-runtime.mjs +123 -0
- package/src/commands/verify.mjs +18 -0
- package/src/lagoon-build.mjs +25 -0
- package/src/lagoon-dev.mjs +32 -0
- package/src/lib/alias-core.mjs +49 -0
- package/src/lib/archipelagos.mjs +111 -0
- package/src/lib/assurance.mjs +144 -0
- package/src/lib/baselines.mjs +107 -0
- package/src/lib/bundlability.mjs +123 -0
- package/src/lib/changed.mjs +157 -0
- package/src/lib/component-props.mjs +145 -0
- package/src/lib/config.mjs +451 -0
- package/src/lib/contracts.mjs +244 -0
- package/src/lib/data-flow.mjs +47 -0
- package/src/lib/declared-sources.mjs +101 -0
- package/src/lib/dock-inject.mjs +47 -0
- package/src/lib/effects.mjs +97 -0
- package/src/lib/eject.mjs +715 -0
- package/src/lib/evidence-json.mjs +74 -0
- package/src/lib/expect-calls.mjs +53 -0
- package/src/lib/finding-memory.mjs +0 -0
- package/src/lib/flow-calls.mjs +53 -0
- package/src/lib/framework.mjs +98 -0
- package/src/lib/host-rules.mjs +55 -0
- package/src/lib/host-sources.mjs +121 -0
- package/src/lib/island-placement.mjs +64 -0
- package/src/lib/island-render.mjs +105 -0
- package/src/lib/islands.mjs +112 -0
- package/src/lib/lagoon-bundle.mjs +138 -0
- package/src/lib/lagoon-declares.mjs +553 -0
- package/src/lib/lagoon-materialize.mjs +217 -0
- package/src/lib/lagoon-vite.mjs +788 -0
- package/src/lib/live-url.mjs +53 -0
- package/src/lib/node-aliases.mjs +73 -0
- package/src/lib/operations.mjs +145 -0
- package/src/lib/provenance-plugin.mjs +74 -0
- package/src/lib/region-refs.mjs +40 -0
- package/src/lib/remote.mjs +171 -0
- package/src/lib/scaffold.mjs +606 -0
- package/src/lib/screens.mjs +266 -0
- package/src/lib/snapshots.mjs +85 -0
- package/src/lib/stubs.mjs +114 -0
- package/src/lib/ts-project.mjs +42 -0
- package/src/lib/util.mjs +626 -0
- package/src/lib/worlds.mjs +236 -0
- package/src/lib/write-if-changed.mjs +35 -0
- package/src/lib/writes-coverage.mjs +32 -0
- package/src/playwright-lagoon.mjs +2305 -0
- package/src/run.mjs +385 -0
- package/src/runtime-harness.mjs +225 -0
- package/README.md +0 -3
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// The lagoon's ENTRIES, rendered by the framework into `.motu/cache` — when the project does not own
|
|
2
|
+
// them.
|
|
3
|
+
//
|
|
4
|
+
// The rule is ownership, not generation: **if the project has its own `index.html`, the project owns
|
|
5
|
+
// its lagoon root and nothing here runs.** That is not a courtesy. The reference ocean's `main.tsx` is
|
|
6
|
+
// ~180 lines of real composition — transport switching between MockTransport and a session-authenticated
|
|
7
|
+
// HttpTransport, an AngularJS host stood up for extracted islands, per-archipelago channels and seeds,
|
|
8
|
+
// tide-line stations — none of which a template could produce. A project whose entries ARE the template
|
|
9
|
+
// (every `motu init --host next` project) carries them for no reason, and those are the ones this
|
|
10
|
+
// deletes.
|
|
11
|
+
//
|
|
12
|
+
// So `motu lagoon eject` is not a special code path: it is this module writing into the project
|
|
13
|
+
// instead of into the cache.
|
|
14
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync } from 'node:fs';
|
|
15
|
+
import { resolve, relative, dirname } from 'node:path';
|
|
16
|
+
import {
|
|
17
|
+
render,
|
|
18
|
+
LAGOON_INDEX_HTML,
|
|
19
|
+
LAGOON_FOCUS_HTML,
|
|
20
|
+
LAGOON_FIXTURES,
|
|
21
|
+
LAGOON_FOCUS_ENTRY,
|
|
22
|
+
LAGOON_GALLERY_ENTRY,
|
|
23
|
+
LAGOON_OVERRIDES,
|
|
24
|
+
ENV_SHIM,
|
|
25
|
+
} from './scaffold.mjs';
|
|
26
|
+
// Every entry below is re-rendered on EVERY lagoon build and is almost always identical. See the
|
|
27
|
+
// module's own doc for why an identical write is not free: Vite watches these, and a touch full-reloads
|
|
28
|
+
// every open lagoon tab.
|
|
29
|
+
import { writeIfChanged } from './write-if-changed.mjs';
|
|
30
|
+
|
|
31
|
+
/** POSIX-style relative path for generated code (never a Windows backslash in a module specifier). */
|
|
32
|
+
function relPosix(from, to) {
|
|
33
|
+
const r = relative(from, to).split('\\').join('/');
|
|
34
|
+
return r === '' ? '.' : r.startsWith('.') ? r : './' + r;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The host application's own global stylesheet — without it the lagoon has no theme tokens and every
|
|
38
|
+
* island renders unstyled, a preview that lies about how the component looks in the app. */
|
|
39
|
+
const HOST_GLOBAL_CSS = ['app/globals.css', 'src/app/globals.css', 'styles/globals.css', 'src/styles/globals.css'];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The stylesheet that DECLARES Tailwind, found by content when it is not at a conventional path.
|
|
43
|
+
*
|
|
44
|
+
* Tailwind v4 moved the config into CSS (`@import "tailwindcss"`, `@theme { … }`) and dropped
|
|
45
|
+
* `tailwind.config.js` — so a v4 project has no config file to detect AND commonly keeps its global
|
|
46
|
+
* stylesheet outside the four paths above. Measured on a cold adoption of a v4 Next app whose CSS
|
|
47
|
+
* lives at `modules/ui/globals.css`: motu found neither, wired no styles, and the lagoon rendered
|
|
48
|
+
* every island with correct text, structure and coupling and NO styling at all — silently. The agent
|
|
49
|
+
* who hit it could not tell a preview bug from an application bug, which is the one thing a preview
|
|
50
|
+
* must never make ambiguous.
|
|
51
|
+
*
|
|
52
|
+
* Bounded, shallow, and skipping the obvious noise: this runs on every lagoon build.
|
|
53
|
+
*/
|
|
54
|
+
function findTailwindCss(hostRoot, depth = 4) {
|
|
55
|
+
const SKIP = new Set(['node_modules', '.next', '.git', 'dist', 'build', 'coverage', '.motu', '.turbo']);
|
|
56
|
+
const queue = [{ dir: hostRoot, d: 0 }];
|
|
57
|
+
while (queue.length) {
|
|
58
|
+
const { dir, d } = queue.shift();
|
|
59
|
+
let entries;
|
|
60
|
+
try {
|
|
61
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
62
|
+
} catch {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
for (const e of entries) {
|
|
66
|
+
if (e.isDirectory()) {
|
|
67
|
+
if (d < depth && !SKIP.has(e.name) && !e.name.startsWith('.')) queue.push({ dir: resolve(dir, e.name), d: d + 1 });
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (!e.name.endsWith('.css')) continue;
|
|
71
|
+
const abs = resolve(dir, e.name);
|
|
72
|
+
try {
|
|
73
|
+
const css = readFileSync(abs, 'utf8');
|
|
74
|
+
if (/@import\s+['"]tailwindcss|@tailwind\s+(base|utilities)/.test(css)) return abs;
|
|
75
|
+
} catch {}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function hostGlobalCssImport(fromDir, hostRoot) {
|
|
82
|
+
for (const rel of HOST_GLOBAL_CSS) {
|
|
83
|
+
const abs = resolve(hostRoot, rel);
|
|
84
|
+
if (existsSync(abs)) return `import '${relPosix(fromDir, abs)}';\n`;
|
|
85
|
+
}
|
|
86
|
+
const found = findTailwindCss(hostRoot);
|
|
87
|
+
if (found) return `import '${relPosix(fromDir, found)}';\n`;
|
|
88
|
+
return '';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Does this project own its lagoon entries? `index.html` is the marker: it is what Vite's root means. */
|
|
92
|
+
export function projectOwnsLagoon(paths) {
|
|
93
|
+
return existsSync(resolve(paths.lagoonDir, 'index.html'));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Render the entries into `outDir`, wiring every import back to the files the PROJECT still owns
|
|
98
|
+
* (`lagoon.config.json`, `src/lagoon.ts`, `src/host-stubs/`, the islands' fixtures, the host's global
|
|
99
|
+
* CSS). Returns the directory to use as Vite's root.
|
|
100
|
+
*/
|
|
101
|
+
export function materializeLagoon(paths, outDir) {
|
|
102
|
+
const src = resolve(outDir, 'src');
|
|
103
|
+
mkdirSync(src, { recursive: true });
|
|
104
|
+
|
|
105
|
+
// The project keeps its overrides (region seeds). A project without them gets an empty stub, so the
|
|
106
|
+
// generated gallery entry's import resolves either way.
|
|
107
|
+
// `.tsx` when the project's overrides carry JSX — a region layout does.
|
|
108
|
+
const overrideCandidates = ['src/lagoon.tsx', 'src/lagoon.ts'].map((f) => resolve(paths.lagoonDir, f));
|
|
109
|
+
const projectOverrides = overrideCandidates.find((f) => existsSync(f)) ?? overrideCandidates[1];
|
|
110
|
+
if (!existsSync(projectOverrides)) writeIfChanged(resolve(src, 'lagoon.ts'), LAGOON_OVERRIDES);
|
|
111
|
+
|
|
112
|
+
const vars = {
|
|
113
|
+
appPackage: paths.appPackage,
|
|
114
|
+
tagPrefix: paths.tagPrefix,
|
|
115
|
+
// A glob cannot go through a tsconfig alias reliably, so it is baked relative to the file.
|
|
116
|
+
// Both evidence layouts. Vite accepts an array of patterns, so a project can migrate island by
|
|
117
|
+
// island without the lagoon losing sight of the ones that have not moved yet.
|
|
118
|
+
fixturesGlob: `['${relPosix(src, paths.islandsDir)}/*.evidence.ts', '${relPosix(src, paths.islandsDir)}/*/fixtures.mock.ts']`,
|
|
119
|
+
// A REGION's evidence, kept in its own glob: it holds the flows a URL can address, and its
|
|
120
|
+
// `fixtures` must NOT join the mock transport's corpus (that would change what every existing
|
|
121
|
+
// check replays, from a change that was supposed to only add an address).
|
|
122
|
+
flowsGlob: `'${relPosix(src, paths.archipelagosDir)}/*/*.evidence.ts'`,
|
|
123
|
+
framesGlob: relPosix(src, resolve(paths.lagoonDir, 'src/frames')) + '/*.css',
|
|
124
|
+
lagoonConfigImport: relPosix(src, resolve(paths.lagoonDir, 'lagoon.config.json')),
|
|
125
|
+
overridesImport: existsSync(projectOverrides) ? relPosix(src, projectOverrides) : './lagoon.js',
|
|
126
|
+
fixturesImport: './fixtures.js',
|
|
127
|
+
galleryEntrySrc: '/src/main.tsx',
|
|
128
|
+
focusEntrySrc: '/src/lagoon.tsx',
|
|
129
|
+
hostGlobalCss: hostGlobalCssImport(src, paths.hostRoot),
|
|
130
|
+
// FIRST import in both entries: host modules read process.env while being imported, so the shim
|
|
131
|
+
// has to land before any of them is evaluated. An ocean has no such modules and needs no shim.
|
|
132
|
+
envShim: paths.host === 'angularjs' ? '' : "import './env.js';\n",
|
|
133
|
+
hostImport: paths.host === 'angularjs' ? "import { angularHostScopeChannel } from '@motujs/adapter-angularjs';\n" : '',
|
|
134
|
+
hostOption: '',
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
writeIfChanged(resolve(outDir, 'index.html'), render(LAGOON_INDEX_HTML, vars));
|
|
138
|
+
writeIfChanged(resolve(outDir, 'lagoon.html'), render(LAGOON_FOCUS_HTML, vars));
|
|
139
|
+
writeIfChanged(resolve(src, 'fixtures.ts'), render(LAGOON_FIXTURES, vars));
|
|
140
|
+
writeIfChanged(resolve(src, 'main.tsx'), render(LAGOON_GALLERY_ENTRY, vars));
|
|
141
|
+
writeIfChanged(resolve(src, 'lagoon.tsx'), render(LAGOON_FOCUS_ENTRY, vars));
|
|
142
|
+
if (paths.host !== 'angularjs') writeIfChanged(resolve(src, 'env.ts'), render(ENV_SHIM, vars));
|
|
143
|
+
|
|
144
|
+
return outDir;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Vite's root for this project: its own lagoon dir if it owns one, else a freshly rendered cache. */
|
|
148
|
+
export function resolveLagoonRoot(paths) {
|
|
149
|
+
if (projectOwnsLagoon(paths)) return paths.lagoonDir;
|
|
150
|
+
return materializeLagoon(paths, resolve(paths.cacheDir, 'lagoon'));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* WHAT AN EJECTED ENTRY IS MISSING, compared with the one motu would write today.
|
|
155
|
+
*
|
|
156
|
+
* Ejecting hands the project its entry and stops the materializer: "the project now owns these —
|
|
157
|
+
* motu will not regenerate them." That is the point of the escape hatch, and it has a cost nobody was
|
|
158
|
+
* told about. Every capability added to the scaffold afterwards reaches an ejected project only if
|
|
159
|
+
* somebody edits that file by hand, and nothing said when they had not.
|
|
160
|
+
*
|
|
161
|
+
* It has already happened twice in this repo's own demo-app: `evidence` (so `?scenario=` addressed a
|
|
162
|
+
* catalogue that was never handed over — every declared state unreachable in a browser) and
|
|
163
|
+
* `lens.onPicked` (so the crosshair could not scope). Both were silent: the entry compiled, the
|
|
164
|
+
* lagoon booted, and the feature simply was not there.
|
|
165
|
+
*
|
|
166
|
+
* DERIVED FROM THE TEMPLATE, never a hand-kept list, or this check becomes the next thing that goes
|
|
167
|
+
* stale. Compares the option keys `startLagoon({...})` is given, and the keys of the `lens` object
|
|
168
|
+
* inside it — the two places options actually arrive.
|
|
169
|
+
*/
|
|
170
|
+
export function ejectedEntryGaps(paths) {
|
|
171
|
+
const entry = resolve(paths.lagoonDir, 'src', 'main.tsx');
|
|
172
|
+
if (!projectOwnsLagoon(paths) || !existsSync(entry)) return null;
|
|
173
|
+
|
|
174
|
+
// NAME-PRESENCE, not shape. Parsing both sides was the first attempt and it was wrong twice over:
|
|
175
|
+
// it read an `import.meta.glob` options object as lagoon options, and it missed demo-app's
|
|
176
|
+
// `lens: __MOTU_DEBUG__ ? {…} : undefined` because that is not a literal `lens: {`. An ejected entry
|
|
177
|
+
// is a file somebody has been editing — it may wrap, rename or conditionalise anything — so the
|
|
178
|
+
// only question worth asking cheaply is whether the capability is MENTIONED at all. That is enough
|
|
179
|
+
// to catch what actually goes wrong here (an option added upstream that nobody copied down), and it
|
|
180
|
+
// cannot cry wolf over formatting.
|
|
181
|
+
// FULL-LINE COMMENTS ONLY. Stripping block comments with a regex ate real code here (a `/*` in a
|
|
182
|
+
// string closed against a later `*/`), and prose is where a key name would otherwise be mistaken
|
|
183
|
+
// for a use — this file's own comments mention `target` and `scenario` while explaining them.
|
|
184
|
+
const strip = (t) => t.replace(/^\s*\/\/.*$/gm, '');
|
|
185
|
+
const mine = strip(readFileSync(entry, 'utf8'));
|
|
186
|
+
const template = strip(LAGOON_GALLERY_ENTRY);
|
|
187
|
+
|
|
188
|
+
const optionNames = (text, opener) => {
|
|
189
|
+
const at = text.indexOf(opener);
|
|
190
|
+
if (at === -1) return [];
|
|
191
|
+
const out = new Set();
|
|
192
|
+
let depth = 0;
|
|
193
|
+
for (let i = at + opener.length - 1; i < text.length; i++) {
|
|
194
|
+
const c = text[i];
|
|
195
|
+
if (c === '{' || c === '(' || c === '[') depth++;
|
|
196
|
+
else if (c === '}' || c === ')' || c === ']') {
|
|
197
|
+
if (--depth === 0) break;
|
|
198
|
+
} else if (depth === 1 && /[A-Za-z_$]/.test(c)) {
|
|
199
|
+
const before = text.slice(0, i).replace(/\s+$/, '').slice(-1);
|
|
200
|
+
const m = /^[A-Za-z_$][\w$]*/.exec(text.slice(i));
|
|
201
|
+
if (m && (before === '{' || before === ',')) {
|
|
202
|
+
const after = text.slice(i + m[0].length).replace(/^\s+/, '').slice(0, 1);
|
|
203
|
+
if (after === ':' || after === ',' || after === '}') out.add(m[0]);
|
|
204
|
+
i += m[0].length - 1;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return [...out];
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const mentions = (name) => new RegExp(`\\b${name}\\b`).test(mine);
|
|
212
|
+
const expected = [...optionNames(template, 'startLagoon({'), ...optionNames(template, 'lens: {')];
|
|
213
|
+
return {
|
|
214
|
+
entry: relative(process.cwd(), entry),
|
|
215
|
+
missing: [...new Set(expected)].filter((k) => !mentions(k)).sort(),
|
|
216
|
+
};
|
|
217
|
+
}
|