catalyst-core 1.0.2-canary.1 → 1.0.2
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/bin/catalyst.js +105 -1
- package/changelog.md +34 -0
- package/dist/cli/README.md +119 -0
- package/dist/cli/appConfig.js +51 -0
- package/dist/cli/buildErrors.js +170 -0
- package/dist/cli/buildWarnings.js +28 -0
- package/dist/cli/devOutput.js +524 -0
- package/dist/cli/diagnostic.js +388 -0
- package/dist/cli/hints.js +69 -0
- package/dist/cli/package.json +3 -0
- package/dist/cli/spinner.js +141 -0
- package/dist/cli/theme.js +155 -0
- package/dist/errors/index.js +7 -3
- package/dist/logger.js +61 -6
- package/dist/native/androidProject/app/build.gradle.kts +2 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +2 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +9 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/SplashActivity.kt +65 -34
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/SplashOverlay.kt +109 -0
- package/dist/native/androidSetup.js +46 -57
- package/dist/native/buildAndroid/assets.js +22 -13
- package/dist/native/buildAndroid/build.js +98 -26
- package/dist/native/buildAndroid/config.js +43 -12
- package/dist/native/buildAndroid/index.js +49 -191
- package/dist/native/buildAppAndroid.js +19 -8
- package/dist/native/buildAppIos.js +35 -16
- package/dist/native/buildIos/assets.js +1 -6
- package/dist/native/buildIos/build.js +66 -95
- package/dist/native/buildIos/config.js +26 -8
- package/dist/native/buildIos/index.js +143 -64
- package/dist/native/buildIos/plugins.js +12 -15
- package/dist/native/internalPluginUtils.js +0 -1
- package/dist/native/iosnativeWebView/CoreLogic/Sources/CatalystCoreLogic/Constants/ConfigConstants.swift +2 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashHTMLView.swift +31 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashView.swift +17 -1
- package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashViewModel.swift +41 -1
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewModel.swift +2 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +3 -0
- package/dist/native/pluginComposerAndroid.js +2 -3
- package/dist/native/renameAndroidProject.js +42 -20
- package/dist/native/setupEmulatorIos.js +28 -49
- package/dist/native/setupServer.js +12 -15
- package/dist/native/terminalProgress.js +186 -147
- package/dist/native/utils.js +340 -54
- package/dist/otel.js +15 -9
- package/dist/scripts/build.js +313 -42
- package/dist/scripts/generateOfflineManifest.js +11 -1
- package/dist/scripts/loadEnvironmentVariables.js +24 -2
- package/dist/scripts/preServerInit.js +32 -10
- package/dist/scripts/preflight.js +35 -11
- package/dist/scripts/scriptUtils.js +34 -0
- package/dist/scripts/serve.js +64 -15
- package/dist/scripts/start.js +58 -21
- package/dist/server/expressServer.js +167 -32
- package/dist/server/renderer/handler.jsx +87 -11
- package/dist/server/utils/validator.js +14 -11
- package/dist/vite/manifest-categorization-plugin.js +7 -5
- package/dist/vite/resolveDevServerConfig.js +9 -2
- package/dist/vite/vite.config.client.js +18 -0
- package/dist/vite/vite.config.js +73 -13
- package/dist/vite/vite.config.server.js +21 -0
- package/dist/web-router/components/MetaTag.jsx +15 -2
- package/dist/web-router/components/MetaTag.test.jsx +71 -2
- package/dist/webmcp/WebMcpProvider.jsx +3 -2
- package/dist/webmcp/useTool.js +4 -1
- package/mcp_v2/conversion-tasks.json +6 -5
- package/mcp_v2/knowledge-base.json +124 -7
- package/mcp_v2/test/config.test.ts +59 -0
- package/mcp_v2/tools/build.js +6 -6
- package/mcp_v2/tools/config.js +35 -18
- package/mcp_v2/tools/conversion.js +7 -13
- package/package.json +5 -5
- package/test/cli/buildErrors.test.ts +173 -0
- package/test/cli/buildWarnings.test.cjs +62 -0
- package/test/cli/devOutput.test.ts +549 -0
- package/test/cli/diagnostic.test.ts +434 -0
- package/test/cli/output.test.cjs +90 -0
- package/test/cli/spinner.test.ts +177 -0
- package/test/cli/theme.test.ts +163 -0
- package/test/native/redactSecrets.test.ts +72 -0
- package/test/native/utils.test.ts +532 -0
- package/test/scripts/build.test.ts +275 -0
- package/test/scripts/serve.test.ts +102 -0
- package/test/server/fixtures/template/src/js/routes/index.jsx +8 -0
- package/test/server/handler.test.ts +17 -0
- package/test/server/logger.test.ts +185 -0
- package/test/server/otel.test.ts +556 -0
- package/test/server/validator.test.ts +19 -8
- package/dist/scripts/checkVersion.js +0 -3
- package/dist/scripts/cleanCache.js +0 -30
package/bin/catalyst.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { spawnSync } from "node:child_process"
|
|
4
|
+
import { createRequire } from "node:module"
|
|
4
5
|
import { dirname, resolve } from "node:path"
|
|
5
6
|
import { fileURLToPath } from "node:url"
|
|
6
7
|
|
|
@@ -19,6 +20,103 @@ const platformScripts = {
|
|
|
19
20
|
}
|
|
20
21
|
const validCommands = new Set([...webCommands, "buildApp", "setupEmulator", ...Object.keys(platformScripts)])
|
|
21
22
|
|
|
23
|
+
const require = createRequire(import.meta.url)
|
|
24
|
+
const pkg = require("../package.json")
|
|
25
|
+
|
|
26
|
+
// The house style when the built CLI layer is present; plain text otherwise,
|
|
27
|
+
// so help still works from a source checkout that has not been built.
|
|
28
|
+
let theme = null
|
|
29
|
+
try {
|
|
30
|
+
theme = require("../dist/cli/theme.js")
|
|
31
|
+
} catch {
|
|
32
|
+
// Unbuilt checkout.
|
|
33
|
+
}
|
|
34
|
+
const paint = (name, text) => (theme ? theme.t[name](text) : text)
|
|
35
|
+
const GUTTER = " "
|
|
36
|
+
|
|
37
|
+
const HELP = [
|
|
38
|
+
[
|
|
39
|
+
"Web",
|
|
40
|
+
[
|
|
41
|
+
["start", "Start the development server"],
|
|
42
|
+
["build", "Build the app for production"],
|
|
43
|
+
["serve", "Serve the production build"],
|
|
44
|
+
],
|
|
45
|
+
],
|
|
46
|
+
[
|
|
47
|
+
"Native",
|
|
48
|
+
[
|
|
49
|
+
["setupEmulator:android", "Configure and boot the Android emulator"],
|
|
50
|
+
["setupEmulator:ios", "Configure and boot the iOS simulator"],
|
|
51
|
+
["buildApp:android", "Build and install the Android app"],
|
|
52
|
+
["buildApp:ios", "Build and install the iOS app"],
|
|
53
|
+
],
|
|
54
|
+
],
|
|
55
|
+
[
|
|
56
|
+
"More",
|
|
57
|
+
[
|
|
58
|
+
["serve:inspect", "Serve with the Node inspector attached"],
|
|
59
|
+
["plugins", "Turn native plugins on or off"],
|
|
60
|
+
],
|
|
61
|
+
],
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
function printHelp() {
|
|
65
|
+
const width = 24
|
|
66
|
+
const lines = [""]
|
|
67
|
+
lines.push(`${GUTTER}${paint("bold", paint("accent", "catalyst"))}${paint("dim", ` v${pkg.version}`)}`)
|
|
68
|
+
lines.push(`${GUTTER}${paint("dim", "─".repeat(46))}`)
|
|
69
|
+
lines.push(`${GUTTER}${paint("dim", "Usage")} catalyst <command> [options]`)
|
|
70
|
+
for (const [group, commands] of HELP) {
|
|
71
|
+
lines.push("", `${GUTTER}${paint("bold", group)}`)
|
|
72
|
+
for (const [name, text] of commands)
|
|
73
|
+
lines.push(`${GUTTER} ${paint("accent", name.padEnd(width))}${paint("dim", text)}`)
|
|
74
|
+
}
|
|
75
|
+
lines.push("", `${GUTTER}${paint("bold", "Options")}`)
|
|
76
|
+
lines.push(
|
|
77
|
+
`${GUTTER} ${"--verbose".padEnd(width)}${paint("dim", "Show every step and the full tool output")}`
|
|
78
|
+
)
|
|
79
|
+
lines.push(
|
|
80
|
+
`${GUTTER} ${"--debug".padEnd(width)}${paint("dim", "Also print stacks and the environment")}`
|
|
81
|
+
)
|
|
82
|
+
lines.push(`${GUTTER} ${"-h, --help".padEnd(width)}${paint("dim", "Show this help")}`)
|
|
83
|
+
lines.push(`${GUTTER} ${"-v, --version".padEnd(width)}${paint("dim", "Print the version")}`)
|
|
84
|
+
console.log(`${lines.join("\n")}\n`)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Edit distance, for suggesting the command someone meant. */
|
|
88
|
+
function distance(a, b) {
|
|
89
|
+
const row = Array.from({ length: b.length + 1 }, (_, i) => i)
|
|
90
|
+
for (let i = 1; i <= a.length; i++) {
|
|
91
|
+
let previous = row[0]
|
|
92
|
+
row[0] = i
|
|
93
|
+
for (let j = 1; j <= b.length; j++) {
|
|
94
|
+
const current = row[j]
|
|
95
|
+
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, previous + (a[i - 1] === b[j - 1] ? 0 : 1))
|
|
96
|
+
previous = current
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return row[b.length]
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function closestCommand(input) {
|
|
103
|
+
let best = null
|
|
104
|
+
for (const command of validCommands) {
|
|
105
|
+
const d = distance(input.toLowerCase(), command.toLowerCase())
|
|
106
|
+
if (d <= Math.max(2, Math.floor(command.length / 3)) && (!best || d < best.d)) best = { command, d }
|
|
107
|
+
}
|
|
108
|
+
return best?.command ?? null
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (args.length === 0 || args[0] === "--help" || args[0] === "-h" || args[0] === "help") {
|
|
112
|
+
printHelp()
|
|
113
|
+
process.exit(0)
|
|
114
|
+
}
|
|
115
|
+
if (args[0] === "--version" || args[0] === "-v") {
|
|
116
|
+
console.log(pkg.version)
|
|
117
|
+
process.exit(0)
|
|
118
|
+
}
|
|
119
|
+
|
|
22
120
|
const scriptIndex = args.findIndex((arg) => validCommands.has(arg))
|
|
23
121
|
const script = scriptIndex === -1 ? args[0] : args[scriptIndex]
|
|
24
122
|
const nodeArgs = scriptIndex > 0 ? args.slice(0, scriptIndex) : []
|
|
@@ -68,5 +166,11 @@ if (webCommands.has(script)) {
|
|
|
68
166
|
handleProcessResult(runScript(`../dist/scripts/${scriptName}.js`, extraArgs))
|
|
69
167
|
}
|
|
70
168
|
|
|
71
|
-
|
|
169
|
+
const suggestion = closestCommand(script)
|
|
170
|
+
console.error(
|
|
171
|
+
`\n${GUTTER}${paint("bad", paint("bold", "error"))}\n${GUTTER}${paint("bold", `Unknown command "${script}"`)}\n\n` +
|
|
172
|
+
`${GUTTER}${paint("accent", "hint")} ${
|
|
173
|
+
suggestion ? `Did you mean ${paint("accent", `catalyst ${suggestion}`)}? ` : ""
|
|
174
|
+
}Run ${paint("accent", "catalyst --help")} for the list.\n`
|
|
175
|
+
)
|
|
72
176
|
process.exit(1)
|
package/changelog.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.0.2] - unreleased
|
|
4
|
+
|
|
5
|
+
This release adds two opt-in features, an animated HTML splash screen for universal apps and experimental WebMCP support, and gives every CLI command the same terminal output. There are no breaking API changes.
|
|
6
|
+
|
|
7
|
+
### HTML splash screen
|
|
8
|
+
|
|
9
|
+
A universal app can ship `public/splashscreen.html`. When `WEBVIEW_CONFIG.splashScreen` is set, the native splash image hands over to that page, which stays up until the app's first page has loaded. The new optional `splashScreen.minDuration` keeps it on screen for at least that many milliseconds, and it is dismissed after 10 seconds regardless. The file must be self-contained, and on Android it cannot make network requests. Without the file, the splash works as before.
|
|
10
|
+
|
|
11
|
+
### WebMCP (experimental)
|
|
12
|
+
|
|
13
|
+
The new `catalyst-core/webmcp` subpath lets an app expose its actions to AI agents running in the browser. `useTool()` registers a tool from a component, a `tool` field on a route derives one from the route, and `WebMcpProvider` adds the built-in `navigate`, `get_current_route` and `get_page_info` tools. `catalyst-core/webmcp/shim` installs an opt-in `document.modelContext` shim. The API may change in a minor release.
|
|
14
|
+
|
|
15
|
+
### New root export
|
|
16
|
+
|
|
17
|
+
The root entry of `catalyst-core` adds `useRouterState`, which returns `{ location, params, navigate, matchedRoutes }` through Catalyst's router context. `catalyst-core/webmcp` uses it to read router state without importing `react-router` from a subpath. The root entry now exports nine names; nothing was removed.
|
|
18
|
+
|
|
19
|
+
### CLI output
|
|
20
|
+
|
|
21
|
+
`catalyst start`, `build`, `serve`, the native build and setup commands, and `create-catalyst-app` now print a short header, one line per step with its timing, and one diagnostic block on failure with the error code, location, hint and docs link. The full bundler and native build output moved behind `--verbose`, and `--debug` adds stacks and the environment. `catalyst --help`, `--version` and a "did you mean" suggestion for mistyped commands are new. Once `catalyst serve` has started the production server, the app's own output passes through unchanged.
|
|
22
|
+
|
|
23
|
+
### Fixes
|
|
24
|
+
|
|
25
|
+
- `catalyst serve` refuses to serve a build whose inlined values (`PUBLIC_STATIC_ASSET_URL`, `PUBLIC_STATIC_ASSET_PATH`, `CLIENT_ENV_VARIABLES`) differ from the current `config/config.json`, with `PREFLIGHT-022`. It also reports a missing build or a taken port before starting.
|
|
26
|
+
- Fast Refresh now works for components written as JSX in `.js` files instead of reloading the page.
|
|
27
|
+
- `MetaTag` no longer depends on `react-helmet-async`, which is removed from the dependencies. Route metadata from `setMetaData` renders through React 19's `<head>` support, and server-rendered head tags no longer outlive their page after client navigation.
|
|
28
|
+
- `catalyst-core` no longer imports `prop-types` at runtime without declaring it.
|
|
29
|
+
- `Otel.init()` no longer throws `ReferenceError: logger is not defined` when the app has not called `configureLogger()`. It logs to `console` until a global logger is set.
|
|
30
|
+
- Signing passwords are redacted from the output of a failed signed build or `keytool` run.
|
|
31
|
+
- File logs written by `configureLogger()` keep structured fields for plain objects again.
|
|
32
|
+
|
|
33
|
+
### create-catalyst-app
|
|
34
|
+
|
|
35
|
+
New apps declare `"type": "module"`, and the Tailwind template's config is `postcss.config.cjs`. `catalyst-core` loads either `postcss.config.cjs` or `postcss.config.js`, so existing apps need no change. `create-catalyst-app` declares `node >= 20.12.0` and runs across that range.
|
|
36
|
+
|
|
3
37
|
## [1.0.0] - 2026-09-29
|
|
4
38
|
|
|
5
39
|
This release freezes the public API. The root entry of `catalyst-core` now exports exactly eight names and nothing else: `RouterDataProvider`, `useCurrentRouteData`, `useRouterData`, `MetaTag`, `split`, `hydrationReady`, `Head`, and `Body`. Anything previously reachable through the root entry but not in that list is no longer public. Applications that import a removed name will fail at build time rather than silently resolving to `undefined`.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# CLI output
|
|
2
|
+
|
|
3
|
+
Every Catalyst command prints through this layer, so the whole CLI reads as one
|
|
4
|
+
program rather than a dozen scripts that happen to ship together.
|
|
5
|
+
|
|
6
|
+
## The rule
|
|
7
|
+
|
|
8
|
+
Every command opens with `header("catalyst <command>", subject)` and prints
|
|
9
|
+
nothing at column 0 — every line starts with the two-space gutter, which means
|
|
10
|
+
child processes are always **piped, never inherited**. Progress is append-only:
|
|
11
|
+
each step is one `✓`/`✗` line, the only moving thing is the one-line spinner,
|
|
12
|
+
and anything printed while it spins goes through `progress.log` (it becomes a
|
|
13
|
+
dim `│` sub-line — only if it says something the `✓` line will not). Blank lines
|
|
14
|
+
separate _kinds_, not lines: steps stick together; a titled block (`Output`,
|
|
15
|
+
`Config`, `Notices`), the summary, and a diagnostic each get one blank line
|
|
16
|
+
before them. End with `✓ <result> in <time>` plus a dim line naming the next
|
|
17
|
+
command in cyan. Cyan marks the one thing to act on, green/red/yellow mark
|
|
18
|
+
state, dim marks structure, and the only glyphs are `✓ ✗ ▲ • › ├ └ │ ─`. Fail
|
|
19
|
+
through `diagnostic()`/`buildFailure()` on stderr with a non-zero exit, never a
|
|
20
|
+
raw stack.
|
|
21
|
+
|
|
22
|
+
## Why piped, never inherited
|
|
23
|
+
|
|
24
|
+
With `stdio: "inherit"` a child writes straight to the terminal at column 0.
|
|
25
|
+
The parent cannot indent it, cannot suppress it, cannot deduplicate it — and,
|
|
26
|
+
worst of all, never sees the text, so it cannot find the compiler error inside
|
|
27
|
+
it. Two parallel children with inherited stdio also interleave
|
|
28
|
+
non-deterministically.
|
|
29
|
+
|
|
30
|
+
Piping is what makes `extractBuildErrors` and `codeFrame` reachable. It is the
|
|
31
|
+
difference between:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
error Command failed with exit code 1
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
and:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
error gradle build
|
|
41
|
+
Unresolved reference: viewBinding
|
|
42
|
+
at MainActivity.kt:9:25
|
|
43
|
+
|
|
44
|
+
8 │ super.onCreate(savedInstanceState)
|
|
45
|
+
> 9 │ setContentView(viewBinding.root)
|
|
46
|
+
│ ^
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
When you pipe, forward `FORCE_COLOR` if the parent is a TTY — otherwise the
|
|
50
|
+
child sees a pipe and strips its own colour.
|
|
51
|
+
|
|
52
|
+
## The modules
|
|
53
|
+
|
|
54
|
+
| File | What it owns |
|
|
55
|
+
| ---------------- | ---------------------------------------------------------------------------------------- |
|
|
56
|
+
| `theme.js` | Colours, glyphs, gutter, `header()`, `step()`, `row()`, `displayPath()`, `isVerbose()` |
|
|
57
|
+
| `spinner.js` | The single live line, plus `withSpinnersPaused()` for anything that must write or ask |
|
|
58
|
+
| `diagnostic.js` | `diagnostic()` for one failure, `buildFailure()`, `runtimeFailure()`, `verboseOutput()` |
|
|
59
|
+
| `buildErrors.js` | Mines real errors out of gradle/xcodebuild/Vite output, incl. gradle's "What went wrong" |
|
|
60
|
+
| `hints.js` | The short list of failures we can give real advice for |
|
|
61
|
+
| `devOutput.js` | Classifies a dev/production server's lines; `superviseServer()`, `ensurePortFree()` |
|
|
62
|
+
|
|
63
|
+
## Default and --verbose
|
|
64
|
+
|
|
65
|
+
The default view keeps what someone acts on: steps, results, warnings, errors
|
|
66
|
+
and the one next command. Everything else -- a native build's narration
|
|
67
|
+
("Package.swift already up to date"), Vite's dependency bookkeeping, Node's
|
|
68
|
+
typeless-package warning -- is still produced, and `--verbose` prints it.
|
|
69
|
+
|
|
70
|
+
In the native builds that split is the `progress.log` level. `info` and
|
|
71
|
+
`success` are narration: they replace the spinner's detail text and leave no
|
|
72
|
+
line behind. `warning`, `error` and `note` always print; use `note` for the rare
|
|
73
|
+
line a person must act on or keep (a manual launch step, where a keystore was
|
|
74
|
+
written). On failure, `--verbose` adds the tool's full output, and `--debug`
|
|
75
|
+
adds stacks and the environment.
|
|
76
|
+
|
|
77
|
+
## Runtime errors
|
|
78
|
+
|
|
79
|
+
An error while the server is up -- a component that throws, a fetcher that
|
|
80
|
+
rejects -- is reported once per request through `runtimeFailure()`: the
|
|
81
|
+
registry code and route as the label, the first frame in the app's own code as
|
|
82
|
+
the location with its source, and no library frames unless `--verbose`. The
|
|
83
|
+
SSR handler is bundled into the app, so it cannot import this layer; the server
|
|
84
|
+
exposes the reporter as `globalThis.__catalystReportRuntimeError`. A repeat of
|
|
85
|
+
the same error prints one `✗ Same error again` line.
|
|
86
|
+
|
|
87
|
+
## Adding a hint
|
|
88
|
+
|
|
89
|
+
`hints.js` is deliberately short. A hint that guesses is worse than no hint —
|
|
90
|
+
it sends people down the wrong path with confidence. Add one only when the
|
|
91
|
+
pattern reliably implies the cause, and say what to **do**, not what went wrong;
|
|
92
|
+
the message above it already said that.
|
|
93
|
+
|
|
94
|
+
## Error codes
|
|
95
|
+
|
|
96
|
+
Codes come from the error registry, not from here. This layer decides only how
|
|
97
|
+
one looks: dim, beside the `error` label, ahead of the scope.
|
|
98
|
+
|
|
99
|
+
Advice has a precedence. A code we own carries an authored `suggestedAction`,
|
|
100
|
+
and that always beats a pattern guess from `hints.js`. The per-stage wrapper
|
|
101
|
+
codes (`BUNDLE-000`, `IOS-000`, `ANDROID-000`) are the exception: their action
|
|
102
|
+
only says to read the upstream error, which is the text `extractBuildErrors`
|
|
103
|
+
just recovered, so the mined hint wins instead.
|
|
104
|
+
|
|
105
|
+
That mining stays load-bearing. A wrapped toolchain error preserves the child
|
|
106
|
+
process object, whose message is `Command failed with exit code 1` -- the real
|
|
107
|
+
compiler error lives in the captured transcript, and nothing else recovers it.
|
|
108
|
+
So never add a `hints.js` entry for a failure that has an owned code; that
|
|
109
|
+
advice belongs in the registry.
|
|
110
|
+
|
|
111
|
+
## Prompts
|
|
112
|
+
|
|
113
|
+
A prompt and a progress sequence are opposite contracts: progress is output you
|
|
114
|
+
watch, a prompt is a region you drive. They must never overlap. Stop the spinner
|
|
115
|
+
before asking, and collapse the answered prompt to a single settled line
|
|
116
|
+
(`✓ Simulator iPhone 15 Pro`) so the transcript reads as if it had always been
|
|
117
|
+
a step. In a non-TTY, never prompt — print a diagnostic naming the config key
|
|
118
|
+
and exit 1, because a prompt that waits forever in CI is the worst possible
|
|
119
|
+
output.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict"
|
|
2
|
+
|
|
3
|
+
const fs = require("fs")
|
|
4
|
+
const path = require("path")
|
|
5
|
+
|
|
6
|
+
const { diagnostic } = require("./diagnostic.js")
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Read the app's config/config.json with an actionable error.
|
|
10
|
+
*
|
|
11
|
+
* The native entry points used to `require()` this at module load, so a missing
|
|
12
|
+
* or malformed config threw a raw MODULE_NOT_FOUND / SyntaxError stack before
|
|
13
|
+
* any of the build output appeared.
|
|
14
|
+
*
|
|
15
|
+
* Lives here because this layer is CJS and is consumed from both sides: the
|
|
16
|
+
* native builds require() it directly, the ESM scripts via createRequire.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} [cwd] - app root; defaults to the current working directory
|
|
19
|
+
*/
|
|
20
|
+
function loadAppConfig(cwd = process.cwd()) {
|
|
21
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - cwd is the local project directory; the file name is a fixed literal.
|
|
22
|
+
const configPath = path.join(cwd, "config", "config.json")
|
|
23
|
+
|
|
24
|
+
let raw
|
|
25
|
+
try {
|
|
26
|
+
raw = fs.readFileSync(configPath, "utf8")
|
|
27
|
+
} catch (error) {
|
|
28
|
+
process.stderr.write(
|
|
29
|
+
diagnostic({
|
|
30
|
+
message:
|
|
31
|
+
error.code === "ENOENT"
|
|
32
|
+
? "Config file not found"
|
|
33
|
+
: `Could not read config file: ${error.message}`,
|
|
34
|
+
file: configPath,
|
|
35
|
+
hint: "Every Catalyst app needs a config/config.json at its root.",
|
|
36
|
+
})
|
|
37
|
+
)
|
|
38
|
+
process.exit(1)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(raw)
|
|
43
|
+
} catch (error) {
|
|
44
|
+
process.stderr.write(
|
|
45
|
+
diagnostic({ message: `Config file is not valid JSON: ${error.message}`, file: configPath })
|
|
46
|
+
)
|
|
47
|
+
process.exit(1)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { loadAppConfig }
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"use strict"
|
|
2
|
+
|
|
3
|
+
const { ANSI } = require("./theme.js")
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Pull the real errors out of a native build's output.
|
|
7
|
+
*
|
|
8
|
+
* A failed gradle or xcodebuild run ends with "Command failed with exit code 1",
|
|
9
|
+
* which tells you nothing -- the Kotlin or Swift error that actually caused it
|
|
10
|
+
* is somewhere in the middle of hundreds of lines. Without this you have to
|
|
11
|
+
* open Android Studio or Xcode to find out what broke.
|
|
12
|
+
*
|
|
13
|
+
* Deliberately pattern-matching rather than parsing: both tools change their
|
|
14
|
+
* output between releases, and a parser that breaks silently is worse than a
|
|
15
|
+
* matcher that occasionally misses a line. Everything is still on screen and in
|
|
16
|
+
* the tail we print, so a miss costs nothing.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// e/ and error: are Kotlin; the file:line:col form covers javac, swiftc and
|
|
20
|
+
// clang. `> Task :app:x FAILED` and `FAILURE:` are gradle's own markers.
|
|
21
|
+
const ERROR_PATTERNS = [
|
|
22
|
+
// Kotlin: e: file:///path/Foo.kt:12:5 Unresolved reference: bar
|
|
23
|
+
/^e:\s+(?:file:\/\/)?(?<file>[^\s:]+):(?<line>\d+):(?<column>\d+)\s+(?<message>.+)$/,
|
|
24
|
+
// Swift / clang / javac: /path/Foo.swift:12:5: error: cannot find 'bar'
|
|
25
|
+
/^(?<file>[^\s:][^:]*):(?<line>\d+):(?<column>\d+):\s*(?:fatal\s+)?error:\s*(?<message>.+)$/i,
|
|
26
|
+
// javac without a column
|
|
27
|
+
/^(?<file>[^\s:][^:]*):(?<line>\d+):\s*error:\s*(?<message>.+)$/i,
|
|
28
|
+
// Gradle/AGP: "> Task :app:compileDebugKotlin FAILED" then a bare message
|
|
29
|
+
/^\s*(?:>\s*)?(?:error|FAILURE):\s*(?<message>.+)$/i,
|
|
30
|
+
// Xcode linker and codesign failures carry no file
|
|
31
|
+
/^(?<message>(?:ld|clang|codesign|xcodebuild): error:.+)$/i,
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
// Lines that look like errors but are summary noise, not the cause.
|
|
35
|
+
const NOISE = [
|
|
36
|
+
/^FAILURE: Build failed with an exception/i,
|
|
37
|
+
/^\*\s*(What went wrong|Try|Exception is|Get more help)/i,
|
|
38
|
+
/^Execution failed for task/i,
|
|
39
|
+
/error: Compilation failed; see the compiler error output/i,
|
|
40
|
+
/^\s*>\s*Compilation error\. See log for more details/i,
|
|
41
|
+
/^The following build commands failed:/i,
|
|
42
|
+
/^\(\d+ failures?\)$/i,
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
// Vite/rollup errors. Kept separate from the native patterns: a Vite build
|
|
46
|
+
// can never emit Kotlin or Xcode markers, and one flat list would become a
|
|
47
|
+
// junk drawer where every toolchain's noise matches every other's.
|
|
48
|
+
const VITE_ERROR_PATTERNS = [
|
|
49
|
+
// [plugin:name] Could not load /path (imported by other/path): ENOENT...
|
|
50
|
+
/\[(?:vite|plugin)[^\]]*\]\s*(?<message>Could not (?:load|resolve)[^:]*)(?::.*)?$/i,
|
|
51
|
+
// Failed to resolve import "x" from "y"
|
|
52
|
+
/^(?<message>Failed to resolve (?:import|entry)[^.]*)\.?$/i,
|
|
53
|
+
// file.js:12:3: error: message (esbuild transform failures)
|
|
54
|
+
/^(?<file>[^\s:][^:]*):(?<line>\d+):(?<column>\d+):\s*ERROR:\s*(?<message>.+)$/i,
|
|
55
|
+
// Bare "error during build:" is a header, not a message -- see NOISE.
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
// "imported by <file>" tells us which of the user's files to point at, which
|
|
59
|
+
// is far more useful than the missing path they typed.
|
|
60
|
+
const IMPORTED_BY = /\(imported by ([^)]+)\)/
|
|
61
|
+
|
|
62
|
+
const MAX_ERRORS = 5
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {string} output - everything the build printed
|
|
66
|
+
* @returns {Array<{file?: string, line?: number, column?: number, message: string}>}
|
|
67
|
+
*/
|
|
68
|
+
function extractBuildErrors(output, { toolchain = "native" } = {}) {
|
|
69
|
+
if (!output) return []
|
|
70
|
+
|
|
71
|
+
const patterns = toolchain === "vite" ? VITE_ERROR_PATTERNS : ERROR_PATTERNS
|
|
72
|
+
|
|
73
|
+
const found = []
|
|
74
|
+
const seen = new Set()
|
|
75
|
+
|
|
76
|
+
for (const raw of String(output).split("\n")) {
|
|
77
|
+
const line = raw.replace(ANSI, "").trimEnd()
|
|
78
|
+
if (!line.trim()) continue
|
|
79
|
+
if (NOISE.some((pattern) => pattern.test(line.trim()))) continue
|
|
80
|
+
|
|
81
|
+
for (const pattern of patterns) {
|
|
82
|
+
const match = pattern.exec(line.trim())
|
|
83
|
+
if (!match) continue
|
|
84
|
+
|
|
85
|
+
const { file, line: lineNo, column, message } = match.groups
|
|
86
|
+
const importedBy = IMPORTED_BY.exec(line)
|
|
87
|
+
const key = `${file || ""}:${lineNo || ""}:${message}`
|
|
88
|
+
if (seen.has(key)) break
|
|
89
|
+
seen.add(key)
|
|
90
|
+
|
|
91
|
+
found.push({
|
|
92
|
+
file: file || (importedBy ? importedBy[1].trim() : undefined),
|
|
93
|
+
line: lineNo ? Number(lineNo) : undefined,
|
|
94
|
+
column: column ? Number(column) : undefined,
|
|
95
|
+
// The "(imported by x)" clause becomes the `file` above, so
|
|
96
|
+
// repeating it in the message is noise.
|
|
97
|
+
message: message.replace(IMPORTED_BY, "").replace(/\s+/g, " ").trim(),
|
|
98
|
+
})
|
|
99
|
+
break
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (found.length >= MAX_ERRORS) break
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (found.length === 0 && toolchain !== "vite") {
|
|
106
|
+
const wrong = gradleWhatWentWrong(output)
|
|
107
|
+
if (wrong) found.push(wrong)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return found
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Gradle's own explanation, for failures that are not compiler errors (a
|
|
115
|
+
* missing SDK, a dependency that will not resolve, a bad plugin version):
|
|
116
|
+
*
|
|
117
|
+
* * What went wrong:
|
|
118
|
+
* Could not determine the dependencies of task ':app:compileDebugJavaWithJavac'.
|
|
119
|
+
* > SDK location not found. Define a valid SDK location with ...
|
|
120
|
+
*
|
|
121
|
+
* The innermost "> " line is the cause; the line above it is context.
|
|
122
|
+
*/
|
|
123
|
+
function gradleWhatWentWrong(output) {
|
|
124
|
+
const lines = String(output).replace(ANSI, "").split("\n")
|
|
125
|
+
const start = lines.findIndex((line) => /^\*\s*What went wrong:/.test(line.trim()))
|
|
126
|
+
if (start === -1) return null
|
|
127
|
+
|
|
128
|
+
const block = []
|
|
129
|
+
for (const line of lines.slice(start + 1)) {
|
|
130
|
+
const trimmed = line.trim()
|
|
131
|
+
if (!trimmed || /^\*\s/.test(trimmed)) break
|
|
132
|
+
block.push(trimmed)
|
|
133
|
+
}
|
|
134
|
+
if (block.length === 0) return null
|
|
135
|
+
|
|
136
|
+
const causes = block.filter((line) => line.startsWith(">")).map((line) => line.replace(/^>+\s*/, ""))
|
|
137
|
+
const message = (causes.length ? causes[causes.length - 1] : block[0])
|
|
138
|
+
// Gradle's sentences run long; the first one carries the meaning.
|
|
139
|
+
.replace(/^(.+?\.)\s+[A-Z].*$/, "$1")
|
|
140
|
+
return { message }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The tail of a build's output, for when no error could be recognised.
|
|
145
|
+
* Better to show the last thing the tool said than to say nothing.
|
|
146
|
+
*/
|
|
147
|
+
// Advice every gradle failure prints regardless of cause. It crowds out the
|
|
148
|
+
// real message, which is worse than useless here: the tail exists precisely
|
|
149
|
+
// because nothing was recognised, so every line it spends must be evidence.
|
|
150
|
+
const TAIL_NOISE = [
|
|
151
|
+
/^>?\s*Run with --(stacktrace|info|debug|scan)/i,
|
|
152
|
+
/^>?\s*Get more help at/i,
|
|
153
|
+
/^Deprecated Gradle features were used/i,
|
|
154
|
+
/^You can use '--warning-mode all'/i,
|
|
155
|
+
/^For more on this, please refer to/i,
|
|
156
|
+
/^BUILD FAILED in /i,
|
|
157
|
+
/^\*\s*(Try|Get more help)/i,
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
function outputTail(output, lines = 12) {
|
|
161
|
+
if (!output) return ""
|
|
162
|
+
return String(output)
|
|
163
|
+
.replace(ANSI, "")
|
|
164
|
+
.split("\n")
|
|
165
|
+
.filter((line) => line.trim() && !TAIL_NOISE.some((pattern) => pattern.test(line.trim())))
|
|
166
|
+
.slice(-lines)
|
|
167
|
+
.join("\n")
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = { extractBuildErrors, outputTail, MAX_ERRORS }
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict"
|
|
2
|
+
|
|
3
|
+
const { ANSI } = require("./theme.js")
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Warnings worth showing from a bundle that otherwise succeeded.
|
|
7
|
+
*
|
|
8
|
+
* The vite configs already drop the advisories nobody can act on; anything
|
|
9
|
+
* still here is a real signal (circular dependency, oversized chunk) that was
|
|
10
|
+
* previously captured and discarded.
|
|
11
|
+
*/
|
|
12
|
+
function bundleWarnings(output, label) {
|
|
13
|
+
if (!output) return []
|
|
14
|
+
const seen = new Set()
|
|
15
|
+
const found = []
|
|
16
|
+
for (const raw of String(output).split("\n")) {
|
|
17
|
+
const line = raw.replace(ANSI, "").trim()
|
|
18
|
+
if (!line) continue
|
|
19
|
+
if (!/^(warning|\(!\))/i.test(line) && !/^\[plugin/.test(line)) continue
|
|
20
|
+
const message = line.replace(/^\(!\)\s*/, "").replace(/^warning:?\s*/i, "")
|
|
21
|
+
if (!message || seen.has(message)) continue
|
|
22
|
+
seen.add(message)
|
|
23
|
+
found.push(`${label} ${message}`)
|
|
24
|
+
}
|
|
25
|
+
return found
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = { bundleWarnings }
|