@motujs/cli 0.0.0-stage → 0.1.0
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,304 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: motu-regions
|
|
3
|
+
description: The motu rules for DECLARING a region, in full: island vs host, the page survey, declaring only decisions, region state and ownership (writes, bind, reads, one producer per key), useRegion/seed, foreign stores (StoreAdapter), catalogue regions, an island that IS the app's component and its providers, `planned: true`, several agents on one region, staged adoption (root, slots, exclusive), sources and ports and the adapter, unit tests for sources, removal-check, and what motu does not check. Load before creating or changing an archipelago, an island's place in a region, a source, or a page that composes a region.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# motu-regions — Declaring a region
|
|
7
|
+
|
|
8
|
+
The rules for declaring a region — the detail behind the short `motu:rules` block in the project's instructions. Read the part you need; each section stands alone.
|
|
9
|
+
|
|
10
|
+
## The rules
|
|
11
|
+
|
|
12
|
+
motu is an ARCHITECTURE framework: you build a page's regions one at a time, and what keeps them
|
|
13
|
+
coherent is DECLARED — who owns each piece of region state, which island fills which slot, what the
|
|
14
|
+
region asks of a backend. The lagoon (every declared state at its own URL) is what those declarations
|
|
15
|
+
make possible; it is not a component gallery and it is not the product. When a rule below asks you to
|
|
16
|
+
declare something, the declaration IS the work — the preview is how you check it.
|
|
17
|
+
|
|
18
|
+
- What the region SHOWS or ACTS ON is an island; what merely ARRANGES it is not. A pure layout and an
|
|
19
|
+
overlay anchored to a DOM id anywhere on the page stay host components. A control that owns region
|
|
20
|
+
state is an island EVEN IF other islands sit inside it — DOM nesting says nothing about ownership,
|
|
21
|
+
and `slots` (prop -> slot, in the archipelago) declares which island fills which of its props, so
|
|
22
|
+
the region composes the same way in the page and in the lagoon.
|
|
23
|
+
|
|
24
|
+
- Before a PAGE gets islands, SURVEY it (`/island-locate`) and write the answer down: which regions
|
|
25
|
+
become islands, which stay host, who owns each shared key, and whether the page becomes an
|
|
26
|
+
archipelago. The scope of a region is the PAGE, never a DOM subtree — an archipelago drawn around
|
|
27
|
+
the div that happens to contain the islands cuts through the coupling it exists to declare.
|
|
28
|
+
|
|
29
|
+
- Declare only what you DECIDED. An island file is `island('x-tag', Component)` — its props, its
|
|
30
|
+
callback-events and the host modules it reaches are read from the component into
|
|
31
|
+
`islands/contracts.generated.ts` by `motu sync`, and a stale one is a build error. What stays
|
|
32
|
+
hand-written is the decision: an event whose region name differs from the callback's
|
|
33
|
+
(`{ events: { onProgress: 'week-progress' } }`), and in the archipelago the `writes`, the `slots`
|
|
34
|
+
and any renamed `bind`.
|
|
35
|
+
|
|
36
|
+
- Region state is DECLARED, not emergent. A key an island updates is declared in that island's
|
|
37
|
+
`writes` (event → key); every other bound key is host-fed, DERIVED — do not list it. `bind` is the
|
|
38
|
+
region's keys (`bind: ['compactMode', { stats: 'networkStats' }]`), the map form only for renames.
|
|
39
|
+
The page cannot assign a produced key — the host-side region type omits them, so it is a compile
|
|
40
|
+
error. If you find yourself computing a value in the page from what one island did and passing it to
|
|
41
|
+
another, that is the laundering the ownership rules exist to stop: declare the output instead.
|
|
42
|
+
|
|
43
|
+
- The page reads region state with the binding's `useRegion()` and establishes it with `seed(...)`,
|
|
44
|
+
both typed from the archipelago (`createRegion`). Never reach for a store directly.
|
|
45
|
+
|
|
46
|
+
- A host that ALREADY has a state architecture (Jotai, Zustand, a Redux slice) does not have to move
|
|
47
|
+
its state into motu's store, and should not be asked to. Declare the region over the keys it
|
|
48
|
+
already has, then install a `StoreAdapter` — `get(key)` and `subscribe(keys, onChange)`, about ten
|
|
49
|
+
lines — and `observeForeignStore(archipelago, adapter)`. motu then AUDITS what it cannot enforce:
|
|
50
|
+
a key an island declares it owns, moving with no declared output to account for it, shows in the
|
|
51
|
+
lens as "wrote outside the declaration". It reports the key and the declared owner, never the
|
|
52
|
+
culprit — naming that would mean being in the write path, which is the rewrite this avoids. Know
|
|
53
|
+
the boundary: motu ENFORCES its own store and only AUDITS someone else's, and no CLI check sees a
|
|
54
|
+
foreign store at all, because every runtime check drives the lagoon.
|
|
55
|
+
|
|
56
|
+
- `docs/05-archipelagos-and-regions.md` in the motu repo is the reference for how a region declares
|
|
57
|
+
ownership, and `docs/07-checks-and-verification.md` for the checks that enforce it; read them
|
|
58
|
+
before changing how a region declares anything.
|
|
59
|
+
|
|
60
|
+
## A UI that lives in the database, not the repository
|
|
61
|
+
|
|
62
|
+
A metadata-driven app (Twenty: widgets and views are rows, dispatched on a codegen'd enum) has no
|
|
63
|
+
`islands: [...]` in source. Declare the region `membership: 'catalogue'` and give every data-summoned
|
|
64
|
+
island a `member:` — the app's own discriminator, not the motu slot. Islands WITHOUT `member` are
|
|
65
|
+
chrome and are still checked for placement; a catalogue region is mixed, and treating it as pure
|
|
66
|
+
reports the chrome as a hallucinated widget type.
|
|
67
|
+
|
|
68
|
+
Do NOT hand-write fixtures to make the lagoon render. The app already had to solve this: its tests
|
|
69
|
+
and stories need the metadata too, so it carries a capture in-repo plus a script that refreshes it
|
|
70
|
+
from a live instance (Twenty: `scripts/mock-data/generate-*.ts` → 2.3 MB under
|
|
71
|
+
`testing/mock-data/generated/`). Point `capture` in `<id>.evidence.ts` at THAT, with `universe` read
|
|
72
|
+
from the schema enum. Both sides are then the app's artifacts and motu only compares.
|
|
73
|
+
|
|
74
|
+
What the comparison buys, and why data-driven UI is an advantage rather than an obstacle: a declared
|
|
75
|
+
member absent from the schema enum is UNREACHABLE (it can never render — a typo or a hallucination),
|
|
76
|
+
a captured type nobody declares is UNCOVERED (the region renders less than the page does). Neither is
|
|
77
|
+
answerable when membership lives in JSX. Watch for the empty answer: Twenty's own transport serves
|
|
78
|
+
`FindAllRecordPageLayouts` with `[]`, which renders as an empty page and looks like a working one —
|
|
79
|
+
`unservedOperations()` catches the missing handler, coverage 0% catches the empty one.
|
|
80
|
+
|
|
81
|
+
## An island can BE the application's component
|
|
82
|
+
|
|
83
|
+
If the host already owns the component, the island declares it directly — `component: FieldsWidget` —
|
|
84
|
+
and no wrapper is written. A wrapper that only installs providers or draws chrome is motu-only code
|
|
85
|
+
in the app's repository, which is what adopting motu is supposed to avoid.
|
|
86
|
+
|
|
87
|
+
Where the pieces go: what an island CANNOT RENDER WITHOUT is `providers` in the lagoon overrides
|
|
88
|
+
(installed in EVERY view, per island, like a channel); the ARRANGEMENT is `layout` (region view
|
|
89
|
+
only); the widget row or props are `seed` DATA. Getting this wrong is invisible in the region view
|
|
90
|
+
and fatal in the mountpoints view — which is the one the flow checks drive, so it reads as "the
|
|
91
|
+
region rendered nothing" rather than "no providers".
|
|
92
|
+
|
|
93
|
+
Providers must be IDEMPOTENT. The frame installs them for its own chrome and each island installs
|
|
94
|
+
them for itself, so they nest — fine for context providers, fatal for a `<Router>` (React Router
|
|
95
|
+
throws, and both cards rendered as "Invalid Configuration"). Gate the ones that cannot nest
|
|
96
|
+
(`useInRouterContext()`).
|
|
97
|
+
|
|
98
|
+
Evidence must not import the frame. `<id>.evidence.ts` is read by plain node where the app's `@/…`
|
|
99
|
+
alias does not resolve, so keep the rows in a module with no application imports and let both sides
|
|
100
|
+
import THAT. The failure is a silent "flows could not be read", not an error at the import.
|
|
101
|
+
|
|
102
|
+
## An island that reads the host's store has to say so
|
|
103
|
+
|
|
104
|
+
`bind` is the prop path, and it is the only reader motu sees by itself. An app with its own state has
|
|
105
|
+
readers with no prop at all — Twenty's side panel subscribes to a Jotai atom and renders the settings
|
|
106
|
+
for whichever widget it names. Declare those with `reads: ['key']` on the island, or the key is
|
|
107
|
+
written by an island, read by nobody motu knows about, and `coupling` reports a real coupling as one
|
|
108
|
+
that escapes the archipelago. It is a CLAIM, not a wire: motu cannot enforce a store it does not own,
|
|
109
|
+
but the claim can be contradicted by the lens and by a flow.
|
|
110
|
+
|
|
111
|
+
**One producer per key, and a catalogue can break that.** Any widget card in Twenty writes
|
|
112
|
+
`pageLayoutEditingWidgetId`, so declaring `writes` on two members made the ownership guard fire — and
|
|
113
|
+
it was right: "either of these writes it" is not a producer. The writer is the CARD, chrome shared by
|
|
114
|
+
every member. When that happens the honest model is motu's own rule — a control that owns region
|
|
115
|
+
state is an island even when others sit inside it — so the card becomes an island containing the
|
|
116
|
+
widget, via nested `slots`. Until then the key is host-written, and say so.
|
|
117
|
+
|
|
118
|
+
## One producer per key, checked in the fast loop
|
|
119
|
+
|
|
120
|
+
Two islands declaring `writes` for the same key is an error, statically — not a runtime store-guard
|
|
121
|
+
complaint you reach after the work is done. Grouped by ELEMENT, not by slot: one island placed in two
|
|
122
|
+
slots (acme's filter panel, desktop + mobile drawer) is one producer and must stay legal.
|
|
123
|
+
|
|
124
|
+
This is what makes parallel agents safe without any new mechanism. Declare the WHOLE region in the
|
|
125
|
+
survey — every slot, every owner — before any island is implemented. Each agent then branches from an
|
|
126
|
+
archipelago that already contains the other agents' ownership, so a second claim on a key fails in
|
|
127
|
+
that agent's own branch, on their first `motu check`. The archipelago IS the claim registry; a
|
|
128
|
+
separate ledger would only restate it.
|
|
129
|
+
|
|
130
|
+
## Surveyed but not built: `planned: true`
|
|
131
|
+
|
|
132
|
+
Declaring a whole region up front is what makes parallel work safe — an agent branches from an
|
|
133
|
+
archipelago that already carries everyone else's ownership. Measured on a two-agent run, the cost was
|
|
134
|
+
a region RED in every branch until the last island landed, and a permanently red region teaches
|
|
135
|
+
everyone to ignore it.
|
|
136
|
+
|
|
137
|
+
`planned: true` on an island entry splits the two questions. Ownership still counts it — a second
|
|
138
|
+
claim on its keys fails exactly as if it were built — while the checks that ask "does it exist, does
|
|
139
|
+
it mount, is it placed" skip it, and a `planned` warning keeps it visible.
|
|
140
|
+
|
|
141
|
+
The flag REMOVES ITSELF: once the island is registered, `planned: true` becomes an error. A survey
|
|
142
|
+
that quietly turns into a list of things nobody built is worse than no survey, so the state cannot
|
|
143
|
+
persist past the moment it stops being true.
|
|
144
|
+
|
|
145
|
+
## When several agents build one region at once
|
|
146
|
+
|
|
147
|
+
These bind you whether or not someone launched you as part of a fan-out.
|
|
148
|
+
|
|
149
|
+
- **You own ONE slot.** Do not edit another island's files, another island's entry in the archipelago,
|
|
150
|
+
or the region's shared type. If you believe the shared type is wrong, SAY SO in your report and stop
|
|
151
|
+
— changing it is a decision about everyone's work, made by someone who can see all of it.
|
|
152
|
+
- **Remove `planned: true` from your own entry only.** It is how the region learns your island exists.
|
|
153
|
+
- **Ship a coverage step for your slot** in the region's `<id>.evidence.ts`: `expectRender` naming text
|
|
154
|
+
your island alone produces. Without it, a slot wired to a neighbour's data passes every check.
|
|
155
|
+
- **Do not repair shared infrastructure.** A failing install, a missing browser, a path that resolves
|
|
156
|
+
outside the project — that is exit 2, "could not run". Report it. An agent that symlinks its way past
|
|
157
|
+
a broken environment inherits a problem it then has to explain, and does it in everyone's tree. This
|
|
158
|
+
happened: one agent stopped and reported, another improvised and produced a false runtime failure.
|
|
159
|
+
- **Say what got in your way.** Framework friction is the most valuable thing you can return, and it
|
|
160
|
+
is invisible to whoever reads only your diff.
|
|
161
|
+
|
|
162
|
+
The files several agents WILL collide in: `islands/registry.ts` and `islands/contracts.generated.ts`
|
|
163
|
+
(generated — regenerate with `motu sync`, never hand-merge), the shared stylesheet (append-only,
|
|
164
|
+
keep both sides), and the archipelago (one entry each, so a line-level merge is usually right). In a
|
|
165
|
+
lagoon frame, prefer an append-only LOOKUP over a ternary chain: two people extending one chain cannot
|
|
166
|
+
both win, and the loser's island renders someone else's data.
|
|
167
|
+
|
|
168
|
+
## "motu is load-bearing" names a file, and the cause is above the line it points at
|
|
169
|
+
|
|
170
|
+
`removal-check` deletes every file that is 100% motu, unwraps its tags everywhere else, and asks
|
|
171
|
+
whether the host still typechecks. When it fails, read it in this order — the report now prints all
|
|
172
|
+
three, so nothing here needs digging:
|
|
173
|
+
|
|
174
|
+
- **"could not rewrite these files"** — the surgery threw and that file was left ALONE, so it proved
|
|
175
|
+
nothing and every error naming it is a consequence, not a finding. It means the rewriter met a JSX
|
|
176
|
+
shape it cannot express. Report the shape: that is motu's bug before it is yours.
|
|
177
|
+
- **"compose a region but are NOT deletable"** — the one that costs an afternoon if it is not said.
|
|
178
|
+
A composition root is deleted WHOLE only when EVERY import it makes is motu's; one application
|
|
179
|
+
import and it is stripped instead, leaving `createRegion(...)` with its imports gone and errors on
|
|
180
|
+
lines that look fine. The report names the offending imports. Whatever needs the application — a
|
|
181
|
+
source's PORT, a service, a browser API — belongs in an app file that renders the real components
|
|
182
|
+
inside `<R.Island slot="…">`, not in the composition root.
|
|
183
|
+
- **a dangling import of a deleted module** — never the line it points at. It is the first case,
|
|
184
|
+
one file earlier.
|
|
185
|
+
|
|
186
|
+
The shape that satisfies all of this is the one the annuaire page already uses: the region binding and
|
|
187
|
+
its wrapper in `components/motu/`, importing nothing but motu; the port, the source and the real
|
|
188
|
+
components in app files that wrap them in `<R.Island>`. The lagoon installs the same source through
|
|
189
|
+
`channelFrom`, because there is no page there to do it.
|
|
190
|
+
|
|
191
|
+
## A source is REAL in the lagoon; its PORT is the stand-in — and the adapter is yours to prove
|
|
192
|
+
|
|
193
|
+
Worth being exact about, because it decides how much a green region actually claims.
|
|
194
|
+
|
|
195
|
+
When a flow runs, the source is the APPLICATION'S OWN OBJECT. Its timeout, its precedence rules, its
|
|
196
|
+
generation guard, its error mapping, its intent dispatch — all of that executes. What is swapped is
|
|
197
|
+
the PORT: production hands it Supabase and the services, the lagoon hands it fixtures. So a region
|
|
198
|
+
flow proves more than "the wiring type-checks"; it proves the page's own logic behaves, against
|
|
199
|
+
answers a human chose.
|
|
200
|
+
|
|
201
|
+
What it CANNOT prove is the seam on the other side of the port: that the real adapter
|
|
202
|
+
(`currentUser()` returning `{ email, user_metadata }`, `fetchCurrentMember()` returning
|
|
203
|
+
`primarySectorId`) matches what the backend actually sends. Get that shape wrong and every motu check
|
|
204
|
+
stays green while production breaks. The lagoon replaces the host module so completely that nothing
|
|
205
|
+
shows a fetch happened — that is the point of it, and it is the boundary.
|
|
206
|
+
|
|
207
|
+
## WHAT MOTU DOES NOT CHECK, and is your job
|
|
208
|
+
|
|
209
|
+
motu checks COMPOSITION: that a region's islands are placed, that a declared coupling carries, that
|
|
210
|
+
every declared state renders. It runs no typecheck and no test runner, and it never will — a green
|
|
211
|
+
`motu check` says nothing whatever about whether your components are correct inside.
|
|
212
|
+
|
|
213
|
+
Two classes are yours, and no amount of motu makes them motu's:
|
|
214
|
+
|
|
215
|
+
- **Logic inside a component.** A filter that stops matching on one field, `some` where `every` was
|
|
216
|
+
meant, an off-by-one on a count. These render fine, compose fine, and pass every check here.
|
|
217
|
+
- **The path from a user's action to a declared output.** A flow EMITS an island's declared event —
|
|
218
|
+
`emit: { slot: 'search', event: 'term', detail: 'staging' }` — which enters the region *past* the
|
|
219
|
+
component's own handler. A handler that ignores its argument, so typing changes nothing, passes
|
|
220
|
+
every flow you can write. This is not a gap to be closed with more flows; it is where the model
|
|
221
|
+
ends and a unit test begins.
|
|
222
|
+
|
|
223
|
+
Measured, on a real screen: eight regressions injected into a motu region and into the same feature
|
|
224
|
+
built with plain props. The project's own unit tests caught five; motu's flows caught one — and that
|
|
225
|
+
one was a RENDER assertion, the class the tests missed. They are different instruments. Write both,
|
|
226
|
+
and do not read a green `motu check` as coverage.
|
|
227
|
+
|
|
228
|
+
So a source that carries logic gets UNIT TESTS, in the host's own runner, over a hand-made port. Not
|
|
229
|
+
because motu asks for a second suite, but because that is the only place the branches no rendered
|
|
230
|
+
state distinguishes can be reached: a lookup that THREW versus one that answered null, a deadline, the
|
|
231
|
+
precedence in a prefill, the slower of two submits not overwriting the faster. `lib/tests/unit/
|
|
232
|
+
actions-week-source.test.ts` is the shape. A flow drives a source through the screen; a unit test
|
|
233
|
+
drives it directly, and you want both for the same reason you want `expectRender` as well as `expect`.
|
|
234
|
+
|
|
235
|
+
THERE IS NO CHECK FOR THIS, deliberately. A `sources-tested` warning used to fire when no test file
|
|
236
|
+
imported a declared source, and it was removed: whether the application's own code has unit tests is
|
|
237
|
+
not motu's judgement to make. Nothing else in the framework opines on a host's test suite, and it
|
|
238
|
+
could only fire once a source already existed — so it encouraged TESTING an extraction, never the
|
|
239
|
+
extraction itself. What produces the pattern is structural: the lagoon renders a region WITHOUT the
|
|
240
|
+
page, so orchestration left in page effects cannot be previewed at all.
|
|
241
|
+
|
|
242
|
+
And the adapter itself — the few lines that turn the backend's shape into the port's — belongs to
|
|
243
|
+
whatever proves this app against a real backend. Where the call goes through one of the app's own
|
|
244
|
+
backend operations, that is ASSAY's ledger: it records the shape of every outbound payload (never the
|
|
245
|
+
values) and fails when it differs from `.assay/operations.json`. That is precisely the other half of
|
|
246
|
+
the port's assumption — the port says what the source expects, the ledger says what the operation
|
|
247
|
+
sends.
|
|
248
|
+
|
|
249
|
+
Know the gap that leaves, and do not repeat the mistake this paragraph used to make. A client that
|
|
250
|
+
talks to the database or the auth service DIRECTLY — `supabase.auth.getUser()`, a PostgREST read or
|
|
251
|
+
an `rpc` in a repository — is not an operation, so it has no declaration and neither tool pins its
|
|
252
|
+
shape. This text called that "a few lines by construction, which is the mitigation". It was wrong:
|
|
253
|
+
counted on the project it was written for, the direct-from-browser path was ~40 modules naming 89
|
|
254
|
+
Postgres functions — the app's DOMINANT data path, not an adapter. Nobody had counted it, and the
|
|
255
|
+
guess became a rule.
|
|
256
|
+
|
|
257
|
+
So: measure the gap in YOUR host before deciding it is small, and never read a green assay run as
|
|
258
|
+
covering it. What guards that path is RLS alone, which makes the review surface the POLICY rather
|
|
259
|
+
than a declaration. Routing the read through a backend operation is the fix where the read deserves
|
|
260
|
+
one; where it does not, say plainly that the path is unpinned rather than describing it as narrow.
|
|
261
|
+
|
|
262
|
+
## Adoption is staged, and stage 1 does not touch the page
|
|
263
|
+
|
|
264
|
+
A region's arrangement lives in the archipelago's `root` — the application's own layout component,
|
|
265
|
+
with `slots` mapping its props to the region's islands, rendered by the page AND by the lagoon from
|
|
266
|
+
the same map — or in a hand-written lagoon frame, which is a second description of the page. Both are
|
|
267
|
+
supported, and which one you are in is a STAGE, not a verdict.
|
|
268
|
+
|
|
269
|
+
Adopt as a THIN OVERLAY first: islands, `<X.Island>` inside the page's existing JSX, a frame that
|
|
270
|
+
holds only the application's own components. `region-root` reports that shape as `ok` and names `root`
|
|
271
|
+
without failing on it, because moving a page is a real refactor of the host's own code and no project
|
|
272
|
+
can do ten of them before its first green run. Then migrate ONE region at a time, when you already
|
|
273
|
+
have a reason to open its page. Then, when the last one is done, set `"regionRoot": "required"` in
|
|
274
|
+
`motu.config.json` — the switch that makes the arc finish rather than stall half-done.
|
|
275
|
+
|
|
276
|
+
A MIGRATION SWEEP ACROSS EVERY REGION IS THE WRONG SHAPE, and the reason is not diff size. Two things
|
|
277
|
+
change in a move to `root` that no check sees:
|
|
278
|
+
|
|
279
|
+
- EXCLUSIVITY GETS DEMOTED. A ternary whose branches cannot both render becomes two independent
|
|
280
|
+
props, and nothing enforces that at most one is non-null — acme's actions page traded
|
|
281
|
+
`weeksLoaded ? <WeekNavigator/> : <Skeleton/>` for a `weekNav` prop and a `weekNavPlaceholder` prop
|
|
282
|
+
rendered adjacently, with a comment standing where the guarantee used to be. DECLARE the pair —
|
|
283
|
+
`exclusive: [['weekNav', 'weekNavPlaceholder']]` on the archipelago — and `<X.Root>` will not
|
|
284
|
+
compile with both: the page writes one condition, `{...(ready ? { weekNav } : { weekNavPlaceholder })}`.
|
|
285
|
+
Undeclared, nothing sees it.
|
|
286
|
+
|
|
287
|
+
- THE SERVER/CLIENT BOUNDARY MOVES. The root renders inside `<X.Region>`, a client component. A
|
|
288
|
+
layout the page rendered on the server crosses into the client bundle when it becomes the root, and
|
|
289
|
+
the nesting inverts: acme's `/forgot-password` went from `page → AuthLayout → Screen` to
|
|
290
|
+
`page → Screen → Region → AuthLayout`. Same pixels, different tree, different bundle. Read what the
|
|
291
|
+
extracted layout imports before moving it.
|
|
292
|
+
|
|
293
|
+
What the migration BUYS is one thing, and it is worth being exact about it: the lagoon previews the
|
|
294
|
+
page instead of a drawing of it. Islands, contracts, ownership, coverage and `removal-check` work the
|
|
295
|
+
same in either shape — `removal-check` rewrites `<X.Root>` back to the layout component and unwraps
|
|
296
|
+
`<X.Island>`, so both shapes survive deleting motu. What it COSTS is that the extraction is not
|
|
297
|
+
reversible by the tool: removal leaves the extracted layout files behind, where stage 1 returns the
|
|
298
|
+
page to exactly the JSX it had. So the trade is lagoon fidelity against a page you can still hand back
|
|
299
|
+
unchanged, and a region earns the move when someone is already editing it.
|
|
300
|
+
|
|
301
|
+
A frame that genuinely must draw its own markup says why once — `inventedArrangement('why', <…/>)`
|
|
302
|
+
downgrades the error to a warning, and warnings do not fail `motu check`. It is a HOLD, not an answer.
|
|
303
|
+
`motu archipelago adopt-root <id>` does the derivable half of a migration and refuses rather than
|
|
304
|
+
guessing when the frame nests two host components.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
motu is an ARCHITECTURE framework: a page's regions are built one at a time and kept coherent by what is
|
|
2
|
+
DECLARED — who owns each key, which island fills which slot, what the region asks of a backend. The lagoon
|
|
3
|
+
renders every declared state at its own URL. These are the invariants; the detail is in the skills —
|
|
4
|
+
LOAD THE ONE THAT MATCHES YOUR TASK before you start it:
|
|
5
|
+
|
|
6
|
+
- `motu-regions` — declaring a region: islands vs host, ownership, sources/ports, adoption, parallel agents
|
|
7
|
+
- `motu-evidence` — scenarios, flows, worlds, the lagoon override, `motu lagoon try`, the final look
|
|
8
|
+
- `motu-checks` — which check when, exit codes, `integrate check`, the handover
|
|
9
|
+
- workflows: `island-locate` (survey a page) · `island-create` · `island-extract` · `archipelago-build`
|
|
10
|
+
|
|
11
|
+
Declaring
|
|
12
|
+
- What a region SHOWS or ACTS ON is an island; what merely ARRANGES it stays host. Survey the PAGE
|
|
13
|
+
(`island-locate`) and write down owners before any island is built.
|
|
14
|
+
- Declare only what you DECIDED. Every key an island updates is in its `writes`; everything else bound is
|
|
15
|
+
host-fed. One producer per key. Never compute in the page what one island did and pass it to another —
|
|
16
|
+
declare the output. The page uses the binding's `useRegion()` / `seed(...)`, never a store.
|
|
17
|
+
|
|
18
|
+
Evidence
|
|
19
|
+
- New UI starts as an island with a scenario in `<name>.evidence.ts`, renders from defaults alone, and is
|
|
20
|
+
looked at in the lagoon before it is placed. Changed UI starts with the scenario showing the state you
|
|
21
|
+
will change.
|
|
22
|
+
- Evidence data comes from the app (fixtures it already has, typed with `import type`, imported by
|
|
23
|
+
RELATIVE path) — never invented to make a frame render.
|
|
24
|
+
- Every declared state is an address (`motu lagoon states`); read `window.__motuLagoonState` before
|
|
25
|
+
believing a screenshot. EXPLORE before you assert: `motu lagoon try <region> [step…]` (~5s) shows what
|
|
26
|
+
each step moves; assertions are yours, never copied from its output.
|
|
27
|
+
- A flow step must be able to fail: end on what ANOTHER island renders. Adding an island means adding a
|
|
28
|
+
step asserting its own render.
|
|
29
|
+
- The final look is a FRESH agent's, given only the state URLs, where the app's vocabulary lives, and
|
|
30
|
+
the question "does anything render what this app never uses?" — never your diff or reasoning.
|
|
31
|
+
|
|
32
|
+
Checks
|
|
33
|
+
- Before saying UI work is done: the host's own typecheck and tests (motu checks neither), `motu check`,
|
|
34
|
+
then `motu integrate check`. Runtime checks are a punctual gate: name the region you touched
|
|
35
|
+
(`motu archipelago verify <id> --runtime`); `motu check --runtime` once, before handing over.
|
|
36
|
+
- Exit codes: 0 holds · 1 contradicted → repair · 2 could not run → retry, do NOT repair. A check that
|
|
37
|
+
examined nothing has not passed.
|
|
38
|
+
- Several agents on one region: you own ONE slot; do not repair shared infrastructure (report exit 2);
|
|
39
|
+
never symlink `node_modules` into a worktree.
|
|
40
|
+
|
|
41
|
+
Handover
|
|
42
|
+
- A person must be able to open it: `motu lagoon serve --watch --detach` (or `motu lagoon publish
|
|
43
|
+
--remote`), and end your report with the URL and the state to look at.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Build a whole screen as a motu region with several agents at once — survey it, declare every slot and owner up front as `planned`, fan out one agent per island, then merge, write the region's flows and drive the result in the lagoon.
|
|
3
|
+
tools: ['codebase', 'editFiles', 'search', 'runCommands', 'problems', 'usages', 'changes']
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Build a screen with several agents
|
|
7
|
+
|
|
8
|
+
Build one SCREEN as a motu region, with several agents working at the same time. You write the
|
|
9
|
+
CONTRACT they build against and integrate what comes back; you do not write the islands. This is the
|
|
10
|
+
workflow the [`archipelago-build` agent](../agents/archipelago-build.agent.md) drives — read that agent
|
|
11
|
+
file and the repo `README.md` for the full rules; do not duplicate them here.
|
|
12
|
+
|
|
13
|
+
Shape of the run: survey the page → declare every slot, every owner and `planned: true` for all of
|
|
14
|
+
them → verify that the region is green before anything exists → one agent per slot in its own
|
|
15
|
+
worktree → merge (regenerate the generated files, never hand-merge them) → write the region's flows →
|
|
16
|
+
open the lagoon and drive it.
|
|
17
|
+
|
|
18
|
+
Use it when a new screen has three or more islands. For a single island use `island-create`; for
|
|
19
|
+
reading a page without building anything use `island-locate`.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Create a brand-new motu island (not migrating legacy) — pick/create the archipelago, build + verify in the lagoon against mock fixtures (including reactive behaviour and sibling-island connections), raise integration concerns, integrate into the ocean, validate with the ocean's own build/tests, then hand off to a human.
|
|
3
|
+
tools: ['codebase', 'editFiles', 'search', 'runCommands', 'problems', 'usages', 'changes']
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Create a new motu island
|
|
7
|
+
|
|
8
|
+
Build a **new** UI capability as a motu island, designed fresh (not lifted from the ocean). Close the
|
|
9
|
+
loop in the **lagoon** first; integrate only once it passes. The `motu` CLI does the deterministic
|
|
10
|
+
work; you own the judgement and the sequence. This is the workflow the
|
|
11
|
+
[`island-create` agent](../agents/island-create.agent.md) drives — read that agent file and the repo
|
|
12
|
+
`README.md` for the full rules; do not duplicate them here.
|
|
13
|
+
|
|
14
|
+
## Workflow (in order)
|
|
15
|
+
|
|
16
|
+
1. **Archipelago first** — decide the page/region the island lives on. Reuse one under
|
|
17
|
+
`demo-app/src/archipelagos/`, or `pnpm motu archipelago init <id>`. Know which store keys it
|
|
18
|
+
reads/writes and which sibling islands it coordinates with.
|
|
19
|
+
2. **Build in the lagoon:** `pnpm motu island create <name>` (scaffolds `ui/<kebab>/` component +
|
|
20
|
+
`islands/<kebab>/` mount point). Write the mode-agnostic component (no fetch/history/document, all
|
|
21
|
+
I/O via `@motujs/contract`, renders from default props), fill the `contract` (input/output), and the
|
|
22
|
+
mock fixtures — use a **function `response`** for reactive filtering and declare `scenarios`.
|
|
23
|
+
3. **Test behaviour + sibling connections** offline: `MOTU_NO_SSL=1 pnpm dev:lagoon`; drive a store
|
|
24
|
+
key and watch the reader island react.
|
|
25
|
+
4. **Verify:** `pnpm motu island verify <name>` (real-browser lagoon, native & legacy fit, data-flow).
|
|
26
|
+
Fix every `✗` until **PASS**. Then WRITE DOWN the integration risks the lagoon can't cover
|
|
27
|
+
(bound-empty inputs, unfired channels, host coupling, stub fixtures, legacy CSS/digest/session).
|
|
28
|
+
5. **Integrate:** `pnpm motu island integrate <name> --archipelago <id>`; fill the `bind`/`on` wiring
|
|
29
|
+
(connect to siblings via the store), and add the `<motu-island slot="…">` marker to the legacy page
|
|
30
|
+
(`legacy-toggle="true"` for progressive rollout).
|
|
31
|
+
6. **Validate with the ocean's own checks:** `pnpm typecheck` + `pnpm build:bridge`, then the host
|
|
32
|
+
app's compilation + unit + integration/e2e suites (ask the human for commands if unknown).
|
|
33
|
+
7. **Hand off to a human** — present the verify PASS, the step-4 integration concerns, and the ocean
|
|
34
|
+
suite result. The human is the final gate; do not mark done without their validation.
|
|
35
|
+
|
|
36
|
+
Never work around a verify failure, widen backend surface beyond the needed `@BrowserCallable` method,
|
|
37
|
+
or add island-to-island imports / runtime module loading (see README "Non-goals").
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Make UI the app ALREADY HAS into a motu island — on a React host the island wraps the component the app owns (no component is written); on an AngularJS ocean host the legacy source is rewritten as a mode-agnostic React component. Loop against `motu island verify` until PASS, then integrate into an archipelago.
|
|
3
|
+
tools: ['codebase', 'editFiles', 'search', 'runCommands', 'problems', 'usages', 'changes']
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Make an existing component a motu island
|
|
7
|
+
|
|
8
|
+
Take one piece of UI the application already has and make it a **motu island**. The `motu` CLI does
|
|
9
|
+
the deterministic work; you do the judgement. **Close the loop on `motu island verify`, not on your
|
|
10
|
+
own judgement.** This is the workflow the
|
|
11
|
+
[`island-extract` agent](../agents/island-extract.agent.md) drives; read that agent file and the
|
|
12
|
+
repo `README.md` for the full rules — do not duplicate them here.
|
|
13
|
+
|
|
14
|
+
**Step 0 — read `motu.config.json`'s `host`, it decides the whole shape.** `next`/`vite`/`none` → WRAP:
|
|
15
|
+
the island points at the component the app owns and you write no component; copying it into `ui/`
|
|
16
|
+
forks it. `angularjs` → REWRITE: there is no React component to wrap, so author one under `ui/`.
|
|
17
|
+
`isolation` is NOT this discriminator — it is shadow vs light DOM, and every reference project is
|
|
18
|
+
`light`, the AngularJS one included.
|
|
19
|
+
|
|
20
|
+
Ask the developer for the component (or legacy source path) and the target archipelago/page if they
|
|
21
|
+
are not obvious.
|
|
22
|
+
|
|
23
|
+
## Workflow
|
|
24
|
+
|
|
25
|
+
1. **Read what exists.** WRAP: find the component and the specifier the app imports it by, then list
|
|
26
|
+
what it cannot render without — providers, its own fetching, router/session, required props with no
|
|
27
|
+
default. REWRITE: controller/JS + template/partial + the REST/service it calls; note what it
|
|
28
|
+
renders, the actions it emits, and every server call.
|
|
29
|
+
2. **Scaffold.** WRAP: `motu island create <name> --from '<the app's own specifier>' [--export <Name>]`.
|
|
30
|
+
REWRITE: `motu island create <name>`.
|
|
31
|
+
3. **Data access.** WRAP: the component keeps the app's; a `channel` answers what reacts and the app's
|
|
32
|
+
own captured mock data answers what it reads. REWRITE: map every call to `@motujs/contract` — if a
|
|
33
|
+
method isn't `@BrowserCallable` yet, annotate it in the backend, rebuild, then `motu codegen`.
|
|
34
|
+
4. **The component.** WRAP: write NONE. What it cannot render without goes in `providers` (every view,
|
|
35
|
+
idempotent — gate a `<Router>` on `useInRouterContext()`), arrangement in `layout`, data in `seed`.
|
|
36
|
+
A wrapper that only installs providers is motu-only code in the app's repo. REWRITE: author
|
|
37
|
+
`ui/<kebab>/<Pascal>.tsx` — props in / callbacks out, no fetch/history/document, all I/O via the
|
|
38
|
+
contract, renders from default props alone.
|
|
39
|
+
5. **Evidence.** Both: scenarios whose outputs DIFFER, or `data-flow` fails. WRAP: point at the capture
|
|
40
|
+
the app already keeps for its own tests — never hand-write a third copy — and import shared evidence
|
|
41
|
+
with a RELATIVE specifier (`@/` does not resolve in the loaders, and the failure is silent).
|
|
42
|
+
REWRITE: fill the evidence file (shapes match the contract returns; a function `response` gives
|
|
43
|
+
reactive filtering) and set the required `legacy` strategy (`fill`/`inline`/`structural`).
|
|
44
|
+
6. **Verify:** `motu island verify <name>` — boots the focused lagoon in a real browser
|
|
45
|
+
(Playwright/Chromium; one-time `cd packages/cli && npx playwright install chromium`). Fix every `✗`,
|
|
46
|
+
re-run until **PASS**. `--fast` uses happy-dom for quick iteration; look at it with
|
|
47
|
+
`motu lagoon serve --watch`.
|
|
48
|
+
7. **Integrate:** `motu archipelago init <page-id>` (if needed) →
|
|
49
|
+
`motu island integrate <name> --archipelago <page-id>`; fill the `TODO(motu:wiring)`
|
|
50
|
+
bindings/handlers, place the region in the page (`createRegion` + `useRegion` on a React host, a
|
|
51
|
+
`<motu-island slot="…">` marker in a legacy template), then `motu integrate check`.
|
|
52
|
+
8. **Finish:** `motu island verify <name>` (expect PASS) and the host's own build/typecheck.
|
|
53
|
+
|
|
54
|
+
For a BRAND-NEW island (nothing exists yet), use the `island-create` agent/prompt instead —
|
|
55
|
+
it adds the archipelago-first, sibling-connection, ocean-validation, and human-gate steps.
|
|
56
|
+
|
|
57
|
+
Never work around a verify failure, copy a component the app owns into `ui/` on a React host, widen
|
|
58
|
+
backend surface beyond the needed `@BrowserCallable` method, or add runtime module loading /
|
|
59
|
+
island-to-island imports (see README "Non-goals").
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Survey one page and locate its island candidates — what becomes an island, what stays host, who owns each shared key, and whether the page itself becomes an archipelago. Produces a ranked LOCATE REPORT and creates no files; island-create / island-extract execute it.
|
|
3
|
+
tools: ['codebase', 'search', 'usages', 'problems']
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Locate the islands on a page
|
|
7
|
+
|
|
8
|
+
Read ONE page and decide what motu should be told about it. This is the reading step before
|
|
9
|
+
`island-create` / `island-extract` — it produces a **report**, not files. This is the workflow the
|
|
10
|
+
[`island-locate` agent](../agents/island-locate.agent.md) drives — read that agent file, the repo
|
|
11
|
+
`README.md` and the host's motu rules block for the full rules; do not duplicate them here.
|
|
12
|
+
|
|
13
|
+
## Workflow (in order)
|
|
14
|
+
|
|
15
|
+
0. **Learn the project's shape** — `motu.config.json`: paths, `tagPrefix`, and above all `host`, which
|
|
16
|
+
answers whether a React component already exists. `next`/`vite`/`none` → the island WRAPS the app's
|
|
17
|
+
own component; never propose lifting it into `ui/`, a copy forks it. `angularjs` → there is nothing
|
|
18
|
+
to wrap, so one is authored under `ui/`. (`isolation` is NOT this discriminator — it is shadow vs
|
|
19
|
+
light DOM, and every reference project is `light`, the AngularJS one included.) Read the existing
|
|
20
|
+
archipelagos and `islands/` — a candidate may already exist, or belong to a region already.
|
|
21
|
+
1. **Enumerate what the page RENDERS** top-down: every panel, banner, card, navigator, control row,
|
|
22
|
+
empty and error state — with its file and its props. Follow every prop to its source.
|
|
23
|
+
2. **Classify each region.** SHOWS content or ACTS ON the region → island (even if other islands sit
|
|
24
|
+
inside it — DOM nesting says nothing about ownership; `slots` declares who fills which prop). Only
|
|
25
|
+
ARRANGES, or is an overlay anchored to a DOM id, or is the composition root → host.
|
|
26
|
+
3. **Find the real couplings.** Each value is one of three things: island-owned (`writes`, event →
|
|
27
|
+
key), host-fed/derived (`bind`, not a write), or page-local (not region state at all). The tell for
|
|
28
|
+
a laundered coupling: the page computes something out of island A's callback and hands it to island
|
|
29
|
+
B. Name the readers and the writer of every key; a key with no reader is not a key.
|
|
30
|
+
4. **Rank** by value (carries a coupling · is the page's content · keeps breaking · reused · about to
|
|
31
|
+
change) ÷ cost (own fetching · host session/context/router · reaches `document` · drilled props ·
|
|
32
|
+
cannot render from defaults · states no input can reach). Then override the ranking with one rule:
|
|
33
|
+
**start with the island that carries the coupling.**
|
|
34
|
+
5. **Answer the archipelago question** — does the page become one (yes as soon as it holds islands;
|
|
35
|
+
say whether it is genuinely coupled or a mix), and what is its SCOPE: the **page**, never a DOM
|
|
36
|
+
subtree. Scoping to the div that "contains the islands" cuts through the one real coupling.
|
|
37
|
+
6. **Write the LOCATE REPORT**: archipelago id + verdict, the region-keys table (key | owner |
|
|
38
|
+
readers | seeded?), ranked candidates (tag, slot, binds, writes, slots, scenarios, next verb),
|
|
39
|
+
what stays host and why, the flows to declare in `<id>.evidence.ts`, and the unknowns/risks the
|
|
40
|
+
lagoon cannot cover. Close with **the first move**: one candidate, one verb, one command.
|
|
41
|
+
|
|
42
|
+
Read-only: no scaffolding, no refactor, no invented keys or events, no islands proposed for
|
|
43
|
+
arrangement to inflate the count. The report is a proposal — a human picks the order and the scope.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// The AngularJS ocean's contribution to the lagoon's Vite config.
|
|
2
|
+
//
|
|
3
|
+
// Unlike the Next host, this one adds no aliases: a workspace-linked ocean resolves @motujs/* through
|
|
4
|
+
// node_modules already. What it does add is the part a legacy backend forces — a single-origin dev
|
|
5
|
+
// proxy and HTTPS.
|
|
6
|
+
//
|
|
7
|
+
// WHAT THE PROXY IS FOR, now that the lagoon is mock-only to look at: `motu fixtures record <island>
|
|
8
|
+
// --transport http` boots this server against the real legacy backend and captures the responses
|
|
9
|
+
// into fixtures. That build is the only one that ever asks for the `http` transport — a human
|
|
10
|
+
// browsing the lagoon always gets mock, because an address has to resolve to a declared state.
|
|
11
|
+
//
|
|
12
|
+
// It is a dev proxy, NOT CORS. The browser talks only to https://localhost:5173; Vite forwards
|
|
13
|
+
// /api/* to the running app server. Auth is why this must be HTTPS: the login page is reCAPTCHA
|
|
14
|
+
// site-key locked, so you do NOT log in through the proxy — you log in at the real origin, and its
|
|
15
|
+
// session + XSRF cookies are host-only `localhost` Secure cookies, which therefore also flow to
|
|
16
|
+
// https://localhost:5173 (same host, port-agnostic).
|
|
17
|
+
//
|
|
18
|
+
// The proxy TARGET is a project fact, so it comes from lagoon.config.json ("proxy"), not from here.
|
|
19
|
+
export async function contribute({ lagoonJson, env, resolveBuildDep }) {
|
|
20
|
+
// MOTU_NO_SSL=1 serves plain HTTP (no dev cert) — used for mock-backed design previews and tooling
|
|
21
|
+
// that don't need the session-cookie flow. Default stays HTTPS for real backend proxying.
|
|
22
|
+
const noSsl = env.MOTU_NO_SSL === '1';
|
|
23
|
+
const basicSsl = noSsl ? null : await resolveBuildDep('@vitejs/plugin-basic-ssl');
|
|
24
|
+
|
|
25
|
+
const proxy = lagoonJson.proxy
|
|
26
|
+
? Object.fromEntries(
|
|
27
|
+
Object.entries(lagoonJson.proxy).map(([path, target]) => [
|
|
28
|
+
path,
|
|
29
|
+
{
|
|
30
|
+
target,
|
|
31
|
+
changeOrigin: false,
|
|
32
|
+
secure: false, // self-signed dev cert
|
|
33
|
+
cookieDomainRewrite: '',
|
|
34
|
+
},
|
|
35
|
+
]),
|
|
36
|
+
)
|
|
37
|
+
: undefined;
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
plugins: basicSsl ? [basicSsl()] : [],
|
|
41
|
+
server: { https: !noSsl, ...(proxy ? { proxy } : {}) },
|
|
42
|
+
};
|
|
43
|
+
}
|