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
@@ -0,0 +1,388 @@
1
+ "use strict"
2
+
3
+ const fs = require("fs")
4
+ const path = require("path")
5
+
6
+ const { GUTTER, glyph, t } = require("./theme.js")
7
+
8
+ const CONTEXT_LINES = 2
9
+
10
+ /**
11
+ * Render one failure the way a person reads it: what went wrong, where, the
12
+ * code around it, and what to do about it.
13
+ *
14
+ * Everything except `message` is optional, so a caller with only a message
15
+ * still gets a well-formed diagnostic rather than a half-empty template.
16
+ *
17
+ * @param {object} options
18
+ * @param {string} options.message - what went wrong, one line
19
+ * @param {string} [options.code] - stable error id, e.g. "BUNDLE-000". The
20
+ * error registry owns which code a failure carries and what it means; this
21
+ * layer only decides how it looks. Optional so callers without a code are
22
+ * unaffected.
23
+ * @param {string} [options.scope] - which part failed, e.g. "client bundle"
24
+ * @param {string} [options.file] - path to the offending file
25
+ * @param {number} [options.line] - 1-indexed
26
+ * @param {number} [options.column] - 1-indexed
27
+ * @param {string} [options.details] - supporting lines under the message,
28
+ * e.g. which config values differ
29
+ * @param {string} [options.hint] - how to fix it
30
+ * @param {string} [options.docs] - a URL that explains more
31
+ * @param {string} [options.cwd] - root for relative paths
32
+ */
33
+ function diagnostic(options) {
34
+ const { message, code, scope, file, line, column, details, hint, docs, cwd = process.cwd() } = options
35
+
36
+ const out = []
37
+ out.push("")
38
+ // The code identifies the failure; the scope says which part produced it.
39
+ // Both are dim -- they orient, they are not the thing you read first.
40
+ const label = [code, scope].filter(Boolean).join(" ")
41
+ out.push(`${GUTTER}${t.bad(t.bold("error"))}${label ? ` ${t.dim(label)}` : ""}`)
42
+ out.push(`${GUTTER}${t.bold(message)}`)
43
+
44
+ if (details) {
45
+ for (const text of String(details).split("\n")) {
46
+ out.push(`${GUTTER}${t.dim(text)}`)
47
+ }
48
+ }
49
+
50
+ if (file) {
51
+ const shown = relativeIfInside(file, cwd)
52
+ const position = line != null ? `:${line}${column != null ? `:${column}` : ""}` : ""
53
+ out.push(`${GUTTER}${t.dim("at")} ${t.accent(shown)}${t.dim(position)}`)
54
+ }
55
+
56
+ const frame = file && line != null ? codeFrame(file, line, column) : null
57
+ if (frame) {
58
+ out.push("")
59
+ out.push(frame)
60
+ }
61
+
62
+ if (hint) {
63
+ out.push("")
64
+ for (const [index, text] of wrap(hint, 72).entries()) {
65
+ out.push(index === 0 ? `${GUTTER}${t.accent("hint")} ${text}` : `${GUTTER} ${text}`)
66
+ }
67
+ }
68
+
69
+ if (docs) {
70
+ // Under a hint the two read as a pair; under a code frame the docs line
71
+ // needs the same gap a hint gets, or it reads as part of the source.
72
+ if (!hint && frame) out.push("")
73
+ out.push(`${GUTTER}${t.dim("docs")} ${t.dim(docs)}`)
74
+ }
75
+
76
+ out.push("")
77
+ return out.join("\n")
78
+ }
79
+
80
+ /**
81
+ * The offending line with its neighbours, and a caret under the column.
82
+ * Returns null rather than throwing when the file cannot be read -- a
83
+ * diagnostic must never fail while reporting a failure.
84
+ */
85
+ function codeFrame(file, line, column) {
86
+ let source
87
+ try {
88
+ source = fs.readFileSync(file, "utf8")
89
+ } catch (error) {
90
+ return null
91
+ }
92
+
93
+ const lines = source.split("\n")
94
+ if (line < 1 || line > lines.length) return null
95
+
96
+ const first = Math.max(1, line - CONTEXT_LINES)
97
+ const last = Math.min(lines.length, line + CONTEXT_LINES)
98
+ const width = String(last).length + 1
99
+
100
+ const rendered = []
101
+ for (let n = first; n <= last; n++) {
102
+ const text = lines[n - 1].replace(/\t/g, " ")
103
+ const number = String(n).padStart(width)
104
+
105
+ if (n === line) {
106
+ rendered.push(`${GUTTER}${t.bad(">")} ${t.dim(number)} ${t.dim(glyph.pipe)} ${text}`)
107
+ if (column != null && column > 0) {
108
+ const pad = " ".repeat(width)
109
+ rendered.push(`${GUTTER} ${pad} ${t.dim(glyph.pipe)} ${" ".repeat(column - 1)}${t.bad("^")}`)
110
+ }
111
+ } else {
112
+ rendered.push(`${GUTTER} ${t.dim(number)} ${t.dim(glyph.pipe)} ${t.dim(text)}`)
113
+ }
114
+ }
115
+ return rendered.join("\n")
116
+ }
117
+
118
+ function relativeIfInside(file, cwd) {
119
+ const relative = path.relative(cwd, file)
120
+ return relative && !relative.startsWith("..") ? relative : file
121
+ }
122
+
123
+ function wrap(text, width) {
124
+ const words = String(text).split(/\s+/)
125
+ const lines = []
126
+ let current = ""
127
+ for (const word of words) {
128
+ if (current && current.length + word.length + 1 > width) {
129
+ lines.push(current)
130
+ current = word
131
+ } else {
132
+ current = current ? `${current} ${word}` : word
133
+ }
134
+ }
135
+ if (current) lines.push(current)
136
+ return lines
137
+ }
138
+
139
+ /**
140
+ * Report a failed native build by showing what the compiler actually said.
141
+ *
142
+ * The alternative -- and what this replaces -- is "Command failed with exit
143
+ * code 1" plus a generic checklist, which sends people to Android Studio or
144
+ * Xcode to find out what broke.
145
+ *
146
+ * @param {object} options
147
+ * @param {Error} options.error - the failure, ideally carrying `.output`
148
+ * @param {string} options.stage - what was being done, e.g. "Gradle build"
149
+ * @param {string} [options.code] - the stage's wrapper code (e.g. "ANDROID-000"),
150
+ * shown when the error does not carry a registry code of its own
151
+ * @param {string} [options.cwd]
152
+ */
153
+ function buildFailure({ error, stage, code: wrapperCode, cwd = process.cwd() }) {
154
+ const { extractBuildErrors, outputTail, MAX_ERRORS } = require("./buildErrors.js")
155
+ const { hintFor } = require("./hints.js")
156
+
157
+ // A wrapped registry error keeps the toolchain failure as `cause`, and the
158
+ // captured transcript rides on whichever of the two actually ran the child.
159
+ const output = error?.output || error?.cause?.output || ""
160
+ const found = extractBuildErrors(output)
161
+ // A registry code on the error (IOS-003) is more specific than the stage's
162
+ // wrapper; anything else (a spawn's "ENOENT") yields to the wrapper, which
163
+ // is the code the error catalog documents for an upstream failure.
164
+ const ownCode = error?.code && typeof error.code === "string" ? error.code : null
165
+ const code = ownCode && /^[A-Z]+(?:-[A-Z]+)*-\d{3}$/.test(ownCode) ? ownCode : wrapperCode || ownCode
166
+
167
+ if (found.length > 0) {
168
+ const out = []
169
+ const label = found.length === 1 ? "error" : `${found.length} errors`
170
+ const scopeLabel = [code, stage].filter(Boolean).join(" ")
171
+ out.push(`\n${GUTTER}${t.bad(t.bold(label))} ${t.dim(scopeLabel)}\n`)
172
+
173
+ for (const item of found) {
174
+ out.push(`${GUTTER}${t.bold(item.message)}\n`)
175
+ if (item.file) {
176
+ const shown = relativeIfInside(item.file.replace(/^file:\/\//, ""), cwd)
177
+ const at =
178
+ item.line != null ? `:${item.line}${item.column != null ? `:${item.column}` : ""}` : ""
179
+ out.push(`${GUTTER}${t.dim("at")} ${t.accent(shown)}${t.dim(at)}\n`)
180
+
181
+ const frame = codeFrame(item.file.replace(/^file:\/\//, ""), item.line, item.column)
182
+ if (frame) out.push(`\n${frame}\n`)
183
+ }
184
+ out.push("\n")
185
+ }
186
+
187
+ // Advice authored against a code we own beats a pattern guess. The
188
+ // per-stage wrapper codes (BUNDLE-000 and friends) are the exception:
189
+ // their suggestedAction only says to read the upstream error, which is
190
+ // the text we just extracted, so the mined hint stays.
191
+ const authored = code && !/-000$/.test(code) ? error?.suggestedAction : null
192
+ const first = authored ? { hint: authored, docs: error?.docUrl } : hintFor(found[0].message)
193
+ if (first) {
194
+ out.push(`${GUTTER}${t.accent("hint")} ${first.hint}\n\n`)
195
+ }
196
+ if (found.length === MAX_ERRORS) {
197
+ out.push(`${GUTTER}${t.dim("… more errors may follow; the full build output is above.")}\n\n`)
198
+ }
199
+ return out.join("")
200
+ }
201
+
202
+ // Nothing recognised: show the tail rather than claiming to know better.
203
+ const tail = outputTail(output)
204
+ const known = hintFor(error?.message || "")
205
+ const bareExit = /^Command failed with exit code \d+$/.test(String(error?.message || "").trim())
206
+ return (
207
+ diagnostic({
208
+ // "Command failed with exit code 1" names the symptom, not the
209
+ // failure. When that is all the child gave us, say where it broke
210
+ // and let the output below carry the detail.
211
+ message: bareExit ? `${stage || "The build"} failed` : error?.message || "The build failed",
212
+ code,
213
+ // Already named in the message above; repeating it reads as a stutter.
214
+ scope: bareExit ? undefined : stage,
215
+ hint: error?.hint || error?.suggestedAction || known?.hint,
216
+ docs: error?.docUrl || known?.docs,
217
+ cwd,
218
+ }) +
219
+ (tail
220
+ ? `\n${GUTTER}${t.dim("Last output from the build:")}\n` +
221
+ tail
222
+ .split("\n")
223
+ .map((line) => `${GUTTER}${t.dim(glyph.pipe)} ${t.dim(line)}`)
224
+ .join("\n") +
225
+ "\n\n"
226
+ : "")
227
+ )
228
+ }
229
+
230
+ /**
231
+ * A registry error (CatalystError) as a diagnostic: its code, message, the
232
+ * cause or details beneath, its suggested action as the hint, and its docs.
233
+ * The default view for preflight and server-startup failures; the registry's
234
+ * own box stays for --verbose and --debug.
235
+ *
236
+ * @param {object} error - a CatalystError, or anything with a message
237
+ * @param {object} [options]
238
+ * @param {string} [options.scope]
239
+ * @param {string} [options.cwd]
240
+ */
241
+ function registryFailure(error, { scope, cwd } = {}) {
242
+ const cause = error?.cause
243
+ return diagnostic({
244
+ message: error?.message || String(error),
245
+ code: typeof error?.code === "string" ? error.code : undefined,
246
+ scope,
247
+ details: cause ? cause.message || String(cause) : error?.details,
248
+ hint: error?.suggestedAction,
249
+ docs: error?.docUrl,
250
+ cwd,
251
+ })
252
+ }
253
+
254
+ // V8 frame: " at fn (file:line:col)" or " at file:line:col".
255
+ const FRAME = /^\s*at\s+(?:(.*?)\s+\()?(?:file:\/\/)?([^()]+?):(\d+):(\d+)\)?\s*$/
256
+
257
+ /** Parse a V8 stack into frames; lines that are not frames are skipped. */
258
+ function parseFrames(stack) {
259
+ const frames = []
260
+ for (const line of String(stack || "").split("\n")) {
261
+ const match = FRAME.exec(line)
262
+ if (!match) continue
263
+ const [, fn, file, lineNo, column] = match
264
+ frames.push({ fn: fn || null, file, line: Number(lineNo), column: Number(column) })
265
+ }
266
+ return frames
267
+ }
268
+
269
+ /**
270
+ * A frame in the user's own code: inside the app, outside node_modules and
271
+ * Node's internals. These are the only frames someone can act on -- a React
272
+ * render error otherwise arrives under ten frames of react-dom.
273
+ */
274
+ function isAppFrame(frame, cwd) {
275
+ if (!frame.file || frame.file.startsWith("node:")) return false
276
+ if (frame.file.includes(`${path.sep}node_modules${path.sep}`)) return false
277
+ const relative = path.relative(cwd, frame.file)
278
+ return Boolean(relative) && !relative.startsWith("..") && !path.isAbsolute(relative)
279
+ }
280
+
281
+ function describeFrame(frame, cwd) {
282
+ const where = `${relativeIfInside(frame.file, cwd)}:${frame.line}:${frame.column}`
283
+ return frame.fn ? `${frame.fn} ${t.dim(`(${where})`)}` : where
284
+ }
285
+
286
+ /**
287
+ * Report an error raised while the server is running -- a render that threw, a
288
+ * data fetcher that rejected, an uncaught exception.
289
+ *
290
+ * One block per failure, pointed at the first frame in the app's own code with
291
+ * the source around it. The default view omits library frames entirely;
292
+ * `verbose` lists the app's frames, `debug` prints the whole stack and the
293
+ * environment.
294
+ *
295
+ * @param {object} options
296
+ * @param {*} options.error - whatever was thrown
297
+ * @param {string} [options.code] - registry code, e.g. "RUNTIME-WEB-001"
298
+ * @param {string} [options.scope] - where it happened, e.g. "GET /products"
299
+ * @param {string} [options.docs]
300
+ * @param {string} [options.mode] - "default" | "verbose" | "debug"
301
+ * @param {object} [options.env] - debug-mode environment, key -> value
302
+ * @param {string} [options.cwd] - the app root
303
+ */
304
+ function runtimeFailure({ error, code, scope, docs, mode = "default", env, cwd = process.cwd() }) {
305
+ const isError = error instanceof Error || (error && typeof error === "object" && "message" in error)
306
+ let message = isError ? String(error.message || error.name || "Error") : String(error)
307
+ // Bundler messages run to several sentences of generic advice; the first
308
+ // says what happened, and the location below says where.
309
+ if (message.length > 100) message = message.replace(/^(.+?\.)\s+[A-Z].*$/s, "$1")
310
+ const frames = isError ? parseFrames(error.stack) : []
311
+ const appFrames = frames.filter((frame) => isAppFrame(frame, cwd))
312
+ // A transform error (Vite) carries the source position as `loc`, its
313
+ // column already 1-based; its stack is the bundler's own and says nothing
314
+ // about the app.
315
+ const loc = isError && error.loc?.file ? error.loc : null
316
+ const origin = loc ? { file: loc.file, line: loc.line, column: loc.column } : appFrames[0]
317
+ const { hintFor } = require("./hints.js")
318
+ const known = hintFor(message)
319
+
320
+ let out = diagnostic({
321
+ message: isError && error.name && error.name !== "Error" ? `${error.name}: ${message}` : message,
322
+ code,
323
+ scope,
324
+ file: origin?.file,
325
+ line: origin?.line,
326
+ column: origin?.column,
327
+ hint: known?.hint,
328
+ docs: docs || known?.docs,
329
+ cwd,
330
+ })
331
+
332
+ if (mode === "verbose" && appFrames.length > 1) {
333
+ out += `${GUTTER}${t.dim("stack")}\n`
334
+ out += appFrames.map((frame) => `${GUTTER} ${t.dim("at")} ${describeFrame(frame, cwd)}`).join("\n")
335
+ out += "\n\n"
336
+ }
337
+ if (mode === "debug") {
338
+ const stack = isError && error.stack ? String(error.stack).split("\n").slice(1) : []
339
+ if (stack.length) out += `${stack.map((line) => `${GUTTER}${t.dim(line.trim())}`).join("\n")}\n\n`
340
+ if (env) {
341
+ out += Object.entries(env)
342
+ .map(([key, value]) => `${GUTTER}${t.dim(key.padEnd(10))}${value}`)
343
+ .join("\n")
344
+ out += "\n\n"
345
+ }
346
+ }
347
+ return out
348
+ }
349
+
350
+ /**
351
+ * The --verbose/--debug addendum for a failed build: the registry code and
352
+ * docs, then everything the tool printed. The default view shows only the
353
+ * extracted errors; when someone asks for more, the useful "more" is the raw
354
+ * output, not a restatement of the exit code.
355
+ */
356
+ function verboseOutput({ code, docs, output, mode = "verbose", error, env }) {
357
+ // Leads with a blank line: it follows a diagnostic block directly.
358
+ const out = [""]
359
+ const label = [code, docs].filter(Boolean).map((part) => t.dim(part))
360
+ if (label.length) out.push(`${GUTTER}${label.join(t.dim(" · "))}`)
361
+ const text = String(output || "").replace(/\s+$/, "")
362
+ if (text) {
363
+ out.push(`${GUTTER}${t.dim("Full output:")}`)
364
+ for (const line of text.split("\n")) out.push(`${GUTTER}${t.dim(glyph.pipe)} ${line}`)
365
+ }
366
+ if (mode === "debug") {
367
+ const stack = error?.stack ? String(error.stack).split("\n").slice(1) : []
368
+ if (stack.length) out.push("", ...stack.map((line) => `${GUTTER}${t.dim(line.trim())}`))
369
+ if (env) {
370
+ out.push(
371
+ "",
372
+ ...Object.entries(env).map(([key, value]) => `${GUTTER}${t.dim(key.padEnd(10))}${value}`)
373
+ )
374
+ }
375
+ }
376
+ return out.length > 1 ? `${out.join("\n")}\n\n` : ""
377
+ }
378
+
379
+ module.exports = {
380
+ diagnostic,
381
+ buildFailure,
382
+ registryFailure,
383
+ runtimeFailure,
384
+ verboseOutput,
385
+ codeFrame,
386
+ parseFrames,
387
+ isAppFrame,
388
+ }
@@ -0,0 +1,69 @@
1
+ "use strict"
2
+
3
+ /**
4
+ * Hints for failures we actually understand.
5
+ *
6
+ * Deliberately a short list. A hint that guesses is worse than no hint: it
7
+ * sends people down the wrong path with false confidence. Only add an entry
8
+ * when the pattern reliably implies the cause, and say what to *do*, not what
9
+ * went wrong -- the message above the hint already said that.
10
+ */
11
+ const HINTS = [
12
+ {
13
+ test: /Failed to resolve import ["']([^"']+)["']/,
14
+ hint: (match) =>
15
+ match[1].startsWith("@")
16
+ ? `The alias ${match[1].split("/")[0]} is not defined in package.json _moduleAliases. Add it, or import with a relative path.`
17
+ : `Check the path, or install the package with npm install ${match[1]}.`,
18
+ docs: "https://catalyst.1mg.com/errors/unresolved-import",
19
+ },
20
+ {
21
+ test: /EADDRINUSE[^0-9]*(?:address already in use\s*)?(\S*?:?(\d+))?\s*$/i,
22
+ hint: (match) => {
23
+ const port = match[2]
24
+ const where = port ? `port ${port}` : "that port"
25
+ return port
26
+ ? `Something is already on ${where}. Free it with \`lsof -ti :${port} | xargs kill\`, or set NODE_SERVER_PORT in config/config.json.`
27
+ : `Something is already on ${where}. Stop it, or set NODE_SERVER_PORT in config/config.json.`
28
+ },
29
+ },
30
+ {
31
+ test: /SDK location not found|ANDROID_HOME/i,
32
+ hint: () =>
33
+ "Set the Android SDK path in config/config.json under WEBVIEW_CONFIG.android.sdkPath, or export ANDROID_HOME.",
34
+ },
35
+ {
36
+ test: /No booted simulator found/i,
37
+ hint: () => "Open Simulator and boot a device, or run catalyst setupEmulator:ios.",
38
+ },
39
+ {
40
+ test: /Unknown AVD name|no such file or directory.*avd/i,
41
+ hint: () =>
42
+ "That emulator does not exist. List the ones you have with `emulator -list-avds`, then set WEBVIEW_CONFIG.android.emulatorName.",
43
+ },
44
+ {
45
+ test: /Command not found: (.+)/,
46
+ hint: (match) =>
47
+ `${match[1].trim()} is not on PATH. Install it, or correct the path in config/config.json.`,
48
+ },
49
+ {
50
+ test: /code signing|CODE_SIGN|provisioning profile/i,
51
+ hint: () =>
52
+ "Open the project in Xcode once and pick a signing team, or build for the simulator instead of a device.",
53
+ },
54
+ ]
55
+
56
+ /**
57
+ * @param {string} message
58
+ * @returns {{hint: string, docs?: string}|null}
59
+ */
60
+ function hintFor(message) {
61
+ if (!message) return null
62
+ for (const entry of HINTS) {
63
+ const match = entry.test.exec(message)
64
+ if (match) return { hint: entry.hint(match), docs: entry.docs }
65
+ }
66
+ return null
67
+ }
68
+
69
+ module.exports = { hintFor }
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -0,0 +1,141 @@
1
+ "use strict"
2
+
3
+ const { GUTTER, SPINNER_FRAMES, t, isInteractive, step } = require("./theme.js")
4
+
5
+ const FRAME_MS = 120
6
+
7
+ // Carriage return + erase-line. Built rather than written as a literal so no
8
+ // raw control byte sits in the source, where editors and diffs mangle it.
9
+ const CLEAR_LINE = `\r${String.fromCharCode(27)}[2K`
10
+
11
+ // Every spinner currently animating, so code that has no handle on one -- a
12
+ // config prompt deep in the native setup -- can still stand them all down.
13
+ // A prompt written under a live spinner is erased by the next frame, which
14
+ // makes a question look like a hang.
15
+ const live = new Set()
16
+
17
+ /**
18
+ * A single-line progress indicator that collapses into its own result.
19
+ *
20
+ * Deliberately one line and no more: it owns exactly the row it is drawn on,
21
+ * so anything printed before it is untouched and anything after starts on a
22
+ * fresh row. That is what lets streamed child output and the spinner coexist
23
+ * without the cursor-math problems a multi-line region creates.
24
+ *
25
+ * Non-interactive (pipe, CI) prints a plain start line and a plain result
26
+ * line: no animation, no escape codes, and every state still recorded.
27
+ */
28
+ class Spinner {
29
+ constructor(stream = process.stdout) {
30
+ this.stream = stream
31
+ this.timer = null
32
+ this.frame = 0
33
+ this.label = ""
34
+ this.startedAt = 0
35
+ this.active = false
36
+ this.interactive = isInteractive(stream)
37
+ }
38
+
39
+ start(label) {
40
+ // Starting twice without an intervening stop would orphan the first
41
+ // interval, leaving two timers writing to the same row forever.
42
+ if (this.timer) {
43
+ clearInterval(this.timer)
44
+ this.timer = null
45
+ }
46
+
47
+ this.label = label
48
+ this.startedAt = Date.now()
49
+ this.active = true
50
+ live.add(this)
51
+
52
+ if (!this.interactive) {
53
+ this.stream.write(`${GUTTER}${t.dim("·")} ${label}\n`)
54
+ return this
55
+ }
56
+
57
+ this.frame = 0
58
+ this.render()
59
+ this.timer = setInterval(() => {
60
+ this.frame = (this.frame + 1) % SPINNER_FRAMES.length
61
+ this.render()
62
+ }, FRAME_MS)
63
+ // Never hold the process open for an animation.
64
+ if (this.timer.unref) this.timer.unref()
65
+ return this
66
+ }
67
+
68
+ /** Change the text without resetting the elapsed timer. */
69
+ update(label) {
70
+ this.label = label
71
+ if (this.interactive && this.active) this.render()
72
+ }
73
+
74
+ render() {
75
+ this.stream.write(`${CLEAR_LINE}${GUTTER}${t.accent(SPINNER_FRAMES[this.frame])} ${this.label}`)
76
+ }
77
+
78
+ /** Replace the live line with its final state. */
79
+ stop(state = "done", label = this.label) {
80
+ if (!this.active) return
81
+ this.active = false
82
+ live.delete(this)
83
+
84
+ const elapsed = Date.now() - this.startedAt
85
+
86
+ if (this.timer) {
87
+ clearInterval(this.timer)
88
+ this.timer = null
89
+ }
90
+
91
+ if (this.interactive) {
92
+ // Erase the animated line, then write the settled one in its place.
93
+ this.stream.write(CLEAR_LINE)
94
+ }
95
+
96
+ this.stream.write(`${step(label, elapsed, state)}\n`)
97
+ }
98
+
99
+ /**
100
+ * Stand the animation down so something else can write, without ending the
101
+ * step. Used when a child process streams output beneath a running step.
102
+ */
103
+ pause() {
104
+ if (!this.interactive || !this.active) return
105
+ if (this.timer) {
106
+ clearInterval(this.timer)
107
+ this.timer = null
108
+ }
109
+ // Erase the live row entirely and leave the cursor on it. Whatever
110
+ // prints next takes that row, and resume() redraws below. Leaving the
111
+ // label behind here would strand a second copy of the step, which then
112
+ // settles into its own duplicate line.
113
+ this.stream.write(CLEAR_LINE)
114
+ }
115
+
116
+ resume() {
117
+ if (!this.interactive || !this.active || this.timer) return
118
+ this.render()
119
+ this.timer = setInterval(() => {
120
+ this.frame = (this.frame + 1) % SPINNER_FRAMES.length
121
+ this.render()
122
+ }, FRAME_MS)
123
+ if (this.timer.unref) this.timer.unref()
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Stand every live spinner down while `fn` writes to or reads from the
129
+ * terminal, then bring them back -- on the failure path too.
130
+ */
131
+ async function withSpinnersPaused(fn) {
132
+ const paused = [...live]
133
+ for (const spinner of paused) spinner.pause()
134
+ try {
135
+ return await fn()
136
+ } finally {
137
+ for (const spinner of paused) spinner.resume()
138
+ }
139
+ }
140
+
141
+ module.exports = { Spinner, withSpinnersPaused }