@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,451 @@
|
|
|
1
|
+
// motu project configuration. The CLI is layout-agnostic: WHERE each piece lands (islands, ui,
|
|
2
|
+
// archipelagos, contract, lagoon, bridge, the backend manifest) is declared here, not hardcoded, so
|
|
3
|
+
// motu adapts to any codebase filesystem. WHAT lives inside those roots stays motu's convention
|
|
4
|
+
// (islands/<kebab>/{element.ts,fixtures.mock.ts,index.ts} + ui/<kebab>/<Pascal>.tsx) — that's what
|
|
5
|
+
// keeps `motu island verify`/`create` deterministic.
|
|
6
|
+
//
|
|
7
|
+
// Config is discovered by walking UP from the cwd for the first `motu.config.json` (or a `motu` key
|
|
8
|
+
// in a package.json); that file's directory is the PROJECT ROOT. All paths default to the reference
|
|
9
|
+
// layout, so a project matching it needs no config at all.
|
|
10
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
11
|
+
import { resolve, dirname, basename } from 'node:path';
|
|
12
|
+
import { CHECKOUT_ROOT, CLI_PACKAGE_ROOT, FRAMEWORK_MODE } from './framework.mjs';
|
|
13
|
+
|
|
14
|
+
// THE LAYOUT KEYS. Every one is relative to the APP root (root/<app>) except `manifest`, which is
|
|
15
|
+
// relative to the PROJECT root — see its comment; that asymmetry is load-bearing and is the single
|
|
16
|
+
// most common thing to get wrong here.
|
|
17
|
+
//
|
|
18
|
+
// Each key below says what READS it and what BREAKS when it points somewhere wrong, because "where
|
|
19
|
+
// the islands live" is obvious and "which command silently finds nothing" is not. A path key that is
|
|
20
|
+
// merely wrong rarely errors: the directory walk returns an empty list and the check that depended on
|
|
21
|
+
// it reports a pass over nothing, which is the failure mode `report.ok(check, msg, seen)` and the
|
|
22
|
+
// `seen: 0` -> skip rule exist to catch.
|
|
23
|
+
const DEFAULTS = {
|
|
24
|
+
// The frontend app root, relative to the project root. EVERY path below resolves through it
|
|
25
|
+
// (`inApp`), so this is the one key that moves all the others at once. Point it at a directory that
|
|
26
|
+
// is not the app and every subsequent walk finds nothing — no error, just empty results everywhere.
|
|
27
|
+
app: '.',
|
|
28
|
+
// Island mount points, plus the two generated files beside them (`registry.ts`,
|
|
29
|
+
// `contracts.generated.ts`). Read by `island create/sync/verify/snapshot`, by `check`, by
|
|
30
|
+
// `--changed`'s file->island attribution (`lib/changed.mjs:87`), and by the lagoon's fixture glob
|
|
31
|
+
// (`lib/lagoon-materialize.mjs:72`). Wrong, and `island verify --all` verifies zero islands
|
|
32
|
+
// successfully.
|
|
33
|
+
islands: 'src/islands',
|
|
34
|
+
// The components the islands mount — `ui/<kebab>/<Pascal>.tsx`. Kept OUTSIDE `islands/` on purpose:
|
|
35
|
+
// mount points must not be able to import each other, and a directory boundary is what enforces it
|
|
36
|
+
// (`util.mjs:29-31`). Read by `island create`, by `island verify`'s component resolution
|
|
37
|
+
// (`verify.mjs:1191`), and by `integrate check`'s "what does motu own" set (`integration.mjs:51`).
|
|
38
|
+
ui: 'src/ui',
|
|
39
|
+
// Regions: `<id>.archipelago.ts`, `<id>.evidence.ts`, `registry.ts`, `coverage.generated.ts`. Read
|
|
40
|
+
// by every `archipelago` verb, by `integrate check` (`integration.mjs:845`) and by `--changed`'s
|
|
41
|
+
// file->region attribution (`changed.mjs:100`). Wrong, and a project reports zero regions rather
|
|
42
|
+
// than failing to find them.
|
|
43
|
+
archipelagos: 'src/archipelagos',
|
|
44
|
+
// Holds exactly one file motu cares about: `<shared>/styles.css`, the single island stylesheet the
|
|
45
|
+
// lagoon aliases as `<appPackage>/styles.css` (`lagoon-vite.mjs:212`). Wrong, and the lagoon builds
|
|
46
|
+
// fine and renders every island unstyled.
|
|
47
|
+
shared: 'src/shared',
|
|
48
|
+
// The module that exports ELEMENT_REGISTRY. It is the app's PUBLIC entry: the runtime harness
|
|
49
|
+
// imports it by path (`runtime-harness.mjs:88`), the lagoon aliases the bare package specifier onto
|
|
50
|
+
// it (`lagoon-vite.mjs:213`), and `archipelago init` edits it to add re-exports
|
|
51
|
+
// (`archipelago.mjs:97`). Wrong, and every `--runtime` check fails to import the registry at all.
|
|
52
|
+
barrel: 'src/index.ts',
|
|
53
|
+
// Output directory of `motu codegen` — where the generated `@motujs/contract` package is written, and
|
|
54
|
+
// whose `index.ts` is the contract entry the lagoon aliases (`util.mjs:78`). Read only by
|
|
55
|
+
// `codegen.mjs:10`, as the default when no out-dir argument is given.
|
|
56
|
+
contract: 'contract/src',
|
|
57
|
+
// The lagoon root: its entries and `lagoon.config.json` (viewports, a11y policy, chrome, mount).
|
|
58
|
+
// Read by every `lagoon` verb, by every `--runtime` check, and by the materializer, which decides
|
|
59
|
+
// whether the PROJECT owns its lagoon by looking for `index.html` here
|
|
60
|
+
// (`lagoon-materialize.mjs:47`). Wrong, and motu materializes a second lagoon beside the real one.
|
|
61
|
+
lagoon: 'roots/lagoon',
|
|
62
|
+
// The ocean composition root — `bridge.js`, the embedded IIFE injected into legacy pages.
|
|
63
|
+
//
|
|
64
|
+
// DECLARED, NOT DRIVEN. `motu init` writes it for `angularjs` hosts only (`init.mjs:180`), and it is
|
|
65
|
+
// resolved to `paths.bridgeDir` — but NO CLI command reads that path. The bridge is built by the
|
|
66
|
+
// project's own vite (`pnpm --filter @demo-app/bridge build`), because it is an application
|
|
67
|
+
// artifact with an application's dependencies, not something motu generates. The key exists so the
|
|
68
|
+
// layout is declared in one place rather than assumed by whoever writes the build script.
|
|
69
|
+
//
|
|
70
|
+
// So: changing it moves nothing on its own. Move the directory and update your build script too.
|
|
71
|
+
bridge: 'roots/bridge',
|
|
72
|
+
// THE ONE PATH RELATIVE TO THE PROJECT ROOT, not the app root. The backend build emits it — for the
|
|
73
|
+
// reference ocean, a Maven build in a sibling tree — so it routinely sits outside the frontend app
|
|
74
|
+
// and could not resolve through `inApp` (`config.mjs`'s `manifest:` line uses `resolve(root, …)`).
|
|
75
|
+
// Read only by `codegen.mjs:9`, as the default input when no manifest argument is given.
|
|
76
|
+
manifest: 'target/motu-manifest.json',
|
|
77
|
+
// Prefix of every custom element tag: `names(x).tag = tagPrefix + kebab` (`util.mjs:359`). It exists
|
|
78
|
+
// because a custom element name MUST contain a hyphen, and because a host page needs a namespace it
|
|
79
|
+
// can reserve. Read by `island create` and `verify`, by the lagoon materializer
|
|
80
|
+
// (`lagoon-materialize.mjs:68`), and by the tag->island reverse lookup (`verify.mjs:2899`).
|
|
81
|
+
//
|
|
82
|
+
// CHANGING IT ON A LIVE PROJECT RENAMES EVERY TAG. The registry is regenerated with new tags while
|
|
83
|
+
// the host's own markup still says the old ones, so every island silently fails to upgrade — a
|
|
84
|
+
// custom element that is not defined renders as an empty unknown element, with no error.
|
|
85
|
+
tagPrefix: 'x-',
|
|
86
|
+
// Shadow or light, project-wide. Baked into the generated registry as `setDefaultIsolation(...)`
|
|
87
|
+
// (`islands.mjs:78-92`); an `isolation` attribute on <motu-archipelago> still wins per region
|
|
88
|
+
// (`archipelago-element.ts:100-103`).
|
|
89
|
+
//
|
|
90
|
+
// 'shadow' gives a region one shadow root and one adopted stylesheet — right for an ocean, whose
|
|
91
|
+
// global CSS would otherwise bleed into the islands. 'light' uses no shadow and injects styles
|
|
92
|
+
// globally — right for a React host, where the app's stylesheet IS the point and a shadow root
|
|
93
|
+
// would cut islands off from it, Tailwind included. `motu init` therefore defaults it to 'light'
|
|
94
|
+
// for every host but 'angularjs' (`init.mjs:164`).
|
|
95
|
+
//
|
|
96
|
+
// What breaks: 'shadow' on a Tailwind host renders every island unstyled, in the lagoon and in the
|
|
97
|
+
// page, with NO check failing. That is also why `renderRegistry` throws rather than defaulting on an
|
|
98
|
+
// unrecognised value (`islands.mjs:54-58`) — a silent default is exactly how this key's own `paths`
|
|
99
|
+
// dead-end bug shipped.
|
|
100
|
+
isolation: 'shadow',
|
|
101
|
+
// The stack the islands are embedded INTO. It selects the adapter, and decides whether the
|
|
102
|
+
// "legacy fit" gate applies: fitting an island to a legacy skin is only meaningful when there IS
|
|
103
|
+
// a legacy skin. 'angularjs' = the reference ocean; 'next'/'none' = a modern React host, where an
|
|
104
|
+
// island is mounted directly and there is nothing to fit to.
|
|
105
|
+
host: 'angularjs',
|
|
106
|
+
// Root of the HOST application (relative to the project root) — where a 'next' lagoon resolves the
|
|
107
|
+
// host's own path aliases, Tailwind config and component library from. Defaults to the app root.
|
|
108
|
+
hostRoot: '.',
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* WHERE THE FRAMEWORK IS, derived from the binary that is running.
|
|
113
|
+
*
|
|
114
|
+
* `packages/cli/src/lib` → the checkout root. This is not a guess: the CLI file being executed lives
|
|
115
|
+
* inside the checkout, so the answer is always available, and `lagoon-vite.mjs` has always computed it
|
|
116
|
+
* this way to resolve vite and the build plugins.
|
|
117
|
+
*
|
|
118
|
+
* Which made `motuRoot` in every adopting project's config a bug rather than a configuration: two
|
|
119
|
+
* modules of the same package disagreed about a knowable fact, and the one that asked put a
|
|
120
|
+
* machine-specific path (`"../../motu"`, `"../../../../motu"`) into a committed file — the single line
|
|
121
|
+
* that made a clone fail on someone else's machine.
|
|
122
|
+
*
|
|
123
|
+
* SO THE CONFIG KEY IS GONE. It was kept for a while as an override for "a differently-placed checkout
|
|
124
|
+
* or a CI image", but `$MOTU_ROOT` already answers both and answers them better: the one property the
|
|
125
|
+
* config key had that the environment variable does not is that it gets COMMITTED, which is precisely
|
|
126
|
+
* the bug. A key whose only distinguishing feature is the failure mode is not an escape hatch.
|
|
127
|
+
*
|
|
128
|
+
* Verified rather than assumed: the derived directory has to hold a checkout. When it does not, the
|
|
129
|
+
* CLI was INSTALLED, and this used to fall back to the project root — which was wrong in a way that
|
|
130
|
+
* only showed once the CLI came from npm: every `packages/...` path then pointed INTO the adopter's
|
|
131
|
+
* app. So the installed case is its own mode, decided in lib/framework.mjs, and `motuRoot` there names
|
|
132
|
+
* the directory the installed `@motujs/*` live in — the one thing "motu's own tree" can mean once motu
|
|
133
|
+
* is a dependency (removal-check skips it; the lagoon's fs allow-list serves it).
|
|
134
|
+
*/
|
|
135
|
+
const FRAMEWORK_ROOT = CHECKOUT_ROOT ?? dirname(CLI_PACKAGE_ROOT);
|
|
136
|
+
|
|
137
|
+
/** Hosts that have a legacy skin an island must be proven to survive. */
|
|
138
|
+
const LEGACY_FIT_HOSTS = new Set(['angularjs']);
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* WHICH PROJECT AM I? Not "whatever is above my cwd", when the cwd is not the project's.
|
|
142
|
+
*
|
|
143
|
+
* The runtime harness is spawned with `cwd` set to the CLI package — it has to be, or `--import tsx`
|
|
144
|
+
* does not resolve — and this walked up from there, straight into motu's OWN motu.config.json. So
|
|
145
|
+
* `motu island verify <name> --runtime --fast`, run in any project, loaded MOTU'S demo app and
|
|
146
|
+
* verified that instead, failing on a file the user's project has never heard of. `MOTU_PROJECT_ROOT`
|
|
147
|
+
* lets the spawner say which project it means, exactly as `MOTU_ROOT` says where the framework is.
|
|
148
|
+
*/
|
|
149
|
+
function findConfig(startDir) {
|
|
150
|
+
let dir = startDir;
|
|
151
|
+
for (;;) {
|
|
152
|
+
const jsonPath = resolve(dir, 'motu.config.json');
|
|
153
|
+
if (existsSync(jsonPath)) {
|
|
154
|
+
try {
|
|
155
|
+
return { dir, config: JSON.parse(readFileSync(jsonPath, 'utf8')) };
|
|
156
|
+
} catch (e) {
|
|
157
|
+
throw new Error(`motu: invalid ${jsonPath}: ${e.message}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const pkgPath = resolve(dir, 'package.json');
|
|
161
|
+
if (existsSync(pkgPath)) {
|
|
162
|
+
try {
|
|
163
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
164
|
+
if (pkg && pkg.motu) return { dir, config: pkg.motu };
|
|
165
|
+
} catch {
|
|
166
|
+
/* unreadable package.json — keep walking up */
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const parent = dirname(dir);
|
|
170
|
+
if (parent === dir) return null;
|
|
171
|
+
dir = parent;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
let cached;
|
|
176
|
+
|
|
177
|
+
/** Resolve the motu project config (cached). All returned paths are absolute. */
|
|
178
|
+
/**
|
|
179
|
+
* Is there a motu project here at all?
|
|
180
|
+
*
|
|
181
|
+
* NO CONFIG IS A SUPPORTED STATE — a project laid out like the reference needs none, which is why
|
|
182
|
+
* discovery falls back to DEFAULTS rather than failing. What is NOT supported, and what this catches,
|
|
183
|
+
* is no config AND none of the directories those defaults name: that is not a project, it is a wrong
|
|
184
|
+
* working directory, and building it produces the worst possible outcome — every path key silently
|
|
185
|
+
* wrong, every directory walk empty, and a lagoon published with zero islands in it.
|
|
186
|
+
*
|
|
187
|
+
* It surfaced the first time a HUMAN ran the CLI. An agent meeting the old failure (a rollup
|
|
188
|
+
* `UNRESOLVED_IMPORT` on `../../../../roots/lagoon/lagoon.config.json`) greps for the config, finds it
|
|
189
|
+
* one directory down, and retries from there — so the message was never wrong enough to block its only
|
|
190
|
+
* reader, and stayed unreadable for the reader who could not search.
|
|
191
|
+
*
|
|
192
|
+
* EXIT 2, NOT 1, and the distinction matters more for an agent than for a person: 1 says "something
|
|
193
|
+
* contradicted a declaration -> repair it", and the repair an agent reaches for here is writing a
|
|
194
|
+
* motu.config.json — scaffolding a second project beside the real one. 2 says "this could not run ->
|
|
195
|
+
* retry, do NOT repair", which is exactly right: the same command, one directory along.
|
|
196
|
+
*/
|
|
197
|
+
export function assertProject(paths, { color } = {}) {
|
|
198
|
+
const dim = color?.dim ?? ((x) => x);
|
|
199
|
+
const red = color?.red ?? ((x) => x);
|
|
200
|
+
// A project that declared itself is not this bug. If its paths are wrong, that is a different
|
|
201
|
+
// error and it deserves its own message rather than being swallowed by this one.
|
|
202
|
+
if (paths.configPath) return;
|
|
203
|
+
// The reference layout, config-free, is legitimate — one of these existing means we are home.
|
|
204
|
+
const roots = [paths.lagoonDir, paths.islandsDir, paths.archipelagosDir];
|
|
205
|
+
if (roots.some((d) => d && existsSync(d))) return;
|
|
206
|
+
|
|
207
|
+
// The answer is almost always one level down, and looking is cheap. Printing WHERE turns the whole
|
|
208
|
+
// failure into one line the reader can act on.
|
|
209
|
+
let nearby = [];
|
|
210
|
+
try {
|
|
211
|
+
nearby = readdirSync(paths.root, { withFileTypes: true })
|
|
212
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith('.') && e.name !== 'node_modules')
|
|
213
|
+
.filter((e) => existsSync(resolve(paths.root, e.name, 'motu.config.json')))
|
|
214
|
+
.map((e) => e.name);
|
|
215
|
+
} catch {
|
|
216
|
+
/* unreadable directory: the message below is still the useful half */
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
console.error(red('✗ no motu project here'));
|
|
220
|
+
console.error(dim(` searched upward from ${paths.root} and found no motu.config.json,`));
|
|
221
|
+
console.error(dim(' so every path fell back to the reference layout — and none of it exists:'));
|
|
222
|
+
for (const d of roots) if (d) console.error(dim(` ${d}`));
|
|
223
|
+
if (nearby.length) {
|
|
224
|
+
console.error('');
|
|
225
|
+
console.error(' There is one directly below you. Run the command from there:');
|
|
226
|
+
for (const n of nearby) console.error(` cd ${n}`);
|
|
227
|
+
} else {
|
|
228
|
+
console.error('');
|
|
229
|
+
console.error(dim(' Run it from the directory holding motu.config.json, or `motu init` to create one.'));
|
|
230
|
+
}
|
|
231
|
+
process.exit(2);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function loadMotuConfig() {
|
|
235
|
+
if (cached) return cached;
|
|
236
|
+
const found = findConfig(process.env.MOTU_PROJECT_ROOT ? resolve(process.env.MOTU_PROJECT_ROOT) : process.cwd());
|
|
237
|
+
const root = found ? found.dir : process.cwd();
|
|
238
|
+
const cfg = { ...DEFAULTS, ...(found?.config ?? {}) };
|
|
239
|
+
const appRoot = resolve(root, cfg.app);
|
|
240
|
+
const inApp = (p) => resolve(appRoot, p);
|
|
241
|
+
cached = {
|
|
242
|
+
root,
|
|
243
|
+
appRoot,
|
|
244
|
+
/**
|
|
245
|
+
* The npm package name whose barrel exports ELEMENT_REGISTRY. Defaults to the app directory name.
|
|
246
|
+
*
|
|
247
|
+
* It is a NAME, not a path: it is the specifier generated code imports from. The lagoon aliases
|
|
248
|
+
* it onto `paths.barrel` (`lagoon-vite.mjs:193`), the materialized entries import from it
|
|
249
|
+
* (`lagoon-materialize.mjs:67`), and `archipelago init` writes it into the composition root it
|
|
250
|
+
* scaffolds (`archipelago-init.mjs:187`). So it has to match the `name` in the app's own
|
|
251
|
+
* package.json, and `basename(appRoot)` is right only when the directory and the package agree.
|
|
252
|
+
*
|
|
253
|
+
* WHAT BREAKS, and it has: this is the key whose silent `undefined` generated an import of a
|
|
254
|
+
* package that does not exist. `loadMotuConfig` resolved it and `paths` did not carry it, so
|
|
255
|
+
* `archipelago init` read `paths.appPackage ?? 'motu-islands'` and got the fallback — a file that
|
|
256
|
+
* imports from a package nobody installed, failing at the app's build rather than at motu's. It
|
|
257
|
+
* is on `paths` now; the shape of the bug is the allowlist dead end described
|
|
258
|
+
* at the top of this file (`util.mjs:58-67`).
|
|
259
|
+
*
|
|
260
|
+
* Set it explicitly whenever the directory name is not the package name — a project whose motu
|
|
261
|
+
* lives in `motu/` but publishes its islands as `motu-islands` must say so.
|
|
262
|
+
*/
|
|
263
|
+
appPackage: cfg.appPackage ?? basename(appRoot),
|
|
264
|
+
tagPrefix: cfg.tagPrefix,
|
|
265
|
+
isolation: cfg.isolation === 'light' ? 'light' : 'shadow',
|
|
266
|
+
host: cfg.host ?? 'angularjs',
|
|
267
|
+
/**
|
|
268
|
+
* The host's own top-level source directories, when the guess is wrong.
|
|
269
|
+
*
|
|
270
|
+
* This whitelist is the config surface: a key absent from it is silently dropped, whatever the
|
|
271
|
+
* project wrote. `hostSources` was added to removal-check with a comment telling users to set it,
|
|
272
|
+
* and it did nothing at all until it appeared here — the escape hatch for a bug, itself broken in
|
|
273
|
+
* the same way the bug was.
|
|
274
|
+
*/
|
|
275
|
+
hostSources: Array.isArray(cfg.hostSources) ? cfg.hostSources : undefined,
|
|
276
|
+
/**
|
|
277
|
+
* THE BACKEND'S OWN LIST OF OPERATIONS — a path, relative to the project root.
|
|
278
|
+
*
|
|
279
|
+
* Read by `operation-reach`, which asks whether the thing a region asked for EXISTS. motu cannot
|
|
280
|
+
* know that on its own: an operation is a fact about the backend and motu has no view of one. So
|
|
281
|
+
* the list comes from a file the project already maintains — in practice assay's
|
|
282
|
+
* `.assay/operations.json`, though only its KEYS are read.
|
|
283
|
+
*
|
|
284
|
+
* Deliberately a path and not a dependency. The moment this reads a declaration's contents motu
|
|
285
|
+
* has married one backend tool, and an OpenAPI document or a hand-written array can fill the same
|
|
286
|
+
* slot. See `lib/operations.mjs`.
|
|
287
|
+
*
|
|
288
|
+
* Absent, the check skips and says which key would switch it on — a project that has not pointed
|
|
289
|
+
* at a universe has not failed anything.
|
|
290
|
+
*/
|
|
291
|
+
operations: typeof cfg.operations === 'string' && cfg.operations.trim() ? cfg.operations.trim() : undefined,
|
|
292
|
+
/**
|
|
293
|
+
* HOW HARD `root` IS PUSHED — 'encouraged' (the default) or 'required'.
|
|
294
|
+
*
|
|
295
|
+
* A region composes either from its archipelago's `root`, which is safe by construction, or from
|
|
296
|
+
* a hand-written lagoon frame, which is a second description of the page and is only as safe as
|
|
297
|
+
* the checks comparing it. Both work. The difference is a real refactor of the host's pages, and
|
|
298
|
+
* a tool that fails a project on its first day for not having done it is a tool nobody adopts.
|
|
299
|
+
*
|
|
300
|
+
* So the default SAYS SO and passes; a project that has finished migrating sets 'required' and
|
|
301
|
+
* the frame becomes an error, which is what stops the old shape creeping back in.
|
|
302
|
+
*/
|
|
303
|
+
regionRoot: cfg.regionRoot === 'required' ? 'required' : 'encouraged',
|
|
304
|
+
/**
|
|
305
|
+
* WHICH STATES A REGION ACTUALLY REACHES — the coverage fold, and the one part of motu designed
|
|
306
|
+
* to run in production.
|
|
307
|
+
*
|
|
308
|
+
* DEPLOYMENT FACTS ONLY. Whether a key is a closed set is a fact about the key and is declared on
|
|
309
|
+
* the archipelago beside it (`coverage.enums`), where it travels with the region. What lives here
|
|
310
|
+
* is whether the fold runs at all, where a corpus is posted, and which regions are watched.
|
|
311
|
+
*
|
|
312
|
+
* Baked into the generated island registry by `motu sync`, so no application file mentions
|
|
313
|
+
* coverage and no host's bundler has to define a constant. `enabled` defaults to FALSE: a thing
|
|
314
|
+
* that runs in production is a thing somebody switched on.
|
|
315
|
+
*/
|
|
316
|
+
coverage: {
|
|
317
|
+
enabled: cfg.coverage?.enabled === true,
|
|
318
|
+
// NO ADDRESSES HERE. They would be baked into the generated island registry, which the lagoon
|
|
319
|
+
// imports and publishes as a public page. The application renders them as meta tags; see
|
|
320
|
+
// `metaContent` in @motujs/coverage. `corpusUrl` is different — it is read by the CLI on a
|
|
321
|
+
// developer's machine and never reaches a browser.
|
|
322
|
+
corpusUrl: typeof cfg.coverage?.corpusUrl === 'string' ? cfg.coverage.corpusUrl : null,
|
|
323
|
+
regions: Array.isArray(cfg.coverage?.regions) ? cfg.coverage.regions : null,
|
|
324
|
+
/** Where the current known set is served from, so it is not frozen at build time. */
|
|
325
|
+
},
|
|
326
|
+
/**
|
|
327
|
+
* Does this project CLAIM motu is removable?
|
|
328
|
+
*
|
|
329
|
+
* `removal-check` asks one question — delete motu, does the application still compile? — and it
|
|
330
|
+
* is the right question for a host that ADOPTED motu: the promise is that islands leave no
|
|
331
|
+
* runtime trace. It is not a question motu's own tools can answer. The baseline review console
|
|
332
|
+
* composes regions with `createRegion` and paints from `@motujs/chrome/react`; motu is meant to be
|
|
333
|
+
* load-bearing there, and it will never compile without it.
|
|
334
|
+
*
|
|
335
|
+
* Reported as a SKIP rather than a pass, because a project opting out has not proved anything —
|
|
336
|
+
* and reported at all, because an opt-out nobody can see is how a check quietly stops running.
|
|
337
|
+
* Default true: a host must say so deliberately, and the answer for an adopting app stays FAIL.
|
|
338
|
+
*/
|
|
339
|
+
removable: cfg.removable !== false,
|
|
340
|
+
/**
|
|
341
|
+
* WHO THIS PROJECT IS ON A LAGOON HOST, when the git repository is the wrong answer.
|
|
342
|
+
*
|
|
343
|
+
* Publishing normally identifies a project by its git remote, which is right until a repository
|
|
344
|
+
* holds more than one publishable app: motu's own demo-app and the baseline review console both
|
|
345
|
+
* live in `Scorbutics/motu` and would land on the same `repo:slug`, each overwriting the other —
|
|
346
|
+
* and a gallery that takes one lagoon per repo would show whichever published last.
|
|
347
|
+
*
|
|
348
|
+
* Either half can be set; whatever is absent keeps its derived value.
|
|
349
|
+
*/
|
|
350
|
+
publishAs: {
|
|
351
|
+
repo: typeof cfg.publishAs?.repo === 'string' ? cfg.publishAs.repo : null,
|
|
352
|
+
slug: typeof cfg.publishAs?.slug === 'string' ? cfg.publishAs.slug : null,
|
|
353
|
+
},
|
|
354
|
+
/**
|
|
355
|
+
* Whether `legacy` fit is a required strategy AND a second verified runtime mount for this host.
|
|
356
|
+
*
|
|
357
|
+
* Derived from `host` — true only for the ocean — and overridable for a host with a legacy skin
|
|
358
|
+
* motu does not know about. Three things read it: `island create` scaffolds `legacy: 'fill'` into
|
|
359
|
+
* a new island (`create.mjs:50`), `island verify` requires the strategy to be declared
|
|
360
|
+
* (`verify.mjs:393`), and the runtime lane drives BOTH fits instead of one
|
|
361
|
+
* (`verify.mjs:1236`, `:2433`). It also reaches the browser as `__MOTU_LEGACY_FIT__`
|
|
362
|
+
* (`lagoon-vite.mjs:285`), so the lagoon can offer the fit chip.
|
|
363
|
+
*
|
|
364
|
+
* WHAT BREAKS, in both directions. Turned ON for a modern host, every island is asked to declare a
|
|
365
|
+
* fit to a skin that does not exist and the runtime lane pays a second browser mount per scenario
|
|
366
|
+
* for nothing — roughly double the cost of the most expensive tier. Turned OFF for an ocean, the
|
|
367
|
+
* `legacy` mount is never driven, so an island that renders correctly standalone and collapses
|
|
368
|
+
* inside the legacy page's CSS passes green: the check that exists precisely to catch that is the
|
|
369
|
+
* one you switched off.
|
|
370
|
+
*/
|
|
371
|
+
legacyFit: cfg.legacyFit ?? LEGACY_FIT_HOSTS.has(cfg.host ?? 'angularjs'),
|
|
372
|
+
hostRoot: resolve(root, cfg.hostRoot ?? cfg.app ?? '.'),
|
|
373
|
+
/**
|
|
374
|
+
* Where the motu framework checkout lives. In checkout mode a project resolves @motujs/* BY PATH
|
|
375
|
+
* into it rather than through node_modules — which is what lets a project adopt motu with no
|
|
376
|
+
* install step. DERIVED from the running CLI (see FRAMEWORK_ROOT); `$MOTU_ROOT` overrides it for a
|
|
377
|
+
* differently-placed checkout or a CI image. Installed, it is the installed packages' directory.
|
|
378
|
+
*
|
|
379
|
+
* There is no config key. See the FRAMEWORK_ROOT comment for why the environment is the only
|
|
380
|
+
* override: this value must not be committed.
|
|
381
|
+
*/
|
|
382
|
+
motuRoot: FRAMEWORK_ROOT,
|
|
383
|
+
/**
|
|
384
|
+
* 'checkout' or 'installed' — whether `@motujs/*` are reached by path into a checkout (aliases,
|
|
385
|
+
* no-install links) or resolved normally from the app's node_modules. See lib/framework.mjs.
|
|
386
|
+
*/
|
|
387
|
+
frameworkMode: FRAMEWORK_MODE,
|
|
388
|
+
/** Generated, never-committed build inputs (see `.gitignore`: `.motu/`). */
|
|
389
|
+
cacheDir: resolve(root, '.motu/cache'),
|
|
390
|
+
/**
|
|
391
|
+
* The parsed config verbatim, defaults merged in — an escape hatch for keys the CLI does not
|
|
392
|
+
* model.
|
|
393
|
+
*
|
|
394
|
+
* NOTHING IN packages/cli READS IT, and that is the honest state rather than an oversight to fix.
|
|
395
|
+
* It was added as the documented way around the allowlist this file opens with, and every time a
|
|
396
|
+
* key has actually been needed the right answer was to add it to the allowlist properly — where
|
|
397
|
+
* it gets a type, a default, a comment, and a place on `paths`. A command reading `raw` would be
|
|
398
|
+
* re-creating the untyped, undefaulted, uncommented surface the allowlist exists to prevent.
|
|
399
|
+
*
|
|
400
|
+
* Kept because it costs one line and it is the correct seam for a genuinely project-specific key
|
|
401
|
+
* — one motu should not model. If you reach for it, ask first whether the key deserves modelling.
|
|
402
|
+
*/
|
|
403
|
+
raw: cfg,
|
|
404
|
+
// The layout, resolved to absolute paths once. What each one drives, and what breaks when it is
|
|
405
|
+
// wrong, is documented on the corresponding key in DEFAULTS at the top of this file.
|
|
406
|
+
islandsDir: inApp(cfg.islands),
|
|
407
|
+
uiDir: inApp(cfg.ui),
|
|
408
|
+
archipelagosDir: inApp(cfg.archipelagos),
|
|
409
|
+
sharedDir: inApp(cfg.shared),
|
|
410
|
+
/**
|
|
411
|
+
* The app barrel — DERIVED from where the islands actually live, unless stated.
|
|
412
|
+
*
|
|
413
|
+
* `barrel` defaulted to a literal `src/index.ts` no matter where the project's motu directories
|
|
414
|
+
* were, so a layout that is not `src/`-shaped pointed it at a file that does not exist. Measured
|
|
415
|
+
* on a Rails application whose JS root is `app/javascript`: the adopter moved motu's directories
|
|
416
|
+
* to match, and `archipelago create` (now `archipelago init`) died with a raw ts-morph `File not found: …/src/index.ts` —
|
|
417
|
+
* an exception from a library the adopter has no reason to have heard of, naming a path they had
|
|
418
|
+
* deliberately not used. `barrel` was configurable all along and mentioned in no output.
|
|
419
|
+
*
|
|
420
|
+
* The islands directory already says where motu lives, so its parent is the answer. An explicit
|
|
421
|
+
* `barrel` still wins.
|
|
422
|
+
*/
|
|
423
|
+
barrel: found?.config?.barrel ? inApp(cfg.barrel) : resolve(dirname(inApp(cfg.islands)), 'index.ts'),
|
|
424
|
+
contractSrcDir: inApp(cfg.contract),
|
|
425
|
+
lagoonDir: inApp(cfg.lagoon),
|
|
426
|
+
/** Declared for the layout's sake; no CLI command reads it. See `bridge` in DEFAULTS. */
|
|
427
|
+
bridgeDir: inApp(cfg.bridge),
|
|
428
|
+
/** PROJECT root, not app root — the backend build output sits outside the frontend app. */
|
|
429
|
+
manifest: resolve(root, cfg.manifest),
|
|
430
|
+
configPath: found ? resolve(found.dir, 'motu.config.json') : null,
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
// A REMOVED KEY MUST NOT BE A SILENT ONE.
|
|
434
|
+
//
|
|
435
|
+
// Dropping `motuRoot` quietly would be the exact failure this file's own allowlist comment is about:
|
|
436
|
+
// a project keeps a line that used to mean something, nothing reads it, and the CLI resolves
|
|
437
|
+
// somewhere else without saying so. `motuRoot` in particular used to point @motujs/* at a checkout, so
|
|
438
|
+
// a project whose line was doing real work needs to hear that the environment now carries it.
|
|
439
|
+
if (cfg.motuRoot !== undefined && !process.env.MOTU_MUTE_DEPRECATED) {
|
|
440
|
+
const where = cached.configPath ?? 'motu.config.json';
|
|
441
|
+
process.emitWarning(
|
|
442
|
+
`motu: \`motuRoot\` was removed and is being ignored (${where}).\n` +
|
|
443
|
+
` The framework checkout is derived from the running CLI: ${cached.motuRoot}\n` +
|
|
444
|
+
` Delete the line. If that path is wrong, export MOTU_ROOT=<checkout> instead — it is the\n` +
|
|
445
|
+
` same override without committing a machine-specific path.`,
|
|
446
|
+
'MotuDeprecationWarning',
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return cached;
|
|
451
|
+
}
|