catalyst-core 1.0.1 → 1.0.2-canary.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.
Files changed (114) hide show
  1. package/bin/catalyst.js +105 -1
  2. package/dist/cli/README.md +119 -0
  3. package/dist/cli/appConfig.js +50 -0
  4. package/dist/cli/buildErrors.js +170 -0
  5. package/dist/cli/buildWarnings.js +28 -0
  6. package/dist/cli/devOutput.js +524 -0
  7. package/dist/cli/diagnostic.js +388 -0
  8. package/dist/cli/hints.js +69 -0
  9. package/dist/cli/package.json +3 -0
  10. package/dist/cli/spinner.js +141 -0
  11. package/dist/cli/theme.js +155 -0
  12. package/dist/errors/index.js +7 -3
  13. package/dist/errors/registry.js +8 -0
  14. package/dist/errors-index.json +7 -0
  15. package/dist/index.d.ts +1 -1
  16. package/dist/index.jsx +1 -1
  17. package/dist/logger.js +61 -6
  18. package/dist/native/androidProject/app/build.gradle.kts +2 -1
  19. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +2 -0
  20. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +9 -0
  21. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/SplashActivity.kt +65 -34
  22. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/SplashOverlay.kt +109 -0
  23. package/dist/native/androidSetup.js +46 -57
  24. package/dist/native/buildAndroid/assets.js +22 -13
  25. package/dist/native/buildAndroid/build.js +98 -26
  26. package/dist/native/buildAndroid/config.js +43 -12
  27. package/dist/native/buildAndroid/index.js +49 -191
  28. package/dist/native/buildAppAndroid.js +19 -8
  29. package/dist/native/buildAppIos.js +35 -16
  30. package/dist/native/buildIos/assets.js +1 -6
  31. package/dist/native/buildIos/build.js +61 -94
  32. package/dist/native/buildIos/config.js +26 -8
  33. package/dist/native/buildIos/index.js +143 -64
  34. package/dist/native/buildIos/plugins.js +12 -15
  35. package/dist/native/internalPluginUtils.js +0 -1
  36. package/dist/native/iosnativeWebView/CoreLogic/Sources/CatalystCoreLogic/Constants/ConfigConstants.swift +2 -0
  37. package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashHTMLView.swift +31 -0
  38. package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashView.swift +17 -1
  39. package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashViewModel.swift +41 -1
  40. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewModel.swift +2 -0
  41. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +3 -0
  42. package/dist/native/pluginComposerAndroid.js +2 -3
  43. package/dist/native/renameAndroidProject.js +42 -20
  44. package/dist/native/setupEmulatorIos.js +28 -49
  45. package/dist/native/setupServer.js +12 -15
  46. package/dist/native/terminalProgress.js +186 -147
  47. package/dist/native/utils.js +335 -54
  48. package/dist/otel.js +15 -9
  49. package/dist/scripts/build.js +314 -42
  50. package/dist/scripts/generateOfflineManifest.js +11 -1
  51. package/dist/scripts/loadEnvironmentVariables.js +24 -2
  52. package/dist/scripts/preServerInit.js +32 -10
  53. package/dist/scripts/preflight.js +115 -11
  54. package/dist/scripts/scriptUtils.js +34 -0
  55. package/dist/scripts/serve.js +65 -9
  56. package/dist/scripts/start.js +58 -21
  57. package/dist/server/expressServer.js +167 -32
  58. package/dist/server/renderer/document/Body.d.ts +0 -11
  59. package/dist/server/renderer/document/Body.jsx +0 -10
  60. package/dist/server/renderer/document/Head.d.ts +0 -13
  61. package/dist/server/renderer/document/Head.jsx +0 -12
  62. package/dist/server/renderer/handler.jsx +87 -11
  63. package/dist/server/utils/validator.js +14 -11
  64. package/dist/vite/manifest-categorization-plugin.js +7 -5
  65. package/dist/vite/resolveDevServerConfig.js +9 -2
  66. package/dist/vite/vite.config.client.js +19 -1
  67. package/dist/vite/vite.config.js +96 -19
  68. package/dist/vite/vite.config.server.js +21 -0
  69. package/dist/web-router/components/MetaTag.jsx +20 -14
  70. package/dist/web-router/components/MetaTag.test.jsx +108 -32
  71. package/dist/web-router/components/RouterDataProvider.d.ts +25 -1
  72. package/dist/web-router/components/RouterDataProvider.jsx +35 -1
  73. package/dist/web-router/components/RouterDataProvider.test.jsx +44 -0
  74. package/dist/web-router/context.d.ts +15 -1
  75. package/dist/web-router/context.jsx +11 -0
  76. package/dist/webmcp/WebMcpProvider.jsx +234 -0
  77. package/dist/webmcp/declarative.js +207 -0
  78. package/dist/webmcp/declarative.test.js +122 -0
  79. package/dist/webmcp/errors.js +59 -0
  80. package/dist/webmcp/errors.test.js +47 -0
  81. package/dist/webmcp/index.js +40 -0
  82. package/dist/webmcp/native.js +37 -0
  83. package/dist/webmcp/native.test.js +41 -0
  84. package/dist/webmcp/pageInfo.js +119 -0
  85. package/dist/webmcp/pageInfo.test.js +85 -0
  86. package/dist/webmcp/registry.dom.test.js +175 -0
  87. package/dist/webmcp/registry.js +540 -0
  88. package/dist/webmcp/registry.test.js +228 -0
  89. package/dist/webmcp/routeContext.js +11 -0
  90. package/dist/webmcp/shim-entry.js +8 -0
  91. package/dist/webmcp/shim.js +149 -0
  92. package/dist/webmcp/useTool.js +124 -0
  93. package/package.json +4 -7
  94. package/test/cli/buildErrors.test.ts +173 -0
  95. package/test/cli/buildWarnings.test.cjs +62 -0
  96. package/test/cli/devOutput.test.ts +549 -0
  97. package/test/cli/diagnostic.test.ts +434 -0
  98. package/test/cli/output.test.cjs +90 -0
  99. package/test/cli/spinner.test.ts +177 -0
  100. package/test/cli/theme.test.ts +163 -0
  101. package/test/native/redactSecrets.test.ts +72 -0
  102. package/test/native/utils.test.ts +532 -0
  103. package/test/scripts/build.test.ts +275 -0
  104. package/test/scripts/preflight.test.ts +100 -2
  105. package/test/scripts/serve.test.ts +102 -0
  106. package/test/server/fixtures/template/src/js/routes/index.jsx +8 -0
  107. package/test/server/handler.test.ts +17 -0
  108. package/test/server/logger.test.ts +185 -0
  109. package/test/server/manifestCache.test.ts +13 -0
  110. package/test/server/otel.test.ts +556 -0
  111. package/test/server/validator.test.ts +19 -8
  112. package/vitest.config.ts +4 -0
  113. package/dist/scripts/checkVersion.js +0 -3
  114. 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
- console.error(`Unknown script "${script}".`)
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)
@@ -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,50 @@
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
+ const configPath = path.join(cwd, "config", "config.json")
22
+
23
+ let raw
24
+ try {
25
+ raw = fs.readFileSync(configPath, "utf8")
26
+ } catch (error) {
27
+ process.stderr.write(
28
+ diagnostic({
29
+ message:
30
+ error.code === "ENOENT"
31
+ ? "Config file not found"
32
+ : `Could not read config file: ${error.message}`,
33
+ file: configPath,
34
+ hint: "Every Catalyst app needs a config/config.json at its root.",
35
+ })
36
+ )
37
+ process.exit(1)
38
+ }
39
+
40
+ try {
41
+ return JSON.parse(raw)
42
+ } catch (error) {
43
+ process.stderr.write(
44
+ diagnostic({ message: `Config file is not valid JSON: ${error.message}`, file: configPath })
45
+ )
46
+ process.exit(1)
47
+ }
48
+ }
49
+
50
+ 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 }