@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,208 @@
|
|
|
1
|
+
// `motu island integrate <name> --archipelago <id>` — makes an existing island a member of an
|
|
2
|
+
// archipelago (deterministic, in-repo). It:
|
|
3
|
+
// 1. AST-inserts an IslandSpec ({ slot, element, + stubbed bind/on }) into the target archipelago's
|
|
4
|
+
// `islands` array in <archipelagos>/<id>/<id>.archipelago.ts.
|
|
5
|
+
// 2. Injects a <motu-island slot="…"> marker into the archipelago's `layout` when that layout is an
|
|
6
|
+
// inline string; warns (with the exact marker) when the layout is an imported constant.
|
|
7
|
+
// The store bindings and event handlers are judgment calls, so they're left as TODO(motu:wiring) for
|
|
8
|
+
// the extraction skill / a human. Injecting the marker into the *legacy* page is stack-specific and is
|
|
9
|
+
// the extract skill's job, not the CLI's.
|
|
10
|
+
import { existsSync } from 'node:fs';
|
|
11
|
+
import { dirname, relative, resolve } from 'node:path';
|
|
12
|
+
import { Project, QuoteKind, SyntaxKind } from 'ts-morph';
|
|
13
|
+
import { paths, names, color, FMT, islandComponentPath } from '../lib/util.mjs';
|
|
14
|
+
|
|
15
|
+
/** `received-summary` -> `receivedSummary`: a slot is kebab, a component prop is not. */
|
|
16
|
+
function camel(kebab) {
|
|
17
|
+
return kebab.split(/[-_]/).map((w, i) => (i === 0 ? w : w[0].toUpperCase() + w.slice(1))).join('');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The binding name a page would use for this region — `actions` -> `Actions`. */
|
|
21
|
+
function archConstName(id) {
|
|
22
|
+
const c = camel(id);
|
|
23
|
+
return c[0].toUpperCase() + c.slice(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Find the archipelago object literal (`{ id: '<id>', islands: [...] }`) for the given id. */
|
|
27
|
+
function findArchipelago(sf, archId) {
|
|
28
|
+
for (const obj of sf.getDescendantsOfKind(SyntaxKind.ObjectLiteralExpression)) {
|
|
29
|
+
const idProp = obj.getProperty('id')?.asKind(SyntaxKind.PropertyAssignment);
|
|
30
|
+
if (!idProp) continue;
|
|
31
|
+
const idVal = idProp.getInitializer()?.getText().replace(/['"]/g, '');
|
|
32
|
+
if (idVal === archId && obj.getProperty('islands')) return obj;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Give a region's declaration the checks a region WITH islands has to make.
|
|
40
|
+
*
|
|
41
|
+
* Idempotent, and additive only: an existing `wiring`/`produced` is left exactly as written, and a
|
|
42
|
+
* hand-narrowed `Pick<ElementTypes, …>` second type argument is never widened back.
|
|
43
|
+
*/
|
|
44
|
+
function upgradeChecks(sf, arch) {
|
|
45
|
+
// `archipelago<…>()(config, checks)` — `arch` is `config`, so the checks are the sibling argument
|
|
46
|
+
// of the call that owns it. `as const` sits between them often enough to be worth stepping over.
|
|
47
|
+
let node = arch;
|
|
48
|
+
while (node && !node.asKind?.(SyntaxKind.CallExpression)) node = node.getParent?.();
|
|
49
|
+
const call = node?.asKind(SyntaxKind.CallExpression);
|
|
50
|
+
if (!call) return;
|
|
51
|
+
const checks = call.getArguments()[1]?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
52
|
+
if (!checks) return;
|
|
53
|
+
|
|
54
|
+
// The elements map, as the second type argument of the INNER call (`archipelago<R, ElementTypes>()`).
|
|
55
|
+
const inner = call.getExpression().asKind(SyntaxKind.CallExpression);
|
|
56
|
+
if (inner && inner.getTypeArguments().length === 1) {
|
|
57
|
+
inner.insertTypeArgument(1, 'ElementTypes');
|
|
58
|
+
const from = relPosix(dirname(arch.getSourceFile().getFilePath()), resolve(paths.islandsDir, 'registry'));
|
|
59
|
+
if (!sf.getImportDeclaration((d) => d.getModuleSpecifierValue() === from)) {
|
|
60
|
+
sf.addImportDeclaration({ moduleSpecifier: from, namedImports: ['ElementTypes'], isTypeOnly: true });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
for (const name of ['wiring', 'produced']) {
|
|
64
|
+
if (!checks.getProperty(name)) checks.addPropertyAssignment({ name, initializer: 'true' });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** POSIX-relative module specifier (never a Windows backslash in generated code). */
|
|
69
|
+
function relPosix(from, to) {
|
|
70
|
+
const r = relative(from, to).split('\\').join('/');
|
|
71
|
+
return r.startsWith('.') ? r : './' + r;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function integrateCommand(argv) {
|
|
75
|
+
const name = argv._[0];
|
|
76
|
+
const archId = argv.archipelago || argv.a;
|
|
77
|
+
if (!name || !archId) {
|
|
78
|
+
console.error('usage: motu island integrate <name> --archipelago <id> [--slot <slot>]');
|
|
79
|
+
process.exit(2);
|
|
80
|
+
}
|
|
81
|
+
const { pascal, kebab, tag } = names(name);
|
|
82
|
+
const slot = argv.slot || kebab;
|
|
83
|
+
|
|
84
|
+
// Follow element.ts to the component the island mounts — it only lives under ui/ when motu wrote
|
|
85
|
+
// it; an island wrapping the app's own component points outside the project.
|
|
86
|
+
if (!existsSync(islandComponentPath(kebab, pascal))) {
|
|
87
|
+
console.error(color.red(`✗ no island component at ${paths.rel(islandComponentPath(kebab, pascal))} — run \`motu island create ${kebab}\` first`));
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const project = new Project({
|
|
92
|
+
manipulationSettings: { quoteKind: QuoteKind.Single, useTrailingCommas: true },
|
|
93
|
+
skipAddingFilesFromTsConfig: true,
|
|
94
|
+
skipFileDependencyResolution: true,
|
|
95
|
+
compilerOptions: { allowJs: true },
|
|
96
|
+
});
|
|
97
|
+
const archPath = paths.archipelagoFile(archId);
|
|
98
|
+
if (!existsSync(archPath)) {
|
|
99
|
+
console.error(color.red(`✗ no archipelago with id '${archId}' at ${paths.rel(archPath)}`));
|
|
100
|
+
console.error(color.dim(` create one with: motu archipelago init ${archId} --page <the page it lives on>`));
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
const sf = project.addSourceFileAtPath(archPath);
|
|
104
|
+
const arch = findArchipelago(sf, archId);
|
|
105
|
+
if (!arch) {
|
|
106
|
+
console.error(color.red(`✗ couldn't locate the '${archId}' archipelago config in ${archId}.archipelago.ts`));
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const islands = arch.getProperty('islands')?.asKind(SyntaxKind.PropertyAssignment)?.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression);
|
|
111
|
+
if (!islands) {
|
|
112
|
+
console.error(color.red(`✗ archipelago '${archId}' has no islands array to extend`));
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Idempotency: bail if this slot or element tag is already a member.
|
|
117
|
+
const exists = islands.getElements().some((e) => {
|
|
118
|
+
const t = e.getText();
|
|
119
|
+
return t.includes(`slot: '${slot}'`) || t.includes(`element: '${tag}'`);
|
|
120
|
+
});
|
|
121
|
+
if (exists) {
|
|
122
|
+
console.log(color.yellow(`! ${tag} (slot '${slot}') is already a member of '${archId}' — nothing to do`));
|
|
123
|
+
process.exit(0);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
islands.addElement(`{
|
|
127
|
+
slot: '${slot}',
|
|
128
|
+
element: '${tag}',
|
|
129
|
+
// TODO(motu:wiring): bind store keys and/or handle the island's events, e.g.
|
|
130
|
+
// bind: { someProp: 'someStoreKey' },
|
|
131
|
+
// on: { 'some-event': (detail, { store }) => store.set('someStoreKey', detail) },
|
|
132
|
+
}`);
|
|
133
|
+
|
|
134
|
+
// THE CHECKS THIS REGION CAN NOW MAKE, added with the member that makes them possible.
|
|
135
|
+
//
|
|
136
|
+
// `wiring` and `produced` are required of a region WITH islands and meaningless for one without —
|
|
137
|
+
// so `archipelago init` scaffolds neither, and this is the command that changes which of the
|
|
138
|
+
// two a region is. It did not add them, so adding your FIRST island to a freshly scaffolded region
|
|
139
|
+
// stopped the project typechecking, with an error on a line motu generated and the adopter had
|
|
140
|
+
// never edited. `wiring` also needs the generated elements map as the second type argument; a bare
|
|
141
|
+
// tag union silently makes it check nothing, which the type itself refuses.
|
|
142
|
+
upgradeChecks(sf, arch);
|
|
143
|
+
|
|
144
|
+
// WHERE THIS ISLAND IS GOING TO BE PLACED, and the answer depends on how the region composes.
|
|
145
|
+
//
|
|
146
|
+
// A region that declares a `root` places its islands BY PROP: the archipelago maps a prop of the
|
|
147
|
+
// application's own component to a slot, and the page passes that prop. Adding a member without
|
|
148
|
+
// both halves leaves an island that is declared and never placed — `integrate check` says so, but
|
|
149
|
+
// only later, and the command that created the situation is the one that should say it now.
|
|
150
|
+
//
|
|
151
|
+
// The slot is ADDED to `slots` here (it is derivable — one line, keyed by the slot's own name), and
|
|
152
|
+
// what cannot be derived is said out loud: the root component needs a prop, and the page needs to
|
|
153
|
+
// pass it.
|
|
154
|
+
let placementNote;
|
|
155
|
+
const rootProp = arch.getProperty('root')?.asKind(SyntaxKind.PropertyAssignment);
|
|
156
|
+
if (rootProp) {
|
|
157
|
+
const rootName = rootProp.getInitializer()?.getText() ?? 'the root';
|
|
158
|
+
const propName = camel(slot);
|
|
159
|
+
const slotsProp = arch.getProperty('slots')?.asKind(SyntaxKind.PropertyAssignment);
|
|
160
|
+
const slotsObj = slotsProp?.getInitializer()?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
161
|
+
if (slotsObj) {
|
|
162
|
+
if (!slotsObj.getProperty(propName)) slotsObj.addPropertyAssignment({ name: propName, initializer: `'${slot}'` });
|
|
163
|
+
placementNote =
|
|
164
|
+
color.green(`✓ mapped ${propName} -> '${slot}' in \`slots\``) +
|
|
165
|
+
color.dim(`\n now give ${rootName} a \`${propName}\` prop, and pass it from the page:`) +
|
|
166
|
+
color.dim(`\n <${archConstName(archId)}.Root ${propName}={<YourComponent … />} … />`);
|
|
167
|
+
} else {
|
|
168
|
+
placementNote =
|
|
169
|
+
color.yellow(`! '${archId}' declares a root but no \`slots\` map`) +
|
|
170
|
+
color.dim(`\n add: slots: { ${propName}: '${slot}' } then give ${rootName} a \`${propName}\` prop`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Layout marker: inject when the layout is an inline string; otherwise tell the user where to add it.
|
|
175
|
+
let layoutNote;
|
|
176
|
+
const layoutProp = arch.getProperty('layout')?.asKind(SyntaxKind.PropertyAssignment);
|
|
177
|
+
const marker = `<motu-island slot="${slot}" theme="motu" fit="native"></motu-island>`;
|
|
178
|
+
if (rootProp) {
|
|
179
|
+
// A root region has no `layout` template and needs none: the arrangement is the app's component.
|
|
180
|
+
layoutNote = null;
|
|
181
|
+
} else if (layoutProp) {
|
|
182
|
+
const init = layoutProp.getInitializer();
|
|
183
|
+
const kind = init?.getKind();
|
|
184
|
+
if (kind === SyntaxKind.StringLiteral || kind === SyntaxKind.NoSubstitutionTemplateLiteral) {
|
|
185
|
+
const raw = init.getLiteralText();
|
|
186
|
+
init.setLiteralValue(`${raw}\n ${marker}`);
|
|
187
|
+
layoutNote = color.green(`✓ added <motu-island slot="${slot}"> to the inline layout`);
|
|
188
|
+
} else {
|
|
189
|
+
layoutNote =
|
|
190
|
+
color.yellow('! layout is an imported constant') +
|
|
191
|
+
color.dim(` — add this marker to it manually:\n ${marker}`);
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
layoutNote =
|
|
195
|
+
color.yellow(`! archipelago '${archId}' has no layout`) +
|
|
196
|
+
color.dim(` — add a layout with:\n ${marker}`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
sf.formatText(FMT);
|
|
200
|
+
await sf.save();
|
|
201
|
+
|
|
202
|
+
console.log(color.green(`✓ integrated ${color.bold(tag)} into archipelago ${color.bold(archId)} (slot '${slot}')`));
|
|
203
|
+
console.log(' ' + color.dim(`${paths.rel(archPath)} (islands[] + IslandSpec)`));
|
|
204
|
+
if (placementNote) console.log(' ' + placementNote);
|
|
205
|
+
if (layoutNote) console.log(' ' + layoutNote);
|
|
206
|
+
console.log('');
|
|
207
|
+
console.log('Next: fill the TODO(motu:wiring) bindings/handlers, then ' + color.bold(`motu island verify ${kebab}`));
|
|
208
|
+
}
|