@ultimat3/cli 5.0.1 → 7.0.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.
Files changed (60) hide show
  1. package/CLAUDE.md +75 -6
  2. package/README.md +2 -2
  3. package/package.json +28 -24
  4. package/src/affected.ts +320 -0
  5. package/src/browser-launcher.ts +109 -0
  6. package/src/ci-log.ts +0 -0
  7. package/src/ci-runs.ts +179 -0
  8. package/src/cmd-affected.ts +109 -0
  9. package/src/cmd-build.ts +36 -3
  10. package/src/cmd-ci.ts +273 -0
  11. package/src/cmd-dev.ts +35 -2
  12. package/src/cmd-generate.ts +16 -348
  13. package/src/cmd-i18n.ts +32 -16
  14. package/src/cmd-pr.ts +308 -0
  15. package/src/cmd-shot.ts +320 -0
  16. package/src/cmd-test.ts +96 -7
  17. package/src/cmd-verify.ts +10 -427
  18. package/src/compile-externals.ts +34 -0
  19. package/src/dev-lock.ts +275 -0
  20. package/src/dev-render.ts +7 -17
  21. package/src/error-codes.ts +18 -0
  22. package/src/generate-files.ts +127 -0
  23. package/src/generate-write.ts +229 -0
  24. package/src/gh-target.ts +118 -0
  25. package/src/gh.ts +204 -0
  26. package/src/i18n-audit.ts +39 -1
  27. package/src/i18n-registration.ts +130 -0
  28. package/src/index.ts +37 -0
  29. package/src/island-bundle.ts +68 -2
  30. package/src/island-solid-production.ts +129 -0
  31. package/src/island-styles.ts +41 -0
  32. package/src/mcp-errors.ts +11 -0
  33. package/src/messages.ts +67 -0
  34. package/src/pr-threads.ts +291 -0
  35. package/src/prerender.ts +52 -10
  36. package/src/registry.ts +8 -0
  37. package/src/shot-verdict.ts +337 -0
  38. package/src/solid-loader.ts +127 -0
  39. package/src/static-report.ts +219 -0
  40. package/src/templates/admin-page.ts +46 -5
  41. package/src/templates/index.ts +1 -0
  42. package/src/templates/island-fixture.ts +76 -0
  43. package/src/templates/island.ts +129 -18
  44. package/src/templates/resource-form-island.ts +279 -0
  45. package/src/templates/resource.ts +52 -43
  46. package/src/templates/route.ts +45 -6
  47. package/src/templates/scaffold-app.ts +70 -19
  48. package/src/templates/scaffold-container.ts +2 -2
  49. package/src/templates/scaffold-db-package.ts +88 -39
  50. package/src/templates/scaffold-docs.ts +18 -1
  51. package/src/templates/scaffold-i18n.ts +9 -2
  52. package/src/templates/scaffold-mcp-package.ts +35 -2
  53. package/src/templates/scaffold-package-shape.ts +7 -2
  54. package/src/templates/scaffold-repo.ts +2 -2
  55. package/src/test-shards.ts +19 -3
  56. package/src/verify-checks.ts +349 -0
  57. package/src/verify-run.ts +122 -0
  58. package/src/verify-step.ts +7 -0
  59. package/src/workspace-graph.ts +241 -0
  60. package/types/babel-modules.d.ts +31 -0
@@ -0,0 +1,130 @@
1
+ // Every finding about an app's strings, and the one composition `x i18n check` and `x verify`'s
2
+ // `i18n` step both report — two callers, one answer, so the command and the gate can never
3
+ // disagree. The runtime half is here because nothing else could ask it: `i18n-audit.ts` compares
4
+ // source against files on disk and was green for every string of a shipped app whose catalog
5
+ // module nothing imported (issue #249).
6
+
7
+ import type { Catalog, Extraction, ExtractReport, Locale } from '@ultimat3/i18n';
8
+ import {
9
+ auditCatalogs,
10
+ catalogFor,
11
+ catalogMissingKeys,
12
+ catalogRegistrationGaps,
13
+ catalogsNeverRegistered,
14
+ catalogUnregistered,
15
+ registeredLocales,
16
+ } from '@ultimat3/i18n';
17
+ import { loadApp } from './app-load';
18
+ import { auditApp } from './i18n-audit';
19
+ import type { Finding } from './output';
20
+ import { findingFrom } from './output';
21
+ import { catalogPath } from './templates/locales';
22
+
23
+ /**
24
+ * What this check needs of a boot. The seam is injected so a fixture can be exactly "the app
25
+ * loaded and registered nothing" — the shipped shape of the bug — without a temp directory that
26
+ * can resolve `@ultimat3/*`. `loadApp` is the production value, and it is the same call
27
+ * `serveApp` makes: asking a different loader than the server uses would prove nothing.
28
+ */
29
+ export type AppLoader = (root: string) => Promise<{
30
+ readonly findings: readonly Finding[];
31
+ readonly defaultLocale: string;
32
+ }>;
33
+
34
+ export interface RegistrationInput {
35
+ readonly root: string;
36
+ /** The catalogs on disk, parsed — `packages/i18n/catalogs/*.json`. */
37
+ readonly catalogs: Readonly<Record<Locale, Catalog>>;
38
+ readonly extraction: Extraction;
39
+ readonly ignoreUnused: readonly string[];
40
+ readonly load?: AppLoader;
41
+ }
42
+
43
+ export interface RegistrationReport {
44
+ readonly ok: boolean;
45
+ readonly findings: readonly Finding[];
46
+ /** Keys that would render a loud miss because registration never happened. */
47
+ readonly unregistered: number;
48
+ /** How many locales are affected — one row of the summary's "across N locale(s)". */
49
+ readonly locales: number;
50
+ /** Which shipped locales the registry cannot fully answer — the `registered` column's `no`. */
51
+ readonly unregisteredLocales: readonly Locale[];
52
+ /** Every locale the registry holds after the load, sorted. Empty is not possible in a real
53
+ * boot: the framework's own catalog is the base layer, so `['en']` is the floor. */
54
+ readonly registered: readonly Locale[];
55
+ }
56
+
57
+ /**
58
+ * The app ships nothing on disk, so there is no file to diff — the only evidence left is whether
59
+ * the keys source actually uses resolve. Audited through `auditCatalogs` rather than a fresh
60
+ * `hasOwn` loop, because a plural family is defined as `n_one`/`n_other` and a bare lookup of the
61
+ * stem `n` would report every plural in the app as unresolved.
62
+ */
63
+ function unresolvedUsedKeys(input: RegistrationInput, locale: Locale): readonly string[] {
64
+ const report = auditCatalogs({
65
+ extraction: input.extraction,
66
+ catalogs: { [locale]: catalogFor(locale) },
67
+ ignoreUnused: input.ignoreUnused,
68
+ });
69
+ return report.locales[0]?.missing ?? [];
70
+ }
71
+
72
+ export async function checkRegistration(input: RegistrationInput): Promise<RegistrationReport> {
73
+ // Importing the app's modules IS the registration, in this process exactly as in the server's.
74
+ const app = await (input.load ?? loadApp)(input.root);
75
+
76
+ const gaps = catalogRegistrationGaps(input.catalogs);
77
+ const findings: Finding[] = gaps.map((gap) => ({
78
+ ...findingFrom(catalogUnregistered(gap)),
79
+ at: catalogPath(gap.locale),
80
+ }));
81
+ let unregistered = gaps.reduce((sum, gap) => sum + gap.missing.length, 0);
82
+ let locales = gaps.length;
83
+
84
+ if (Object.keys(input.catalogs).length === 0) {
85
+ const unresolved = unresolvedUsedKeys(input, app.defaultLocale);
86
+ if (unresolved.length > 0) {
87
+ findings.push(findingFrom(catalogsNeverRegistered(app.defaultLocale, unresolved)));
88
+ unregistered += unresolved.length;
89
+ locales += 1;
90
+ }
91
+ }
92
+
93
+ return {
94
+ ok: findings.length === 0,
95
+ // The load's own findings ride along ONLY when something is unregistered, and that condition is
96
+ // the whole value: a module that would not import registers nothing, so "packages/i18n/src/
97
+ // index.ts: SyntaxError" is the evidence for the gap above it. With every catalog registered, a
98
+ // broken route file is not this command's business and reporting it would be noise on a pass.
99
+ findings: findings.length === 0 ? findings : [...findings, ...app.findings],
100
+ unregistered,
101
+ locales,
102
+ unregisteredLocales: gaps.map((gap) => gap.locale),
103
+ registered: registeredLocales(),
104
+ };
105
+ }
106
+
107
+ /**
108
+ * The file half: a key source uses that a locale's catalog does not define. Built here rather than
109
+ * in `cmd-i18n.ts` because `x verify`'s `i18n` step reports the same finding, and a second
110
+ * construction of it is two renderers of one fact waiting to drift.
111
+ */
112
+ export function missingKeyFindings(report: ExtractReport): readonly Finding[] {
113
+ return report.locales
114
+ .filter((audit) => audit.missing.length > 0)
115
+ .map((audit) => ({
116
+ ...findingFrom(catalogMissingKeys(audit.locale, audit.missing)),
117
+ at: catalogPath(audit.locale),
118
+ }));
119
+ }
120
+
121
+ /**
122
+ * Both halves of one question — does every string this app renders resolve? — for a caller that
123
+ * wants the verdict and not the table. `x verify`'s `i18n` step is that caller; `cmd-i18n.ts`
124
+ * composes the same two pieces itself because it also renders per-locale rows.
125
+ */
126
+ export async function catalogFindings(root: string): Promise<readonly Finding[]> {
127
+ const { report, catalogs, extraction, ignoreUnused } = await auditApp(root);
128
+ const registration = await checkRegistration({ root, catalogs, extraction, ignoreUnused });
129
+ return [...missingKeyFindings(report), ...registration.findings];
130
+ }
package/src/index.ts CHANGED
@@ -60,6 +60,12 @@ export {
60
60
  plannedCommands,
61
61
  plannedSubcommand,
62
62
  } from './cmd-planned';
63
+ // `shotCommand`, `prCommand` and `ciCommand` are deliberately NOT re-exported here. They reach
64
+ // `x` through `registry.ts`, which is the only thing that makes a command exist — and the barrel
65
+ // is the surface an APP imports. Exporting them puts `cmd-shot.ts` in the module graph of every
66
+ // app that imports `@ultimat3/cli`, which then has to resolve `@ultimat3/scraping` — a browser
67
+ // driver it never uses. Measured: it reds `tsc -b` on `dummy/social-media-clone` with five
68
+ // TS2307s in files that app never calls. The app path does not pay for the tool path.
63
69
  export { actionsCommand, entitiesCommand, queriesCommand } from './cmd-registries';
64
70
  export { renderRouteTable, routesCommand } from './cmd-routes';
65
71
  export { testCommand } from './cmd-test';
@@ -177,6 +183,13 @@ export type { DeclaredFlag } from './flag-reads';
177
183
  export { checkFlagReads, declaredFlags, readsFlag } from './flag-reads';
178
184
  export type { Guard } from './guards';
179
185
  export { findingProblem, GUARD_DIR, guardFindings, guardPaths } from './guards';
186
+ // The island bundler, and only its entry point. An island is the one module Ultimate ships to a
187
+ // browser, so an app has to be able to build one to TEST one — `mountIsland` from
188
+ // `@ultimat3/testing` takes this function as its `build` parameter (issue #260). `discoverIslands`,
189
+ // `islandBundle`, `writeIslands`, `ISLAND_BASE_PATH` and `ISLAND_GLOB` stay internal: they are
190
+ // `x build`'s and `x dev`'s wiring, and every name here is a semver promise forever.
191
+ export type { IslandBundle, IslandChunk } from './island-bundle';
192
+ export { buildIslands } from './island-bundle';
180
193
  export type { DrainFailure, DrainOutcome, DrainSkip } from './jobs-drain';
181
194
  export { drainJobs } from './jobs-drain';
182
195
  export type { JobsListFilter, JobsListResult } from './jobs-report';
@@ -235,6 +248,24 @@ export {
235
248
  isVendored,
236
249
  SOURCE_GLOBS,
237
250
  } from './source-files';
251
+ export type {
252
+ EmittedPage,
253
+ RouteFacts,
254
+ SkippedRoute,
255
+ SkipReason,
256
+ StaticReport,
257
+ } from './static-report';
258
+ export {
259
+ parseStaticReport,
260
+ readStaticReport,
261
+ removeStaticReport,
262
+ renderStaticReport,
263
+ SKIP_REASONS,
264
+ STATIC_REPORT_FILE,
265
+ skippedRoute,
266
+ skipReasonFor,
267
+ writeStaticReport,
268
+ } from './static-report';
238
269
  export type { TestCounts } from './test-counts';
239
270
  export { countsOf } from './test-counts';
240
271
  export type { TestFile } from './test-select';
@@ -286,4 +317,10 @@ export {
286
317
  SEMVER,
287
318
  workspacePackages,
288
319
  } from './workspace-checks';
320
+ export type { WorkspaceNode, WorkspaceScan } from './workspace-graph';
321
+ // The graph itself, not just the gate's verdict on it: issue #239's complaint is that a
322
+ // scaffolded repo's dependency graph exists only inside `tsc`, so an app's own tooling has
323
+ // nothing to read. `checkWorkspaceDependencies` stays internal — it is reached through
324
+ // `x verify`, which is the one way a rule is enforced here.
325
+ export { readWorkspaceGraph, scanWorkspaces } from './workspace-graph';
289
326
  export { writeLine } from './write-line';
@@ -13,6 +13,9 @@ import {
13
13
  islandModuleId,
14
14
  } from '@ultimat3/render';
15
15
  import { IslandBuildFailedError } from './errors';
16
+ import { solidProductionPlugin } from './island-solid-production';
17
+ import { islandStylesPlugin } from './island-styles';
18
+ import { solidJsxPlugin } from './solid-loader';
16
19
 
17
20
  /**
18
21
  * Where a chunk is served from, in `x dev`, in the container and in a static export — one base
@@ -78,6 +81,18 @@ async function buildOne(root: string, file: string): Promise<IslandChunk> {
78
81
  format: 'esm',
79
82
  splitting: false,
80
83
  minify: true,
84
+ // A build with no `plugins` is a build with no JSX transform: `Bun.plugin` installs into the
85
+ // RUNTIME's loader and `Bun.build` walks its own graph, so render's `.tsx` loader never sees
86
+ // an island. The app's tsconfig says `jsx: "preserve"`, which makes the bundler fall back to
87
+ // classic `React.createElement` — emitted into a browser chunk that imports no React, with
88
+ // `success: true` and no log. Every island shipped that way through five majors.
89
+ //
90
+ // The other two close the same shape of failure — a wrong answer `Bun.build` reports as
91
+ // `success: true`: without the second, `target: 'browser'` resolves the `development`
92
+ // export condition and the chunk carries Solid's dev build; without the third, Bun's file
93
+ // loader resolves a `.module.scss` to its asset PATH, so `styles['x']` is `undefined` and
94
+ // every element renders unclassed.
95
+ plugins: [solidJsxPlugin, solidProductionPlugin, islandStylesPlugin],
81
96
  });
82
97
  } catch (error) {
83
98
  throw new IslandBuildFailedError({ file, logs: describeBuildError(error) });
@@ -114,13 +129,64 @@ function describeBuildError(error: unknown): string {
114
129
  return error instanceof Error ? error.message : String(error);
115
130
  }
116
131
 
132
+ export interface BuildIslandsOptions {
133
+ /**
134
+ * Build ONE island, named app-root-relative — the whole option surface. A test that mounts a
135
+ * single island otherwise pays every OTHER island's Babel pass and `Bun.build` on every file,
136
+ * and the reference app is the one that feels it.
137
+ *
138
+ * Optional, and it must stay optional: `buildIslands` is on `@ultimat3/cli`'s public surface and
139
+ * `@ultimat3/testing`'s `IslandBuilder` satisfies it STRUCTURALLY as `(root: string) => …`, which
140
+ * is what keeps the `cli -> testing` edge pointing the one legal way.
141
+ */
142
+ readonly only?: string;
143
+ }
144
+
117
145
  /** Build every island in the app. An app with none returns an empty bundle and costs one glob. */
118
- export async function buildIslands(root: string): Promise<IslandBundle> {
119
- const files = await discoverIslands(root);
146
+ export async function buildIslands(
147
+ root: string,
148
+ options: BuildIslandsOptions = {},
149
+ ): Promise<IslandBundle> {
150
+ const discovered = await discoverIslands(root);
151
+ const only = options.only;
152
+ const files = only === undefined ? discovered : discovered.filter((file) => file === only);
153
+ // A filter that matches nothing is a typo in the CALLER, never an app with no islands. Answering
154
+ // an empty bundle here would surface two steps later, as a chunk table with no entry for a file
155
+ // the caller can see on disk.
156
+ if (only !== undefined && files.length === 0) throw onlyMissing(only, discovered);
120
157
  const chunks = await Promise.all(files.map((file) => buildOne(root, file)));
121
158
  return islandBundle(chunks);
122
159
  }
123
160
 
161
+ /**
162
+ * Same code as an unbuildable `src`: "this path cannot become a client entry" is one condition.
163
+ *
164
+ * Two fixes, because there are two causes and only one of them can be repaired by naming a path.
165
+ * The line was `pass only: '<app-root-relative path>.island.tsx'` — a placeholder nobody can run,
166
+ * which no gate could see: `fixProblem` fails a fix only for ADVICE with no command token, and a
167
+ * sentence with neither is not advice. Both forms below are constructed from what the caller
168
+ * already handed in, so neither can name a path this app does not have.
169
+ */
170
+ function onlyMissing(only: string, discovered: readonly string[]): IslandInvalidError {
171
+ const cause =
172
+ `buildIslands was asked for ${JSON.stringify(only)} alone, which is not one of the ` +
173
+ `${discovered.length} islands this app has (${discovered.length === 0 ? 'none' : discovered.join(', ')})`;
174
+ // The basename match first: a filter that misses normally missed on the PREFIX — a route-relative
175
+ // specifier where `discoverIslands`' app-root-relative path was wanted — and the filename
176
+ // survives that. Falling back to the first keeps the fix a real path rather than a shape.
177
+ const nearest =
178
+ discovered.find((file) => posix.basename(file) === posix.basename(only)) ?? discovered[0];
179
+ // An app with no islands cannot be pointed at one, so the fix WRITES the file that was asked
180
+ // for — the same command `entryMissing` hands back, split off the same path.
181
+ if (nearest === undefined) {
182
+ return new IslandInvalidError(
183
+ cause,
184
+ `x g island ${posix.basename(only, ISLAND_EXTENSION)} --at ${posix.dirname(only)}`,
185
+ );
186
+ }
187
+ return new IslandInvalidError(cause, `buildIslands(root, { only: '${nearest}' })`);
188
+ }
189
+
124
190
  export function islandBundle(chunks: readonly IslandChunk[]): IslandBundle {
125
191
  const byFile = new Map(chunks.map((chunk) => [chunk.file, chunk]));
126
192
  const byUrl = new Map(chunks.map((chunk) => [chunk.url, chunk]));
@@ -0,0 +1,129 @@
1
+ // Every `solid-js` import in an island chunk resolves to Solid's PRODUCTION browser build.
2
+ // `Bun.build({ target: 'browser' })` always adds the `development` export condition and offers no
3
+ // option that removes it — `conditions`, `production`, `env` and `define` were each measured under
4
+ // Bun 1.4 and none of them does — so without this seam an island ships the dev bundle silently.
5
+
6
+ // `node:path` by necessity: Bun ships no path API, and this file resolves a package entry back
7
+ // to the directory its `exports` map is relative to.
8
+ import { dirname, join } from 'node:path';
9
+ import type { BunPlugin } from 'bun';
10
+ import { IslandBuildFailedError } from './errors';
11
+
12
+ /**
13
+ * The conditions an island's `solid-js` subpath is resolved under. `development` is the one NOT in
14
+ * the set, which is the whole point of the file; `production` is in it because an island chunk is
15
+ * only ever built to be shipped — `x dev` serves the same chunk the container does, so a second
16
+ * answer here would be a bundle the byte budget never measured.
17
+ */
18
+ const ISLAND_CONDITIONS: ReadonlySet<string> = new Set([
19
+ 'production',
20
+ 'browser',
21
+ 'module',
22
+ 'import',
23
+ 'default',
24
+ ]);
25
+
26
+ /** `solid-js` and its subpaths, and nothing else: Solid is the runtime an island is compiled for. */
27
+ const SOLID_SPECIFIER = /^solid-js(?:\/|$)/;
28
+
29
+ /**
30
+ * Node's conditional-exports walk, restricted to what this file needs: the first key of the object
31
+ * that the build's condition set contains, depth-first, with an array as an ordered fallback list.
32
+ * Written out rather than delegated to `Bun.resolveSync` because the ONE thing it has to do
33
+ * differently from Bun's resolver is refuse `development` — and `types` with it, which would
34
+ * otherwise win on Solid's map and hand the bundler a `.d.ts`.
35
+ */
36
+ export function selectCondition(node: unknown, conditions: ReadonlySet<string>): string | null {
37
+ if (typeof node === 'string') return node;
38
+ if (Array.isArray(node)) {
39
+ for (const alternative of node as readonly unknown[]) {
40
+ const picked = selectCondition(alternative, conditions);
41
+ if (picked !== null) return picked;
42
+ }
43
+ return null;
44
+ }
45
+ if (typeof node !== 'object' || node === null) return null;
46
+ for (const [condition, value] of Object.entries(node)) {
47
+ if (!conditions.has(condition)) continue;
48
+ const picked = selectCondition(value, conditions);
49
+ if (picked !== null) return picked;
50
+ }
51
+ return null;
52
+ }
53
+
54
+ /** One parse per manifest: an island imports Solid from several files, and every file asks again. */
55
+ const exportsCache = new Map<string, unknown>();
56
+
57
+ async function exportsOf(manifest: string): Promise<unknown> {
58
+ const hit = exportsCache.get(manifest);
59
+ if (hit !== undefined || exportsCache.has(manifest)) return hit;
60
+ const parsed: unknown = JSON.parse(await Bun.file(manifest).text());
61
+ const field =
62
+ typeof parsed === 'object' && parsed !== null && 'exports' in parsed
63
+ ? (parsed as { readonly exports?: unknown }).exports
64
+ : undefined;
65
+ exportsCache.set(manifest, field);
66
+ return field;
67
+ }
68
+
69
+ /** Test seam: the cache is process-global because `x dev` rebuilds in one process. */
70
+ export function clearSolidExportsCache(): void {
71
+ exportsCache.clear();
72
+ }
73
+
74
+ /**
75
+ * The absolute file `specifier` must resolve to, or `null` for "Bun's own answer is already the
76
+ * right one" — which is every subpath Solid declares as a plain string or a pattern, since a
77
+ * declaration with no conditions on it cannot select the development build.
78
+ */
79
+ export async function solidProductionEntry(
80
+ specifier: string,
81
+ resolveDir: string,
82
+ importer: string,
83
+ ): Promise<string | null> {
84
+ let manifest: string;
85
+ try {
86
+ // `solid-js/package.json` is an `exports` entry of Solid's own map, so this reaches the exact
87
+ // copy the island would have imported — not a hoisted sibling at a different version.
88
+ manifest = Bun.resolveSync('solid-js/package.json', resolveDir);
89
+ } catch {
90
+ // Solid is not installed here. Bun's resolver says so, in its own words, naming the importer.
91
+ return null;
92
+ }
93
+ const field = await exportsOf(manifest);
94
+ const subpath = specifier === 'solid-js' ? '.' : `.${specifier.slice('solid-js'.length)}`;
95
+ if (typeof field !== 'object' || field === null || !(subpath in field)) return null;
96
+
97
+ const entry = selectCondition((field as Record<string, unknown>)[subpath], ISLAND_CONDITIONS);
98
+ const file = entry === null ? null : join(dirname(manifest), entry);
99
+ if (file === null || !(await Bun.file(file).exists())) {
100
+ throw new IslandBuildFailedError({
101
+ file: importer.length > 0 ? importer : specifier,
102
+ logs:
103
+ `${specifier} has no production browser entry: ${manifest} answers ` +
104
+ `${entry === null ? 'nothing' : JSON.stringify(entry)} under ` +
105
+ `[${[...ISLAND_CONDITIONS].join(', ')}], and an island may not ship Solid's ` +
106
+ 'development build',
107
+ });
108
+ }
109
+ return file;
110
+ }
111
+
112
+ /**
113
+ * The plugin `island-bundle.ts` hands `Bun.build`, beside `solidJsxPlugin`. Stateless apart from
114
+ * the manifest cache, so one frozen descriptor serves every concurrent island build.
115
+ *
116
+ * The absolute path it answers with no longer matches `SOLID_SPECIFIER`, so Solid's own internal
117
+ * `import … from 'solid-js'` is the only re-entry — and that one is wanted: it is how `web.js`
118
+ * reaches `solid.js` rather than `dev.js`.
119
+ */
120
+ export const solidProductionPlugin: BunPlugin = {
121
+ name: 'ultimate-island-solid-production',
122
+ setup(build): void {
123
+ build.onResolve({ filter: SOLID_SPECIFIER }, async ({ path, importer, resolveDir }) => {
124
+ const from = resolveDir.length > 0 ? resolveDir : dirname(importer);
125
+ const entry = await solidProductionEntry(path, from, importer);
126
+ return entry === null ? undefined : { path: entry };
127
+ });
128
+ },
129
+ };
@@ -0,0 +1,41 @@
1
+ // The stylesheet half of an island build: `import styles from './x.module.scss'` answers the class
2
+ // map the SERVER hashed, and the CSS lands in the same registry a document renders from. Bun's
3
+ // default loader answers the asset PATH — a string — so `styles['track']` is `undefined`, every
4
+ // element renders unclassed, and `Bun.build` reports `success: true` with no log.
5
+
6
+ import { renderThrowable, UltimateError } from '@ultimat3/core';
7
+ import { loadStylesheet } from '@ultimat3/render';
8
+ import type { BunPlugin } from 'bun';
9
+ import { IslandBuildFailedError } from './errors';
10
+
11
+ /**
12
+ * The same spelling `installRenderLoader` filters on, so there is ONE answer to "what is a
13
+ * stylesheet import" across the server loader and the island bundler. A plain `.css`/`.scss` is in
14
+ * deliberately: it compiles to an empty class map and registers its rules, which is what a global
15
+ * stylesheet an island imports has to do.
16
+ */
17
+ const STYLESHEET = /\.s?css$/;
18
+
19
+ /**
20
+ * `loadStylesheet`, not `compileStylesheet`: compiling alone answers the class names and drops the
21
+ * RULES on the floor, and an island is the one importer a document's own module graph never sees.
22
+ * Registering here is what puts them in `stylesFor(surface)` — `buildIslands` runs before the first
23
+ * document is rendered, in `x dev` and in `prerenderSite` alike.
24
+ */
25
+ export const islandStylesPlugin: BunPlugin = {
26
+ name: 'ultimate-island-styles',
27
+ setup(build): void {
28
+ build.onLoad({ filter: STYLESHEET }, async ({ path }) => {
29
+ const source = await Bun.file(path).text();
30
+ try {
31
+ return { contents: loadStylesheet(path, source), loader: 'js' };
32
+ } catch (error) {
33
+ // A coded failure already names the file and carries a fix — re-wrapping it would bury
34
+ // both. Anything else is rendered through `renderThrowable`, because this is the plugin's
35
+ // last frame and a value that fights being read would escape a Bun plugin as a bare throw.
36
+ if (error instanceof UltimateError) throw error;
37
+ throw new IslandBuildFailedError({ file: path, logs: renderThrowable(error) });
38
+ }
39
+ });
40
+ },
41
+ };
package/src/mcp-errors.ts CHANGED
@@ -48,6 +48,15 @@ const CLI_FIXES: Readonly<Record<CliErrorCode, string>> = {
48
48
  X_JOB_UNKNOWN: 'x jobs ls --json',
49
49
  X_FIX_TARGET_UNKNOWN: 'x fix boundary apps/web/site/page.tsx --json',
50
50
  X_ERROR_FIX_INVALID: 'x verify --json # the finding names the file, the line and the fix text',
51
+ X_WORKSPACE_DEP_UNDECLARED:
52
+ 'x verify --json # the package-shape finding carries the dependency line to add',
53
+ X_SHOT_BROWSER_MISSING: 'bun add -d puppeteer-core',
54
+ X_GH_UNAVAILABLE: 'gh auth login # install first from https://cli.github.com',
55
+ X_GH_NOT_AUTHENTICATED: 'gh auth login',
56
+ X_GH_COMMAND_FAILED: 'x ci --json # the finding carries the gh invocation that failed',
57
+ X_GH_RESPONSE_INVALID: 'x pr review --json # the finding names the field that did not parse',
58
+ X_PR_NOT_FOUND: 'x pr review --pr 1 --json # or open one first with: gh pr create',
59
+ X_CI_RUN_NOT_FOUND: 'x ci --branch main --json',
51
60
  X_ERROR_CODE_UNDOCUMENTED: 'x verify --json # the finding names the code and the missing page',
52
61
  X_ERROR_CODE_UNREGISTERED:
53
62
  'x errors list --json # register the code in its package src/errors.ts, or move its row under "Reserved codes"',
@@ -91,6 +100,8 @@ const CLI_FIXES: Readonly<Record<CliErrorCode, string>> = {
91
100
  X_RUNTIME_DRIVER_SPLIT: 'x dev --json # the boot names the driver the app installed twice',
92
101
  X_GENERATE_CONFLICT: 'x g route posts --force --json',
93
102
  X_PORT_IN_USE: 'x dev --port 3001 --json',
103
+ X_DEV_ALREADY_RUNNING:
104
+ 'x dev --json # after stopping the x dev that already owns this checkout',
94
105
  // Not `x db status`: there is no such subcommand (`x db` is gen, migrate, reset, studio, branch),
95
106
  // so the fix answered a failed step with X_CLI_UNKNOWN_COMMAND. `x doctor` is what reports
96
107
  // reachability and drift, and is already this table's answer for X_DB_STUDIO_FAILED.
package/src/messages.ts CHANGED
@@ -72,6 +72,9 @@ const CATALOG = {
72
72
  'cli.dev.mail.external': 'mail=external({driver} via {detail})',
73
73
  'cli.dev.mail.refused': 'mail=refused({detail})',
74
74
  'cli.dev.hmr': 'reloaded {file} in {ms}ms',
75
+ // A hard kill leaves the lock behind and that is normal, not a fault — worth one line so a
76
+ // reader knows why the boot paused, and never a finding.
77
+ 'cli.dev.staleLock': 'cleared a stale dev.lock — the previous x dev did not shut down cleanly',
75
78
  'cli.dev.roles': ' roles {roles}',
76
79
  'cli.dev.panels': ' panels {panels}',
77
80
  'cli.dev.introspect': ' introspect {url}',
@@ -156,7 +159,71 @@ const CATALOG = {
156
159
  'cli.routes.empty': 'no routes in the manifest — run `x manifest` first',
157
160
  'cli.tasks.count': '{count} task(s)',
158
161
  'cli.tasks.shown': '{name} — {cron} ({tz}), next {next}',
162
+ 'cli.affected.count':
163
+ '{count} workspace(s) affected by {base}...HEAD, from {changed} changed file(s)',
164
+ 'cli.affected.dirty':
165
+ ' including the working tree (--dirty): every uncommitted change in this checkout, whoever made it',
166
+ 'cli.affected.none': 'no workspace is affected by {base}...HEAD, from {changed} changed file(s)',
167
+ 'cli.affected.rootWide':
168
+ ' every workspace: {files} belongs to none of them and changes what all of them compile',
169
+ // `x shot` — the picture is the `lines`, the verdict is the artifact. The summary names the
170
+ // GATING fact, and a redirect comes first: a photograph of the sign-in page with every island
171
+ // missing reads as a bug in the app, and it is a bug in the capture.
172
+ 'cli.shot.ok': '{route} clean — {islands} island(s) mounted, nothing logged and nothing threw',
173
+ 'cli.shot.errors': '{route}: {errors} console error(s) — verdict.json names each one',
174
+ 'cli.shot.redirected': '{route} redirected to {url} — the picture is not the route asked for',
175
+ 'cli.shot.server.booted': ' server booted for this shot on {url}',
176
+ 'cli.shot.server.reused': ' server the x dev already running on {url}',
177
+ 'cli.shot.canvas': ' canvas {width}x{height}',
178
+ 'cli.shot.canvasUnreadable': ' canvas unreadable — {bytes} byte(s), not a decodable image',
179
+ 'cli.shot.islands': ' islands {booted} of {declared} mounted ({strategies})',
180
+ 'cli.shot.islandsUnknown': ' islands not counted — the page answered no probe',
181
+ 'cli.shot.network': ' network {requests} request(s), {refused} refused, {dropped} dropped',
182
+ 'cli.shot.console': ' console {level}: {text}',
183
+ 'cli.shot.threw': '{route}: {thrown} uncaught exception(s) — {first}',
184
+ 'cli.shot.pageError': ' threw {message} {at}',
185
+ 'cli.shot.picture': ' picture {path}',
186
+ 'cli.shot.verdict': ' verdict {path}',
187
+ 'cli.shot.blind.status':
188
+ 'HTTP response status is not observed — the port records requests, never responses',
189
+ 'cli.ci.failed':
190
+ '{failed} of {runs} workflow run(s) on {branch} failed — {findings} finding(s) recovered from the log',
191
+ 'cli.ci.green': 'every one of {runs} workflow run(s) on {branch} passed',
192
+ 'cli.ci.job': ' {conclusion} {job} ({steps})',
193
+ 'cli.ci.jobs.other': ' {count} other job(s) in this run',
194
+ 'cli.ci.logs.empty': ' the failed step wrote no log — {url}',
195
+ /** The conclusion of a run GitHub has not finished — a value, not a column key. */
196
+ 'cli.ci.pending': 'pending',
197
+ 'cli.ci.run': '{conclusion} {workflow} {url}',
198
+ 'cli.ci.running':
199
+ '{running} of {runs} workflow run(s) on {branch} has not finished — nothing has failed yet',
200
+ 'cli.ci.tail': ' log tail, {job}:',
201
+ 'cli.pr.body.truncated': ' … {hidden} more line(s) — re-run with --full',
202
+ /** The line of a thread whose anchor GitHub answers null for — a value, not a column key. */
203
+ 'cli.pr.line.unknown': '-',
204
+ 'cli.pr.replied': 'replied on thread {id}: {url}',
205
+ // Resolving closes a CONVERSATION. Whether the finding is fixed is a fact about the code that
206
+ // no GitHub mutation observes, and a summary saying "addressed" would assert one from the other.
207
+ 'cli.pr.resolved':
208
+ 'thread {id} is marked resolved on GitHub — that records the conversation, not that the finding is fixed',
209
+ 'cli.pr.review.count':
210
+ '{unresolved} unresolved and {resolved} resolved review thread(s) on {repo}#{pr}',
211
+ 'cli.pr.review.current': ' submitted against the current head {head}',
212
+ 'cli.pr.review.decision': ' review: {decision} by {author} at {submitted}',
213
+ 'cli.pr.review.none': 'no review thread is anchored to a line on {repo}#{pr}',
214
+ 'cli.pr.review.stale':
215
+ ' submitted against {commit}; the head is now {head} ({committed}) — this decision predates the current code',
216
+ 'cli.pr.review.truncated':
217
+ ' more than {count} threads — this is the first page, not the whole review',
218
+ 'cli.pr.review.undecided': ' GitHub reports no review decision yet',
219
+ 'cli.pr.thread.closed': ' resolved {path}:{line} {id}',
220
+ 'cli.pr.thread.comment': ' {author} at {createdAt}',
221
+ 'cli.pr.thread.more': ' {hidden} more comment(s) on this thread',
222
+ 'cli.pr.thread.open': ' unresolved {path}:{line} {id}',
223
+ 'cli.pr.thread.outdated':
224
+ ' the diff has moved under this thread — {line} is where the comment was written',
159
225
  'cli.test.fail': '{failed} of {workers} shard(s) failed',
226
+ 'cli.test.affected.none': 'nothing is affected by {base}...HEAD — 0 test file(s) ran',
160
227
  'cli.test.pass': '{files} test file(s) on {workers} worker(s) passed in {ms}ms',
161
228
  'cli.test.sampled': 'sampled {kept} of {total} {type} file(s)',
162
229
  'cli.test.type.fail': '{type} — {failed} of {workers} shard(s) failed',