@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,108 @@
|
|
|
1
|
+
// Vite aliases point next/* here: the lagoon is a plain SPA with no Next runtime, but an island — or
|
|
2
|
+
// the app's own layout, on a SCREEN — is still allowed to render a link or read the router.
|
|
3
|
+
//
|
|
4
|
+
// INERT BY DEFAULT. Navigation intents belong to the host bridge (@motujs/adapter-next), so outside a
|
|
5
|
+
// screen these render and no-op: a link click is swallowed and logged, never followed (following it
|
|
6
|
+
// left the lagoon for a 404). If an island needs more of Next than this, it is coupling to the host
|
|
7
|
+
// too tightly.
|
|
8
|
+
//
|
|
9
|
+
// ON A SCREEN they follow the screen's declared routes (`@motujs/react` screens, experimental). A
|
|
10
|
+
// screen renders the application's own layout, and a Next layout navigates with <Link> and
|
|
11
|
+
// `useRouter()` rather than with motu intents — the sidebar of an app adopting motu is usually not a
|
|
12
|
+
// region. So a click or `router.push(path)` is handed to the mounted screen, which opens whichever
|
|
13
|
+
// screen serves `path` (or refuses out loud), and `usePathname()` / `useParams()` report the screen's
|
|
14
|
+
// path so the layout lights the right item. They talk through two window events, which keeps this
|
|
15
|
+
// adapter free of any dependency on @motujs/react.
|
|
16
|
+
import { createElement, forwardRef, useSyncExternalStore, type ComponentProps, type MouseEvent } from 'react';
|
|
17
|
+
|
|
18
|
+
/** Mirrors `window.__motuScreen` (lagoon-screen.tsx): set while a screen is showing. */
|
|
19
|
+
interface ScreenLocation {
|
|
20
|
+
ok?: boolean;
|
|
21
|
+
path?: string;
|
|
22
|
+
params?: Record<string, string>;
|
|
23
|
+
}
|
|
24
|
+
const screenNow = (): ScreenLocation | undefined =>
|
|
25
|
+
typeof window === 'undefined' ? undefined : (window as unknown as { __motuScreen?: ScreenLocation }).__motuScreen;
|
|
26
|
+
const onScreen = () => screenNow()?.ok === true && typeof screenNow()?.path === 'string';
|
|
27
|
+
|
|
28
|
+
/** Hand a path to the mounted screen; outside one, stay inert and say so. */
|
|
29
|
+
function go(path: string): void {
|
|
30
|
+
if (onScreen()) {
|
|
31
|
+
window.dispatchEvent(new CustomEvent('motu:screen-navigate', { detail: path }));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
console.log('[next stub] navigate', path, '— inert outside a screen');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const subscribe = (fn: () => void) => {
|
|
38
|
+
window.addEventListener('motu:screen-location', fn);
|
|
39
|
+
return () => window.removeEventListener('motu:screen-location', fn);
|
|
40
|
+
};
|
|
41
|
+
const pathSnapshot = () => (onScreen() ? (screenNow()!.path as string) : '/');
|
|
42
|
+
const paramsKey = () => JSON.stringify(onScreen() ? screenNow()!.params ?? {} : {});
|
|
43
|
+
|
|
44
|
+
const hrefOf = (href: unknown): string => {
|
|
45
|
+
if (typeof href === 'string') return href;
|
|
46
|
+
if (href && typeof href === 'object' && 'pathname' in href) return String((href as { pathname?: unknown }).pathname ?? '#');
|
|
47
|
+
return '#';
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const Link = forwardRef<HTMLAnchorElement, ComponentProps<'a'> & { href?: unknown; prefetch?: unknown; replace?: unknown; scroll?: unknown }>(
|
|
51
|
+
function Link({ href, children, onClick, prefetch, replace, scroll, ...rest }, ref) {
|
|
52
|
+
void prefetch;
|
|
53
|
+
void replace;
|
|
54
|
+
void scroll;
|
|
55
|
+
const to = hrefOf(href);
|
|
56
|
+
return createElement(
|
|
57
|
+
'a',
|
|
58
|
+
{
|
|
59
|
+
...rest,
|
|
60
|
+
ref,
|
|
61
|
+
href: to,
|
|
62
|
+
onClick: (e: MouseEvent<HTMLAnchorElement>) => {
|
|
63
|
+
onClick?.(e);
|
|
64
|
+
if (e.defaultPrevented) return;
|
|
65
|
+
// A modified click (new tab, download) keeps the browser's own meaning.
|
|
66
|
+
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) return;
|
|
67
|
+
if (/^[a-z]+:/i.test(to) || to.startsWith('#')) return; // external, mailto:, in-page anchors
|
|
68
|
+
e.preventDefault();
|
|
69
|
+
go(to);
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
children,
|
|
73
|
+
);
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
export const Image = forwardRef<HTMLImageElement, ComponentProps<'img'>>(function Image(props, ref) {
|
|
78
|
+
return createElement('img', { ...props, ref });
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const noop = () => {};
|
|
82
|
+
|
|
83
|
+
export function useRouter() {
|
|
84
|
+
return { push: (p: string) => go(p), replace: (p: string) => go(p), back: noop, forward: noop, refresh: noop, prefetch: noop };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function usePathname() {
|
|
88
|
+
return useSyncExternalStore(subscribe, pathSnapshot, () => '/');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function useSearchParams() {
|
|
92
|
+
const path = usePathname();
|
|
93
|
+
return new URLSearchParams(path.includes('?') ? path.slice(path.indexOf('?') + 1) : '');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function useParams() {
|
|
97
|
+
return JSON.parse(useSyncExternalStore(subscribe, paramsKey, () => '{}')) as Record<string, string>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Server-side in Next; a page that calls these is not something the lagoon renders. Fail loudly. */
|
|
101
|
+
export function redirect(path: string): never {
|
|
102
|
+
throw new Error(`[next stub] redirect("${path}") — a server redirect has no meaning in the lagoon`);
|
|
103
|
+
}
|
|
104
|
+
export function notFound(): never {
|
|
105
|
+
throw new Error('[next stub] notFound() — a server 404 has no meaning in the lagoon');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export default Link;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// The Next host's contribution to the lagoon's Vite config.
|
|
2
|
+
//
|
|
3
|
+
// This is host-adapter knowledge, not project configuration, which is why it lives here rather than in
|
|
4
|
+
// every adopting project's vite.config.ts: mapping `next/*` to inert stubs, speaking the host's `@/…`
|
|
5
|
+
// alias, and loading the host's own Tailwind are true of EVERY Next project motu is dropped into.
|
|
6
|
+
// A project declares only what is genuinely its own — module stand-ins and browser env — in
|
|
7
|
+
// lagoon.config.json.
|
|
8
|
+
import { resolve, dirname } from 'node:path';
|
|
9
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
|
|
12
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The lagoon's Tailwind setup — the HOST's config, with one thing changed, GENERATED into `.motu/cache`
|
|
16
|
+
* rather than committed.
|
|
17
|
+
*
|
|
18
|
+
* Tailwind resolves `content` globs against the current working directory, and here that is the lagoon
|
|
19
|
+
* rather than the app. The host's own './components/**' globs would therefore match nothing, Tailwind
|
|
20
|
+
* would emit no utilities at all, and every island would render unstyled — a preview that quietly lies
|
|
21
|
+
* about how the component looks in the app. Re-anchoring the globs on the host root fixes that, and the
|
|
22
|
+
* island sources are added so a class used ONLY inside an island still gets generated. Everything else
|
|
23
|
+
* (theme, plugins, dark mode, breakpoints) is inherited, so the lagoon and the app cannot drift apart
|
|
24
|
+
* on design tokens.
|
|
25
|
+
*
|
|
26
|
+
* It is written to disk rather than passed as an object because Tailwind's own loader is what handles a
|
|
27
|
+
* host config that `require()`s CJS plugins — hand it a path and that keeps working; hand it an
|
|
28
|
+
* imported object and the ESM bundling that breaks on is back.
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* The host's own Tailwind config, if it has one.
|
|
32
|
+
*
|
|
33
|
+
* A Next app does not necessarily use Tailwind, and this adapter used to assume it did: the postcss
|
|
34
|
+
* plugin was installed unconditionally and pointed at `<hostRoot>/tailwind.config`, so a Next host
|
|
35
|
+
* without one got a lagoon that 500s on the first CSS request with "Cannot find module" naming a
|
|
36
|
+
* generated file the project never wrote. Found by motu's own lagoon host, which is deliberately
|
|
37
|
+
* Tailwind-free — acme, the adapter's only other consumer, has Tailwind, so nothing had ever asked.
|
|
38
|
+
*/
|
|
39
|
+
function hostTailwindConfig(paths) {
|
|
40
|
+
for (const name of ['tailwind.config.ts', 'tailwind.config.js', 'tailwind.config.mjs', 'tailwind.config.cjs']) {
|
|
41
|
+
const candidate = resolve(paths.hostRoot, name);
|
|
42
|
+
if (existsSync(candidate)) return candidate;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* `writeIfChanged` comes from the CLI's contribute context, and using it is not housekeeping: this file
|
|
49
|
+
* lands in `.motu/cache`, which Vite WATCHES, and it is regenerated on every lagoon build. Writing the
|
|
50
|
+
* same bytes still bumps mtime, which still full-reloads every open lagoon tab — so a check running in
|
|
51
|
+
* another terminal used to reload the page a person was looking at. See the CLI's write-if-changed.mjs.
|
|
52
|
+
*/
|
|
53
|
+
function generateTailwindConfig(paths, writeIfChanged) {
|
|
54
|
+
mkdirSync(paths.cacheDir, { recursive: true });
|
|
55
|
+
const out = resolve(paths.cacheDir, 'tailwind.lagoon.config.ts');
|
|
56
|
+
writeIfChanged(
|
|
57
|
+
out,
|
|
58
|
+
`// GENERATED by motu (@motujs/adapter-next). Do not edit; do not commit.
|
|
59
|
+
import { resolve } from 'node:path';
|
|
60
|
+
import hostConfig from ${JSON.stringify(resolve(paths.hostRoot, 'tailwind.config'))};
|
|
61
|
+
|
|
62
|
+
const HOST_ROOT = ${JSON.stringify(paths.hostRoot)};
|
|
63
|
+
const hostContent = Array.isArray(hostConfig.content) ? hostConfig.content : (hostConfig.content?.files ?? []);
|
|
64
|
+
|
|
65
|
+
export default {
|
|
66
|
+
...hostConfig,
|
|
67
|
+
content: [
|
|
68
|
+
...hostContent.map((glob) => (typeof glob === 'string' ? resolve(HOST_ROOT, glob) : glob)),
|
|
69
|
+
${JSON.stringify(resolve(paths.appRoot, 'src/**/*.{ts,tsx}'))},
|
|
70
|
+
],
|
|
71
|
+
};
|
|
72
|
+
`,
|
|
73
|
+
);
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function contribute({ paths, lagoonJson, resolveBuildDep, writeIfChanged }) {
|
|
78
|
+
// Only when the HOST has one. See hostTailwindConfig: assuming it did is what made this adapter
|
|
79
|
+
// unusable on a Next app that styles itself any other way.
|
|
80
|
+
const usesTailwind = hostTailwindConfig(paths) !== null;
|
|
81
|
+
const tailwindcss = usesTailwind ? await resolveBuildDep('tailwindcss') : null;
|
|
82
|
+
const autoprefixer = await resolveBuildDep('autoprefixer');
|
|
83
|
+
const stubs = resolve(HERE, 'lagoon/next-stubs.tsx');
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
define: {
|
|
87
|
+
// The host application's modules read process.env AT IMPORT TIME — a Supabase client that throws
|
|
88
|
+
// when its keys are missing is enough to take the whole lagoon down before anything renders, and
|
|
89
|
+
// the error ("process is not defined") says nothing about why. There is no backend here, so the
|
|
90
|
+
// values only have to exist: declare them under "env" in lagoon.config.json. Never put a real
|
|
91
|
+
// secret there — the lagoon gets published as a static page.
|
|
92
|
+
'process.env': JSON.stringify({ NODE_ENV: 'development', ...(lagoonJson.env ?? {}) }),
|
|
93
|
+
},
|
|
94
|
+
alias: [
|
|
95
|
+
// (the framework's own @motujs/* aliases are added unconditionally by buildLagoonViteConfig)
|
|
96
|
+
// Project aliases from lagoon.config.json "alias" — BEFORE the host's own '@/…' rule, because the
|
|
97
|
+
// first matching entry wins and '^@/' would otherwise rewrite the specifier to the real module
|
|
98
|
+
// before this ever sees it. This is the seam for standing a host module down inside the lagoon —
|
|
99
|
+
// a React context whose real provider talks to a backend, a service module that fetches on import
|
|
100
|
+
// — without touching the component that consumes it. Paths are relative to the lagoon root.
|
|
101
|
+
...Object.entries(lagoonJson.alias ?? {}).map(([find, replacement]) => ({
|
|
102
|
+
find,
|
|
103
|
+
replacement: resolve(paths.lagoonDir, replacement),
|
|
104
|
+
})),
|
|
105
|
+
// The lagoon mounts components written for the Next app, so it speaks the same module language:
|
|
106
|
+
// the host's '@/…' alias, and next/* mapped to inert stubs.
|
|
107
|
+
{ find: /^@\//, replacement: paths.hostRoot + '/' },
|
|
108
|
+
{ find: /^next\/link$/, replacement: stubs },
|
|
109
|
+
{ find: /^next\/image$/, replacement: stubs },
|
|
110
|
+
{ find: /^next\/navigation$/, replacement: stubs },
|
|
111
|
+
],
|
|
112
|
+
css: {
|
|
113
|
+
postcss: {
|
|
114
|
+
plugins: [
|
|
115
|
+
...(usesTailwind ? [tailwindcss({ config: generateTailwindConfig(paths, writeIfChanged) })] : []),
|
|
116
|
+
autoprefixer(),
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// The generic Vite host's contribution to the lagoon's Vite config.
|
|
2
|
+
//
|
|
3
|
+
// Every other adapter here RESTATES its framework: the Next one knows about `next/*` stubs, the app's
|
|
4
|
+
// `@/…` alias and Tailwind, because a Next project's build is conventional enough to describe once.
|
|
5
|
+
// A Vite application is not conventional — it IS its vite.config.ts — and any list motu wrote of what
|
|
6
|
+
// such a project might need (linaria? lingui? svgr? a tsconfig-paths plugin? which aliases?) would be
|
|
7
|
+
// a restatement that drifts the day the app adds a plugin.
|
|
8
|
+
//
|
|
9
|
+
// So this adapter states nothing. It LOADS the host's own config with Vite's own loader and borrows
|
|
10
|
+
// two things from it: the plugins that transform the host's source, and the aliases that resolve it.
|
|
11
|
+
// Same principle as the transport seam — consume the application's artifact rather than reproduce it —
|
|
12
|
+
// and the reason it is worth the file is that it is what lets the lagoon render the host's REAL
|
|
13
|
+
// components instead of stand-ins.
|
|
14
|
+
//
|
|
15
|
+
// Two classes of plugin are dropped, both because the lagoon is a different application than the one
|
|
16
|
+
// the config was written for:
|
|
17
|
+
// - BUILD REPORTERS (visualizer, bundle-size guards) assert on an output the lagoon does not produce
|
|
18
|
+
// - the host's REACT plugin is kept, and motu's is then skipped: two React transforms in one
|
|
19
|
+
// pipeline is a duplicated JSX runtime, which surfaces as hooks failing in ways nothing explains
|
|
20
|
+
import { resolve } from 'node:path';
|
|
21
|
+
import { existsSync } from 'node:fs';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Plugins the lagoon does not borrow, because they describe a build it is not doing.
|
|
25
|
+
*
|
|
26
|
+
* Reporters and size guards assert on an output that does not exist here. SERVICE-WORKER plugins are
|
|
27
|
+
* the addition, and they are the sharpest case: `vite-plugin-pwa` contributes five plugins, and they
|
|
28
|
+
* do not merely go unused — `dev-sw` adds an `options` hook and `build` a `generateBundle`, which
|
|
29
|
+
* between them build the SERVICE WORKER from `srcDir` resolved into the lagoon's copied root, where
|
|
30
|
+
* nothing is. The whole build dies before anything renders:
|
|
31
|
+
*
|
|
32
|
+
* [UNRESOLVED_ENTRY] Cannot resolve entry module .motu/cache/lagoon/src/service-worker.ts
|
|
33
|
+
*
|
|
34
|
+
* GUARDING THEM IS NOT ENOUGH, and that was tried first: the guard below drops a plugin whose hook
|
|
35
|
+
* THROWS, and this one does not throw. It succeeds and asks the build for an entry that cannot exist,
|
|
36
|
+
* so the failure lands in rolldown's entry resolution where no plugin hook can catch it.
|
|
37
|
+
*
|
|
38
|
+
* A service worker means nothing in a preview — it is offline caching for a deployed app — so this
|
|
39
|
+
* costs the lagoon nothing and is not a workaround.
|
|
40
|
+
*
|
|
41
|
+
* Measured on shlink (Vite + npm): the lagoon was unbootable, `motu lagoon serve` failed three times
|
|
42
|
+
* identically, and the adopter's fix was to fork the app's Vite config into their own repository —
|
|
43
|
+
* motu-only code in the host, which adopting motu exists to avoid.
|
|
44
|
+
*/
|
|
45
|
+
const DROP = /visualizer|bundle|size-?limit|checker|legacy|pwa|service-?worker|workbox/i;
|
|
46
|
+
/** A React transform, whatever the flavour (plugin-react, plugin-react-swc, plugin-react-oxc). */
|
|
47
|
+
const IS_REACT = /^vite:react|react-swc|react-oxc|react-babel|react-refresh/i;
|
|
48
|
+
|
|
49
|
+
export async function contribute({ paths, lagoonJson }) {
|
|
50
|
+
const configFile = lagoonJson.hostViteConfig
|
|
51
|
+
? resolve(paths.hostRoot, lagoonJson.hostViteConfig)
|
|
52
|
+
: ['vite.config.ts', 'vite.config.js', 'vite.config.mts'].map((f) => resolve(paths.hostRoot, f)).find(existsSync);
|
|
53
|
+
|
|
54
|
+
if (!configFile || !existsSync(configFile)) {
|
|
55
|
+
// Not an error: a Vite host with no config is a Vite host with nothing to borrow.
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const vite = await import('vite');
|
|
60
|
+
// `serve`, not `build` — a config often branches on the command, and what the lagoon runs is a dev
|
|
61
|
+
// server. The host's `mode` env (loadEnv) is read by the config itself; nothing here supplies one.
|
|
62
|
+
const loaded = await vite.loadConfigFromFile({ command: 'serve', mode: 'development' }, configFile, paths.hostRoot);
|
|
63
|
+
if (!loaded?.config) return {};
|
|
64
|
+
|
|
65
|
+
const hostPlugins = (loaded.config.plugins ?? []).flat(Infinity).filter(Boolean);
|
|
66
|
+
const named = hostPlugins.filter((p) => typeof p?.name === 'string');
|
|
67
|
+
const kept = named.filter((p) => !DROP.test(p.name));
|
|
68
|
+
|
|
69
|
+
// A HOST PLUGIN THAT CANNOT RUN HERE IS DROPPED BY NAME, NOT ALLOWED TO KILL THE LAGOON.
|
|
70
|
+
//
|
|
71
|
+
// These are ALREADY-INSTANTIATED plugin objects, whose hooks closed over the config they were built
|
|
72
|
+
// for. The lagoon's root and envDir are not the host's, so a plugin whose `config`/`configResolved`
|
|
73
|
+
// hook asserts on them throws — and the throw propagates out of Vite before anything renders.
|
|
74
|
+
// Measured twice, on unrelated plugins: Mastodon's theme plugin
|
|
75
|
+
// (`if (!userConfig.root || !userConfig.envDir) throw new Error('Unknown project directory')`) and
|
|
76
|
+
// shlink's `vite-plugin-pwa` (resolving `srcDir` against the lagoon's copied root). Both times the
|
|
77
|
+
// whole lagoon was unbootable and the adopter's fix was to fork the app's Vite config into their own
|
|
78
|
+
// repository — motu-only code in the host, which adopting motu is supposed to avoid.
|
|
79
|
+
//
|
|
80
|
+
// So each borrowed plugin's config-time hooks are guarded. One that throws is removed and SAID, and
|
|
81
|
+
// the rest of the host's pipeline still transforms the host's real components. A dropped transform
|
|
82
|
+
// may of course mean a component that no longer compiles — which is a legible failure naming the
|
|
83
|
+
// plugin, instead of an assertion from a file the adopter did not write.
|
|
84
|
+
const dropped = [];
|
|
85
|
+
// EVERY HOOK THAT CAN FAIL, AND ASYNCHRONOUSLY. The first version wrapped four CONFIG-TIME hooks
|
|
86
|
+
// with a synchronous try/catch, which missed the case its own comment above names.
|
|
87
|
+
//
|
|
88
|
+
// `vite-plugin-pwa` does not throw while configuring. It succeeds, and then launches its OWN build
|
|
89
|
+
// for the service worker from `closeBundle` — against `srcDir` resolved into the lagoon's copied
|
|
90
|
+
// root, where nothing is:
|
|
91
|
+
//
|
|
92
|
+
// [UNRESOLVED_ENTRY] Cannot resolve entry module .motu/cache/lagoon/src/service-worker.ts
|
|
93
|
+
//
|
|
94
|
+
// Two reasons the guard could not see it: `closeBundle` was not in the list, and that child build is
|
|
95
|
+
// ASYNC — it rejects a promise rather than throwing, so a synchronous `catch` never runs. Measured
|
|
96
|
+
// on shlink: the lagoon was unbootable, `lagoon serve` failed three times identically, and the
|
|
97
|
+
// adopter forked the app's Vite config into their own repo to get past it — motu-only code in the
|
|
98
|
+
// host, which adopting motu exists to avoid.
|
|
99
|
+
//
|
|
100
|
+
// A build-time hook that fails is dropped for the REST of the run too: a service worker half-built
|
|
101
|
+
// once is not worth attempting again on the next rebuild.
|
|
102
|
+
const HOOKS = [
|
|
103
|
+
'config',
|
|
104
|
+
'configResolved',
|
|
105
|
+
'configureServer',
|
|
106
|
+
'options',
|
|
107
|
+
'buildStart',
|
|
108
|
+
'transformIndexHtml',
|
|
109
|
+
'renderStart',
|
|
110
|
+
'generateBundle',
|
|
111
|
+
'writeBundle',
|
|
112
|
+
'buildEnd',
|
|
113
|
+
'closeBundle',
|
|
114
|
+
];
|
|
115
|
+
const guard = (plugin) => {
|
|
116
|
+
const wrapped = { ...plugin };
|
|
117
|
+
const record = (hook, err) => {
|
|
118
|
+
if (!dropped.some((d) => d.name === plugin.name)) {
|
|
119
|
+
dropped.push({ name: plugin.name, hook, message: String(err?.message ?? err).split('\n')[0] });
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
for (const hook of HOOKS) {
|
|
123
|
+
const original = plugin[hook];
|
|
124
|
+
if (typeof original !== 'function') continue;
|
|
125
|
+
wrapped[hook] = function (...args) {
|
|
126
|
+
// Already failed once in this run: do not call it again.
|
|
127
|
+
if (dropped.some((d) => d.name === plugin.name)) return undefined;
|
|
128
|
+
try {
|
|
129
|
+
const out = original.apply(this, args);
|
|
130
|
+
// A REJECTED PROMISE IS A FAILURE TOO, and it is the one that mattered here.
|
|
131
|
+
return out && typeof out.then === 'function'
|
|
132
|
+
? out.then(undefined, (err) => {
|
|
133
|
+
record(hook, err);
|
|
134
|
+
return undefined;
|
|
135
|
+
})
|
|
136
|
+
: out;
|
|
137
|
+
} catch (err) {
|
|
138
|
+
record(hook, err);
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return wrapped;
|
|
144
|
+
};
|
|
145
|
+
const plugins = kept.map(guard);
|
|
146
|
+
// Reported through the returned contribution so the CLI can print it beside its other notices; the
|
|
147
|
+
// array is filled as the hooks run, which is after this function returns.
|
|
148
|
+
const droppedHostPlugins = dropped;
|
|
149
|
+
const hasReact = kept.some((p) => IS_REACT.test(p.name));
|
|
150
|
+
|
|
151
|
+
// The host's aliases, ABSOLUTE against the host root. A config that wrote them relative meant them
|
|
152
|
+
// relative to itself, and the lagoon's root is elsewhere.
|
|
153
|
+
//
|
|
154
|
+
// BOTH SHAPES. Vite accepts `resolve.alias` as an array of {find, replacement} OR as a plain object
|
|
155
|
+
// of find -> replacement, and this assumed the array — so a host using the object form (the shape
|
|
156
|
+
// Vite's own docs lead with) died on `.map is not a function` before the lagoon could build. Found
|
|
157
|
+
// by the first greenfield project on a vite host, whose config was written the documented way.
|
|
158
|
+
const declared = loaded.config.resolve?.alias ?? [];
|
|
159
|
+
const entries = Array.isArray(declared)
|
|
160
|
+
? declared
|
|
161
|
+
: Object.entries(declared).map(([find, replacement]) => ({ find, replacement }));
|
|
162
|
+
const alias = entries.map((a) =>
|
|
163
|
+
typeof a?.replacement === 'string' && !a.replacement.startsWith('/') && /^[./]/.test(a.replacement)
|
|
164
|
+
? { ...a, replacement: resolve(paths.hostRoot, a.replacement) }
|
|
165
|
+
: a,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
plugins,
|
|
170
|
+
/** Filled while the host's plugins run: those whose config-time hooks threw in this pipeline. */
|
|
171
|
+
droppedHostPlugins,
|
|
172
|
+
// Told to the lagoon builder, which owns the decision to add its own React plugin or not.
|
|
173
|
+
ownsReactTransform: hasReact,
|
|
174
|
+
alias,
|
|
175
|
+
// `tsconfigPaths: true` is a Vite 6+ resolve option; pass it through when the host uses it, since
|
|
176
|
+
// that is often the ONLY thing mapping the app's `@/…` imports.
|
|
177
|
+
...(loaded.config.resolve?.tsconfigPaths ? { resolveExtra: { tsconfigPaths: true } } : {}),
|
|
178
|
+
};
|
|
179
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,67 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@motujs/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "The motu command line: scaffold islands, check that a page region still matches what it declares, and serve, snapshot or publish the lagoon preview.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"motu",
|
|
7
|
+
"react",
|
|
8
|
+
"islands",
|
|
9
|
+
"ui-architecture",
|
|
10
|
+
"state-management",
|
|
11
|
+
"cli",
|
|
12
|
+
"ui-testing",
|
|
13
|
+
"visual-regression",
|
|
14
|
+
"component-preview",
|
|
15
|
+
"storybook-alternative"
|
|
16
|
+
],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/Scorbutics/motu.git",
|
|
21
|
+
"directory": "packages/cli"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/Scorbutics/motu#readme",
|
|
24
|
+
"type": "module",
|
|
25
|
+
"bin": {
|
|
26
|
+
"motu": "src/cli.mjs"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@happy-dom/global-registrator": "^20.11.2",
|
|
30
|
+
"@vitejs/plugin-basic-ssl": "^1.1.0",
|
|
31
|
+
"@vitejs/plugin-react": "^4.3.3",
|
|
32
|
+
"autoprefixer": "^10.4.20",
|
|
33
|
+
"axe-core": "^4.13.0",
|
|
34
|
+
"happy-dom": "^20.11.2",
|
|
35
|
+
"pixelmatch": "^7.2.0",
|
|
36
|
+
"playwright": "^1.62.1",
|
|
37
|
+
"pngjs": "^7.0.0",
|
|
38
|
+
"tailwindcss": "^3.4.0",
|
|
39
|
+
"ts-morph": "^28.0.0",
|
|
40
|
+
"tsx": "^4.23.12",
|
|
41
|
+
"typescript": "^5.6.3",
|
|
42
|
+
"vite": "^5.4.10",
|
|
43
|
+
"@motujs/adapter-angularjs": "0.1.1",
|
|
44
|
+
"@motujs/chrome": "0.1.1",
|
|
45
|
+
"@motujs/adapter-next": "0.1.1",
|
|
46
|
+
"@motujs/core": "0.1.1",
|
|
47
|
+
"@motujs/debug-overlay": "0.1.1",
|
|
48
|
+
"@motujs/react": "0.1.1",
|
|
49
|
+
"@motujs/runtime": "0.1.1",
|
|
50
|
+
"@motujs/coverage": "0.1.1",
|
|
51
|
+
"@motujs/types": "0.1.1"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"react": "^18.3.1",
|
|
55
|
+
"react-dom": "^18.3.1"
|
|
56
|
+
},
|
|
57
|
+
"files": [
|
|
58
|
+
"src",
|
|
59
|
+
"bundled"
|
|
60
|
+
],
|
|
61
|
+
"publishConfig": {
|
|
62
|
+
"access": "public"
|
|
63
|
+
},
|
|
64
|
+
"scripts": {
|
|
65
|
+
"test": "node --test test/*.test.mjs"
|
|
66
|
+
}
|
|
6
67
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// A node ESM resolve hook that speaks the LAGOON's aliases.
|
|
2
|
+
//
|
|
3
|
+
// The happy-dom path (`--fast`) is plain node, which knows nothing about `@/…`, `motu-islands` or the
|
|
4
|
+
// raw-TypeScript `@motujs/*` checkout — so it could not load an island that imports anything from its
|
|
5
|
+
// own app, which is every island in a real project.
|
|
6
|
+
//
|
|
7
|
+
// NOT tsconfig `paths`, which is the obvious answer and the wrong one: those are a TYPE resolution
|
|
8
|
+
// map, and a project may legitimately point one at a `.d.ts`. acme maps `react` to
|
|
9
|
+
// `@types/react/index.d.ts` on purpose, to force one React type definition across a checkout boundary
|
|
10
|
+
// — follow that at runtime and node tries to execute a declaration file.
|
|
11
|
+
//
|
|
12
|
+
// The aliases here are the ones motu already gives Vite, so the browser path and the node path resolve
|
|
13
|
+
// the same specifier to the same file by construction rather than by two lists someone keeps in sync.
|
|
14
|
+
import { readFileSync } from 'node:fs';
|
|
15
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
16
|
+
import { ASSET, ASSET_STUB, aliasEntries, mapSpecifier } from './lib/alias-core.mjs';
|
|
17
|
+
|
|
18
|
+
const entries = aliasEntries();
|
|
19
|
+
|
|
20
|
+
export async function resolve(specifier, context, nextResolve) {
|
|
21
|
+
// A stylesheet is not a module. Answering with an inert one keeps the component's own code running,
|
|
22
|
+
// which is the thing being checked; refusing would fail an island for importing its own CSS.
|
|
23
|
+
if (ASSET.test(specifier)) return { url: pathToFileURL(ASSET_STUB).href, shortCircuit: true };
|
|
24
|
+
|
|
25
|
+
const file = mapSpecifier(specifier, entries);
|
|
26
|
+
if (file) {
|
|
27
|
+
try {
|
|
28
|
+
return asModuleSource(await nextResolve(pathToFileURL(file).href, context));
|
|
29
|
+
} catch {
|
|
30
|
+
// An alias that resolves to nothing loadable is not fatal — fall through, so one stale entry
|
|
31
|
+
// cannot make every import fail, and node reports the original specifier honestly.
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return asModuleSource(await nextResolve(specifier, context));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const SOURCE = /\.(ts|tsx|jsx)$/;
|
|
38
|
+
const ESM_SYNTAX = /^\s*(import|export)\b/m;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* An application's own TypeScript is ESM, whatever its `package.json` says.
|
|
42
|
+
*
|
|
43
|
+
* A Next or Vite app is written in `import`/`export` and compiled by a bundler, so its package.json
|
|
44
|
+
* routinely has no `"type": "module"`. Plain node then classifies every `.tsx` in it as CommonJS, tsx
|
|
45
|
+
* compiles it that way, and the island's named import of it fails — "does not provide an export
|
|
46
|
+
* named 'ActionsGuide'" — because node cannot see esbuild's CJS exports. The registry imports every
|
|
47
|
+
* island, so ONE such file made `--fast` die for every island in the project, under an error naming
|
|
48
|
+
* an unrelated one. Bundlers never ask this question; this lane has to answer it the way they do.
|
|
49
|
+
*
|
|
50
|
+
* Narrow on purpose: only a TS/JSX source outside node_modules that node would load as CommonJS and
|
|
51
|
+
* that is written in ESM syntax. A genuinely CommonJS file keeps its format.
|
|
52
|
+
*/
|
|
53
|
+
function asModuleSource(result) {
|
|
54
|
+
if (!result?.url?.startsWith('file:') || result.format === 'module') return result;
|
|
55
|
+
if (!SOURCE.test(result.url) || result.url.includes('/node_modules/')) return result;
|
|
56
|
+
if (result.format && result.format !== 'commonjs') return result;
|
|
57
|
+
try {
|
|
58
|
+
if (!ESM_SYNTAX.test(readFileSync(fileURLToPath(result.url), 'utf8'))) return result;
|
|
59
|
+
} catch {
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
return { ...result, format: 'module' };
|
|
63
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// `--import` RUNS a module; it does not install resolver hooks. Only `module.register` does, and
|
|
2
|
+
// forgetting that is silent: the hook file loads, exports `resolve`, and node never calls it.
|
|
3
|
+
//
|
|
4
|
+
// Both module systems need teaching, and only one of them is served by a hook. An island's `import`
|
|
5
|
+
// goes through the ESM resolver; a `require` deeper in the graph does not, and acme died on exactly
|
|
6
|
+
// that — past `@/app` through the hook, then `Cannot find module '@/lib/utils'` from CJS.
|
|
7
|
+
import { createRequire, register } from 'node:module';
|
|
8
|
+
import { ASSET, ASSET_STUB_CJS, aliasEntries, mapSpecifier } from './lib/alias-core.mjs';
|
|
9
|
+
|
|
10
|
+
register('./alias-loader.mjs', import.meta.url);
|
|
11
|
+
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
const Module = require('node:module');
|
|
14
|
+
const entries = aliasEntries();
|
|
15
|
+
const original = Module._resolveFilename;
|
|
16
|
+
|
|
17
|
+
Module._resolveFilename = function (request, ...rest) {
|
|
18
|
+
if (ASSET.test(request)) return ASSET_STUB_CJS;
|
|
19
|
+
const mapped = mapSpecifier(request, entries);
|
|
20
|
+
if (mapped) {
|
|
21
|
+
try {
|
|
22
|
+
return original.call(this, mapped, ...rest);
|
|
23
|
+
} catch {
|
|
24
|
+
// fall through to the original request, so a bad alias reports the real name
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return original.call(this, request, ...rest);
|
|
28
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// What a stylesheet or an image becomes under the node harness. A bundler turns these into a class
|
|
2
|
+
// map or a URL; node cannot parse them at all, and refusing to load one would fail an island for
|
|
3
|
+
// importing its own CSS — which says nothing about whether the island works.
|
|
4
|
+
export default '';
|
|
5
|
+
export const __motuAssetStub = true;
|
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The `motu` entry point — a preflight, then the real CLI.
|
|
3
|
+
//
|
|
4
|
+
// WHY THIS FILE EXISTS. Everything below `run.mjs` is static ESM, so its imports are hoisted and
|
|
5
|
+
// resolved before a single line of ours runs. A missing dependency therefore cannot be caught in
|
|
6
|
+
// there: node prints `ERR_MODULE_NOT_FOUND: Cannot find package 'ts-morph'` with a stack, and exits.
|
|
7
|
+
//
|
|
8
|
+
// Measured on a clean machine, following the README exactly: clone, `./install.sh`, and then EVERY
|
|
9
|
+
// command — including `motu --help` — died that way in about two seconds, because the documented
|
|
10
|
+
// install path never installs the framework's own dependencies. That is the whole first impression a
|
|
11
|
+
// stranger gets, so this checks first and says what to do.
|
|
12
|
+
import { existsSync } from 'node:fs';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
import { dirname, resolve } from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
// Node built-ins only, so it is safe above the dependency check.
|
|
17
|
+
import { INSTALLED } from './lib/framework.mjs';
|
|
18
|
+
|
|
19
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const CHECKOUT = resolve(here, '../../..');
|
|
21
|
+
|
|
22
|
+
/** Packages the CLI needs to do anything at all, in the order a missing one would be hit. */
|
|
23
|
+
const REQUIRED = ['ts-morph', 'tsx'];
|
|
24
|
+
|
|
25
|
+
const require = createRequire(import.meta.url);
|
|
26
|
+
const missing = REQUIRED.filter((name) => {
|
|
27
|
+
try {
|
|
28
|
+
require.resolve(name);
|
|
29
|
+
return false;
|
|
30
|
+
} catch {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (missing.length && INSTALLED) {
|
|
36
|
+
// INSTALLED FROM NPM, the checkout advice below is wrong twice over: there is no checkout to `cd`
|
|
37
|
+
// into, and `./install.sh` does not exist. These are @motujs/cli's own dependencies, so a missing
|
|
38
|
+
// one is an interrupted or pruned install of it — the fix is in the app, not anywhere else.
|
|
39
|
+
console.error(`\x1b[31m✗ motu cannot start: ${missing.join(', ')} ${missing.length > 1 ? 'are' : 'is'} not installed\x1b[0m`);
|
|
40
|
+
console.error(`\x1b[2m ${missing.length > 1 ? 'They are dependencies' : 'It is a dependency'} of @motujs/cli, so its install is incomplete. Reinstall it:\x1b[0m`);
|
|
41
|
+
console.error('');
|
|
42
|
+
console.error(' npm i -D @motujs/cli');
|
|
43
|
+
console.error('');
|
|
44
|
+
process.exit(2);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (missing.length) {
|
|
48
|
+
// A pnpm workspace: `npm install` cannot resolve `workspace:*`, so never suggest it. corepack ships
|
|
49
|
+
// with node and honours the declared `packageManager`, which is the one instruction that works
|
|
50
|
+
// whether or not pnpm is already on PATH.
|
|
51
|
+
const pm = existsSync(resolve(CHECKOUT, 'pnpm-lock.yaml')) ? 'corepack pnpm install' : 'npm install';
|
|
52
|
+
console.error(`\x1b[31m✗ motu cannot start: ${missing.join(', ')} ${missing.length > 1 ? 'are' : 'is'} not installed\x1b[0m`);
|
|
53
|
+
console.error(`\x1b[2m The framework resolves its own build tools from its checkout, so that checkout needs its\x1b[0m`);
|
|
54
|
+
console.error(`\x1b[2m dependencies once — your project still needs none:\x1b[0m`);
|
|
55
|
+
console.error('');
|
|
56
|
+
console.error(` cd ${CHECKOUT} && ${pm}`);
|
|
57
|
+
console.error('');
|
|
58
|
+
console.error(`\x1b[2m Then re-run this command. (\`./install.sh\` does it for you.)\x1b[0m`);
|
|
59
|
+
process.exit(2);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// THE PACKAGES HAVE TO BE BUILT, and only in a checkout. `@motujs/*` publish compiled output — that is
|
|
63
|
+
// what puts them inside a host's node_modules, where `react` resolves to the HOST's copy instead of
|
|
64
|
+
// whatever sits next to motu. Their `exports` therefore point at `dist/`, which is gitignored, so a
|
|
65
|
+
// fresh clone has none: the no-install links land on a package whose entry point does not exist, and
|
|
66
|
+
// the host's bundler says `@motujs/core could not be resolved` — pointing at the consumer, not here.
|
|
67
|
+
// Same failure mode the dependency check above exists for, one layer down.
|
|
68
|
+
if (!INSTALLED && existsSync(resolve(CHECKOUT, 'packages/core/src/index.ts')) && !existsSync(resolve(CHECKOUT, 'packages/core/dist/index.js'))) {
|
|
69
|
+
console.error(`\x1b[31m✗ motu cannot start: the framework packages are not built\x1b[0m`);
|
|
70
|
+
console.error(`\x1b[2m They ship compiled, so their entry points live in dist/ — which a fresh clone does not have:\x1b[0m`);
|
|
71
|
+
console.error('');
|
|
72
|
+
console.error(` cd ${CHECKOUT} && node scripts/build-packages.mjs`);
|
|
73
|
+
console.error('');
|
|
74
|
+
console.error(`\x1b[2m (\`./install.sh\` does it for you.)\x1b[0m`);
|
|
75
|
+
process.exit(2);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
await import('./run.mjs');
|