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.
- package/bin/catalyst.js +105 -1
- package/dist/cli/README.md +119 -0
- package/dist/cli/appConfig.js +50 -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/errors/registry.js +8 -0
- package/dist/errors-index.json +7 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.jsx +1 -1
- 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 +61 -94
- 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 +335 -54
- package/dist/otel.js +15 -9
- package/dist/scripts/build.js +314 -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 +115 -11
- package/dist/scripts/scriptUtils.js +34 -0
- package/dist/scripts/serve.js +65 -9
- package/dist/scripts/start.js +58 -21
- package/dist/server/expressServer.js +167 -32
- package/dist/server/renderer/document/Body.d.ts +0 -11
- package/dist/server/renderer/document/Body.jsx +0 -10
- package/dist/server/renderer/document/Head.d.ts +0 -13
- package/dist/server/renderer/document/Head.jsx +0 -12
- 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 +19 -1
- package/dist/vite/vite.config.js +96 -19
- package/dist/vite/vite.config.server.js +21 -0
- package/dist/web-router/components/MetaTag.jsx +20 -14
- package/dist/web-router/components/MetaTag.test.jsx +108 -32
- package/dist/web-router/components/RouterDataProvider.d.ts +25 -1
- package/dist/web-router/components/RouterDataProvider.jsx +35 -1
- package/dist/web-router/components/RouterDataProvider.test.jsx +44 -0
- package/dist/web-router/context.d.ts +15 -1
- package/dist/web-router/context.jsx +11 -0
- package/dist/webmcp/WebMcpProvider.jsx +234 -0
- package/dist/webmcp/declarative.js +207 -0
- package/dist/webmcp/declarative.test.js +122 -0
- package/dist/webmcp/errors.js +59 -0
- package/dist/webmcp/errors.test.js +47 -0
- package/dist/webmcp/index.js +40 -0
- package/dist/webmcp/native.js +37 -0
- package/dist/webmcp/native.test.js +41 -0
- package/dist/webmcp/pageInfo.js +119 -0
- package/dist/webmcp/pageInfo.test.js +85 -0
- package/dist/webmcp/registry.dom.test.js +175 -0
- package/dist/webmcp/registry.js +540 -0
- package/dist/webmcp/registry.test.js +228 -0
- package/dist/webmcp/routeContext.js +11 -0
- package/dist/webmcp/shim-entry.js +8 -0
- package/dist/webmcp/shim.js +149 -0
- package/dist/webmcp/useTool.js +124 -0
- package/package.json +4 -7
- 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/preflight.test.ts +100 -2
- 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/manifestCache.test.ts +13 -0
- package/test/server/otel.test.ts +556 -0
- package/test/server/validator.test.ts +19 -8
- package/vitest.config.ts +4 -0
- package/dist/scripts/checkVersion.js +0 -3
- package/dist/scripts/cleanCache.js +0 -30
package/dist/scripts/build.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import path from "path"
|
|
2
2
|
import { spawn } from "child_process"
|
|
3
|
-
import { arrayToObject, resolveOutputMode, getDebugEnvInfo } from "./scriptUtils.js"
|
|
3
|
+
import { arrayToObject, loaderImportArg, resolveOutputMode, getDebugEnvInfo } from "./scriptUtils.js"
|
|
4
4
|
import { fileURLToPath } from "url"
|
|
5
5
|
import { dirname } from "path"
|
|
6
|
-
import { readFileSync, existsSync, rmSync } from "fs"
|
|
6
|
+
import { readFileSync, readdirSync, existsSync, rmSync } from "fs"
|
|
7
|
+
import { gzipSync } from "zlib"
|
|
7
8
|
import { createRequire } from "module"
|
|
8
|
-
import {
|
|
9
|
-
import { runStaticPreflightOrExit } from "./preflight.js"
|
|
9
|
+
import { getDocUrl, ERROR_CODES } from "../errors/index.js"
|
|
10
|
+
import { runStaticPreflightOrExit, writeBuildInfo } from "./preflight.js"
|
|
10
11
|
|
|
11
12
|
// Fail fast on a misconfigured app (missing/invalid config.json, package.json,
|
|
12
13
|
// or moduleAliases) with a coded, doc-linked error — must run before the raw
|
|
@@ -16,9 +17,23 @@ runStaticPreflightOrExit()
|
|
|
16
17
|
|
|
17
18
|
const __filename = fileURLToPath(import.meta.url)
|
|
18
19
|
const __dirname = dirname(__filename)
|
|
20
|
+
|
|
21
|
+
// src/cli is CommonJS so src/native (also CJS) can share it; pull it in here
|
|
22
|
+
// rather than duplicating the visual language for the web commands.
|
|
23
|
+
const requireCjs = createRequire(import.meta.url)
|
|
24
|
+
const { header, step, glyph, t, GUTTER, duration, bytes } = requireCjs("../cli/theme.js")
|
|
25
|
+
const { diagnostic, verboseOutput } = requireCjs("../cli/diagnostic.js")
|
|
26
|
+
const { Spinner } = requireCjs("../cli/spinner.js")
|
|
27
|
+
const { loadAppConfig } = requireCjs("../cli/appConfig.js")
|
|
28
|
+
const { bundleWarnings } = requireCjs("../cli/buildWarnings.js")
|
|
29
|
+
const { extractBuildErrors, outputTail } = requireCjs("../cli/buildErrors.js")
|
|
30
|
+
const { hintFor } = requireCjs("../cli/hints.js")
|
|
19
31
|
const loaderPath = path.resolve(__dirname, "../../dist/vite/node-loader.mjs")
|
|
20
|
-
|
|
21
|
-
|
|
32
|
+
|
|
33
|
+
// A missing or malformed config used to throw a raw ENOENT/SyntaxError stack at
|
|
34
|
+
// module load, before any output at all. Note the "utf-8" was being passed as
|
|
35
|
+
// JSON.parse's reviver argument, not as the read encoding.
|
|
36
|
+
const configJSON = loadAppConfig(process.env.PWD || process.cwd())
|
|
22
37
|
|
|
23
38
|
// Resolve vite's actual JS entry point rather than spawning the "vite" command name,
|
|
24
39
|
// which on Windows only resolves via the npm-installed .cmd shim (i.e. requires a shell).
|
|
@@ -27,27 +42,220 @@ const require = createRequire(import.meta.url)
|
|
|
27
42
|
const viteBinPath = path.join(path.dirname(require.resolve("vite/package.json")), "bin", "vite.js")
|
|
28
43
|
|
|
29
44
|
/**
|
|
45
|
+
* A child's exit code, or 1 when there is not a usable one.
|
|
46
|
+
*
|
|
47
|
+
* Spawn failures carry a string `code` like "ENOENT", and process.exit throws
|
|
48
|
+
* ERR_INVALID_ARG_TYPE on anything that is not an integer -- which would
|
|
49
|
+
* replace the friendly error with an internal stack trace.
|
|
50
|
+
*/
|
|
51
|
+
const MAX_CAPTURED_BYTES = 1024 * 1024
|
|
52
|
+
|
|
53
|
+
function exitCodeFrom(error) {
|
|
54
|
+
return Number.isInteger(error?.code) ? error.code : 1
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Run one build step, capturing its output.
|
|
59
|
+
*
|
|
60
|
+
* Output is piped rather than inherited. With inherit the bundler wrote
|
|
61
|
+
* straight to the terminal at column 0 -- so two parallel bundles interleaved,
|
|
62
|
+
* the same error printed once per bundle, and the parent held nothing but an
|
|
63
|
+
* exit code. Holding the text is what makes it possible to find the real
|
|
64
|
+
* compiler error, dedupe it, and show it in the house style.
|
|
65
|
+
*
|
|
30
66
|
* @param {string[]} args
|
|
31
67
|
* @param {import('child_process').SpawnOptions} options
|
|
68
|
+
* @param {string} label - names this step, e.g. "client"
|
|
32
69
|
*/
|
|
33
|
-
function runBuildStep(args, options) {
|
|
70
|
+
function runBuildStep(args, options, label = "Build step") {
|
|
34
71
|
return new Promise((resolve, reject) => {
|
|
35
|
-
const child = spawn(process.execPath, args,
|
|
72
|
+
const child = spawn(process.execPath, args, {
|
|
73
|
+
...options,
|
|
74
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
// Only the tail is ever shown, so a pathological build cannot grow
|
|
78
|
+
// this without bound.
|
|
79
|
+
let output = ""
|
|
80
|
+
const keep = (chunk) => {
|
|
81
|
+
output += chunk.toString()
|
|
82
|
+
if (output.length > MAX_CAPTURED_BYTES) output = output.slice(-MAX_CAPTURED_BYTES)
|
|
83
|
+
}
|
|
84
|
+
child.stdout.on("data", keep)
|
|
85
|
+
child.stderr.on("data", keep)
|
|
86
|
+
|
|
36
87
|
child.on("close", (code) => {
|
|
37
88
|
if (code === 0) {
|
|
38
|
-
resolve()
|
|
89
|
+
resolve({ label, output })
|
|
39
90
|
} else {
|
|
40
|
-
reject(
|
|
91
|
+
reject(
|
|
92
|
+
Object.assign(new Error(`${label} failed with exit code ${code}`), {
|
|
93
|
+
code,
|
|
94
|
+
label,
|
|
95
|
+
output,
|
|
96
|
+
})
|
|
97
|
+
)
|
|
41
98
|
}
|
|
42
99
|
})
|
|
43
100
|
child.on("error", reject)
|
|
44
101
|
})
|
|
45
102
|
}
|
|
46
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Report bundle failures once each, not once per bundle.
|
|
106
|
+
*
|
|
107
|
+
* One broken import fails both bundles, so the naive loop printed the same
|
|
108
|
+
* error twice with two identical hints. Errors are keyed on their cause, and
|
|
109
|
+
* the bundles they broke become a dim suffix rather than a second block.
|
|
110
|
+
*/
|
|
111
|
+
function shortenPaths(text, cwd) {
|
|
112
|
+
return String(text).split(`${cwd}/`).join("")
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** A step's own last words, fenced under the diagnostic that names it. */
|
|
116
|
+
function fencedTail(output, lines = 8) {
|
|
117
|
+
const tail = outputTail(output || "", lines)
|
|
118
|
+
return tail
|
|
119
|
+
? tail
|
|
120
|
+
.split("\n")
|
|
121
|
+
.map((line) => `${GUTTER}${t.dim(glyph.pipe)} ${t.dim(line)}`)
|
|
122
|
+
.join("\n") + "\n\n"
|
|
123
|
+
: ""
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function reportBundleFailures(failures, cwd) {
|
|
127
|
+
// Every bundle failure is the registry's upstream wrapper: its code and
|
|
128
|
+
// docs head each block, as they did before the extracted-error view.
|
|
129
|
+
const code = ERROR_CODES.BUNDLE_UPSTREAM_ERROR
|
|
130
|
+
const docs = getDocUrl(code)
|
|
131
|
+
const byCause = new Map()
|
|
132
|
+
const unrecognised = []
|
|
133
|
+
|
|
134
|
+
for (const failure of failures) {
|
|
135
|
+
const found = extractBuildErrors(failure.output, { toolchain: "vite" })
|
|
136
|
+
if (found.length === 0) {
|
|
137
|
+
unrecognised.push(failure)
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
for (const error of found) {
|
|
141
|
+
const key = `${error.file || ""}:${error.line || ""}:${error.message}`
|
|
142
|
+
const existing = byCause.get(key)
|
|
143
|
+
if (existing) existing.bundles.push(failure.label)
|
|
144
|
+
else byCause.set(key, { ...error, bundles: [failure.label] })
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Nothing recognised: fence the bundler's own words rather than inventing
|
|
149
|
+
// a summary that says less than they do. Rendered even when the other
|
|
150
|
+
// bundle's error was recognised, so neither failure goes unreported.
|
|
151
|
+
const unrecognisedReport = unrecognised
|
|
152
|
+
.map(
|
|
153
|
+
(failure) =>
|
|
154
|
+
diagnostic({
|
|
155
|
+
message: `The ${failure.label || "production"} bundle failed`,
|
|
156
|
+
code,
|
|
157
|
+
scope: "production build",
|
|
158
|
+
docs,
|
|
159
|
+
cwd,
|
|
160
|
+
}) + fencedTail(failure.output)
|
|
161
|
+
)
|
|
162
|
+
.join("")
|
|
163
|
+
|
|
164
|
+
// Each cause renders as the same diagnostic block every other failure
|
|
165
|
+
// uses: message, location, the source around it, and a hint when the
|
|
166
|
+
// cause is one we understand.
|
|
167
|
+
const recognisedReport = [...byCause.values()]
|
|
168
|
+
.map((cause) =>
|
|
169
|
+
diagnostic({
|
|
170
|
+
message: shortenPaths(cause.message, cwd),
|
|
171
|
+
code,
|
|
172
|
+
scope: `${cause.bundles.join(" + ")} bundle`,
|
|
173
|
+
file: cause.file ? path.resolve(cwd, cause.file.replace(/^(\.\.\/)+/, "")) : undefined,
|
|
174
|
+
line: cause.line,
|
|
175
|
+
// esbuild reports columns 0-based; the frame's caret is 1-based.
|
|
176
|
+
column: cause.column != null ? cause.column + 1 : undefined,
|
|
177
|
+
hint: hintFor(cause.message)?.hint,
|
|
178
|
+
docs,
|
|
179
|
+
cwd,
|
|
180
|
+
})
|
|
181
|
+
)
|
|
182
|
+
.join("")
|
|
183
|
+
|
|
184
|
+
return recognisedReport + unrecognisedReport
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* List what was actually produced, largest first, with gzip where we have it.
|
|
189
|
+
*
|
|
190
|
+
* Reads the directory rather than parsing the bundler's output: the files on
|
|
191
|
+
* disk are the truth, and this keeps working if the bundler changes format.
|
|
192
|
+
*/
|
|
193
|
+
function printAssetTable(outputPath, label) {
|
|
194
|
+
const entries = []
|
|
195
|
+
|
|
196
|
+
const walk = (dir, prefix) => {
|
|
197
|
+
let items
|
|
198
|
+
try {
|
|
199
|
+
items = readdirSync(dir, { withFileTypes: true })
|
|
200
|
+
} catch (error) {
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
for (const item of items) {
|
|
204
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - names come from readdirSync of our own build output.
|
|
205
|
+
const full = path.join(dir, item.name)
|
|
206
|
+
const rel = prefix ? `${prefix}/${item.name}` : item.name
|
|
207
|
+
if (item.isDirectory()) {
|
|
208
|
+
walk(full, rel)
|
|
209
|
+
} else if (/\.(js|css|mjs|cjs)$/.test(item.name)) {
|
|
210
|
+
try {
|
|
211
|
+
const raw = readFileSync(full)
|
|
212
|
+
entries.push({ name: rel, size: raw.length, gzip: gzipSync(raw).length })
|
|
213
|
+
} catch (error) {
|
|
214
|
+
// A file we cannot read is not worth failing a build over.
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
walk(outputPath, "")
|
|
221
|
+
if (entries.length === 0) return
|
|
222
|
+
|
|
223
|
+
// The server bundle never reaches a browser, so its per-file sizes are not
|
|
224
|
+
// something to optimise; one total line keeps it visible without crowding
|
|
225
|
+
// the table people actually read.
|
|
226
|
+
const server = entries.filter((entry) => entry.name.startsWith("server/"))
|
|
227
|
+
entries.splice(0, entries.length, ...entries.filter((entry) => !entry.name.startsWith("server/")))
|
|
228
|
+
|
|
229
|
+
entries.sort((a, b) => b.size - a.size)
|
|
230
|
+
const shown = entries.slice(0, 12)
|
|
231
|
+
const width = Math.max(...shown.map((e) => e.name.length))
|
|
232
|
+
|
|
233
|
+
console.log(`\n${GUTTER}${t.bold("Client")} ${t.dim(`${label}/`)}`)
|
|
234
|
+
for (const [index, entry] of shown.entries()) {
|
|
235
|
+
const isLast = index === shown.length - 1 && entries.length === shown.length
|
|
236
|
+
console.log(
|
|
237
|
+
`${GUTTER}${t.dim(isLast ? glyph.last : glyph.branch)} ${entry.name.padEnd(width + 2)}` +
|
|
238
|
+
`${bytes(entry.size).padStart(9)}` +
|
|
239
|
+
`${t.dim(` ${bytes(entry.gzip).padStart(8)} gzip`)}`
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
if (entries.length > shown.length) {
|
|
243
|
+
console.log(`${GUTTER}${t.dim(glyph.last)} ${t.dim(`and ${entries.length - shown.length} more`)}`)
|
|
244
|
+
}
|
|
245
|
+
if (server.length) {
|
|
246
|
+
const total = server.reduce((sum, entry) => sum + entry.size, 0)
|
|
247
|
+
const files = `${server.length} file${server.length === 1 ? "" : "s"}`
|
|
248
|
+
console.log(
|
|
249
|
+
`\n${GUTTER}${t.bold("Server")} ${t.dim(`${label}/server/`)} ${t.dim(`${files}, ${bytes(total)}`)}`
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
47
254
|
/**
|
|
48
255
|
* @description - builds the application for production
|
|
49
256
|
*/
|
|
50
257
|
async function build() {
|
|
258
|
+
const startedAt = Date.now()
|
|
51
259
|
const commandLineArguments = process.argv.slice(2)
|
|
52
260
|
const argumentsObject = arrayToObject(commandLineArguments)
|
|
53
261
|
const outputMode = resolveOutputMode(process.argv)
|
|
@@ -57,21 +265,26 @@ async function build() {
|
|
|
57
265
|
const packageJson = JSON.parse(readFileSync(path.join(process.env.PWD, "package.json"), "utf-8"))
|
|
58
266
|
const { name } = packageJson
|
|
59
267
|
|
|
268
|
+
process.stdout.write(header("catalyst build", "production"))
|
|
269
|
+
|
|
60
270
|
const buildOutputPath = path.join(process.env.PWD, configJSON.BUILD_OUTPUT_PATH || "build")
|
|
61
271
|
if (existsSync(buildOutputPath)) {
|
|
62
|
-
|
|
272
|
+
const cleaning = Date.now()
|
|
63
273
|
rmSync(buildOutputPath, { recursive: true, force: true })
|
|
274
|
+
console.log(step("Cleaned previous output", Date.now() - cleaning))
|
|
64
275
|
}
|
|
65
276
|
|
|
66
|
-
console.log("🏗️ Building application for production...")
|
|
67
|
-
|
|
68
277
|
const baseEnv = {
|
|
69
278
|
...process.env,
|
|
279
|
+
// The children are piped now, so their stdout is not a TTY and the
|
|
280
|
+
// bundler would drop colour. Forward our own terminal's answer, so a
|
|
281
|
+
// redirect to a file still comes out plain.
|
|
282
|
+
...(process.stdout.isTTY && !process.env.NO_COLOR ? { FORCE_COLOR: "1" } : {}),
|
|
70
283
|
src_path: process.env.PWD,
|
|
71
284
|
NODE_ENV: "production",
|
|
72
285
|
VITE_BUILD_MODE: "true",
|
|
73
286
|
APPLICATION: name || "catalyst_app",
|
|
74
|
-
NODE_OPTIONS: `--
|
|
287
|
+
NODE_OPTIONS: `--import ${loaderImportArg(loaderPath)}`,
|
|
75
288
|
CATALYST_OUTPUT_MODE: outputMode,
|
|
76
289
|
...argumentsObject,
|
|
77
290
|
filterKeys: JSON.stringify([
|
|
@@ -85,41 +298,100 @@ async function build() {
|
|
|
85
298
|
|
|
86
299
|
const serverBuildArgs = [viteBinPath, "build", "--config", "./dist/vite/vite.config.server.js", "--ssr"]
|
|
87
300
|
const clientBuildArgs = [viteBinPath, "build", "--config", "./dist/vite/vite.config.client.js"]
|
|
88
|
-
const spawnBase = {
|
|
89
|
-
|
|
90
|
-
|
|
301
|
+
const spawnBase = { cwd: dirname }
|
|
302
|
+
|
|
303
|
+
// A live row while the bundles build: a large app takes tens of seconds
|
|
304
|
+
// here, and a screen that stops changing reads as a hang. Both children
|
|
305
|
+
// are piped, so nothing else writes to the terminal meanwhile.
|
|
306
|
+
const bundling = new Spinner().start("Building client and server bundles")
|
|
307
|
+
|
|
308
|
+
// Both bundles build in parallel and their output is captured, so failures
|
|
309
|
+
// are reported once, attributed to the bundle that produced them.
|
|
310
|
+
// allSettled rather than all: if both bundles are broken, Promise.all would
|
|
311
|
+
// report only whichever rejected first and hide the other.
|
|
312
|
+
const results = await Promise.allSettled([
|
|
313
|
+
runBuildStep(
|
|
314
|
+
serverBuildArgs,
|
|
315
|
+
{ ...spawnBase, env: { ...baseEnv, CATALYST_VITE_CACHE_ID: "ssr" } },
|
|
316
|
+
"server"
|
|
317
|
+
),
|
|
318
|
+
runBuildStep(
|
|
319
|
+
clientBuildArgs,
|
|
320
|
+
{ ...spawnBase, env: { ...baseEnv, CATALYST_VITE_CACHE_ID: "client" } },
|
|
321
|
+
"client"
|
|
322
|
+
),
|
|
323
|
+
])
|
|
324
|
+
|
|
325
|
+
const failures = results.filter((result) => result.status === "rejected").map((result) => result.reason)
|
|
326
|
+
|
|
327
|
+
if (failures.length > 0) {
|
|
328
|
+
bundling.stop("fail", "Build failed")
|
|
329
|
+
process.stderr.write(reportBundleFailures(failures, process.env.PWD))
|
|
330
|
+
// --verbose/--debug add the registry code and the bundler's full
|
|
331
|
+
// output under the extracted errors.
|
|
332
|
+
if (outputMode !== "default") {
|
|
333
|
+
process.stderr.write(
|
|
334
|
+
verboseOutput({
|
|
335
|
+
code: ERROR_CODES.BUNDLE_UPSTREAM_ERROR,
|
|
336
|
+
docs: getDocUrl(ERROR_CODES.BUNDLE_UPSTREAM_ERROR),
|
|
337
|
+
output: failures.map((failure) => `[${failure.label}]\n${failure.output}`).join("\n"),
|
|
338
|
+
mode: outputMode,
|
|
339
|
+
error: failures[0],
|
|
340
|
+
env: outputMode === "debug" ? getDebugEnvInfo() : undefined,
|
|
341
|
+
})
|
|
342
|
+
)
|
|
343
|
+
}
|
|
344
|
+
process.exit(exitCodeFrom(failures[0]))
|
|
91
345
|
}
|
|
92
346
|
|
|
93
|
-
|
|
347
|
+
// Warnings that survived the config filters were captured and then thrown
|
|
348
|
+
// away on a green build, so a real one (a circular dependency, an oversized
|
|
349
|
+
// chunk) vanished silently. Surface them under the step that produced them.
|
|
350
|
+
const warnings = results
|
|
351
|
+
.filter((result) => result.status === "fulfilled")
|
|
352
|
+
.flatMap((result) => bundleWarnings(result.value.output, result.value.label))
|
|
94
353
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
env: { ...baseEnv, CATALYST_VITE_CACHE_ID: "ssr" },
|
|
100
|
-
}),
|
|
101
|
-
runBuildStep(clientBuildArgs, {
|
|
102
|
-
...spawnBase,
|
|
103
|
-
env: { ...baseEnv, CATALYST_VITE_CACHE_ID: "client" },
|
|
104
|
-
}),
|
|
105
|
-
])
|
|
106
|
-
} catch (err) {
|
|
107
|
-
console.error("❌ Build failed!")
|
|
108
|
-
const debugEnv = outputMode === "debug" ? getDebugEnvInfo() : undefined
|
|
109
|
-
console.error(formatError(wrapForeignError("BUNDLE", err), outputMode, debugEnv))
|
|
110
|
-
process.exit(1)
|
|
354
|
+
bundling.stop("done", "Built client and server bundles")
|
|
355
|
+
// Warnings belong to the step that produced them, beneath it.
|
|
356
|
+
for (const warning of warnings) {
|
|
357
|
+
console.log(`${GUTTER}${t.dim(glyph.pipe)} ${t.warn(glyph.warn)} ${t.dim(warning)}`)
|
|
111
358
|
}
|
|
112
359
|
|
|
113
|
-
|
|
360
|
+
// Lets `serve` refuse a build that no longer matches config/config.json.
|
|
361
|
+
writeBuildInfo(process.env.PWD, configJSON, argumentsObject)
|
|
114
362
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
363
|
+
const manifest = new Spinner().start("Generating offline manifest")
|
|
364
|
+
try {
|
|
365
|
+
await runBuildStep(
|
|
366
|
+
["./dist/scripts/generateOfflineManifest.js"],
|
|
367
|
+
{ ...spawnBase, env: baseEnv },
|
|
368
|
+
"Generating the offline manifest"
|
|
369
|
+
)
|
|
370
|
+
} catch (error) {
|
|
371
|
+
manifest.stop("fail", "Generating offline manifest failed")
|
|
372
|
+
// Without this the manifest step fell through to the top-level handler,
|
|
373
|
+
// which prints a raw stack trace. The step's output is captured, so
|
|
374
|
+
// show its tail -- the exit code alone says nothing about why -- and
|
|
375
|
+
// all of it under --verbose/--debug.
|
|
376
|
+
process.stderr.write(
|
|
377
|
+
diagnostic({
|
|
378
|
+
message: `Generating the offline manifest failed: ${error.message}`,
|
|
379
|
+
scope: "offline manifest",
|
|
380
|
+
}) + (outputMode === "default" ? fencedTail(error.output) : "")
|
|
381
|
+
)
|
|
382
|
+
if (outputMode !== "default") {
|
|
383
|
+
process.stderr.write(verboseOutput({ output: error.output, mode: outputMode, error }))
|
|
384
|
+
}
|
|
385
|
+
process.exit(exitCodeFrom(error))
|
|
386
|
+
}
|
|
387
|
+
manifest.stop("done", "Generated offline manifest")
|
|
388
|
+
|
|
389
|
+
printAssetTable(buildOutputPath, configJSON.BUILD_OUTPUT_PATH || "build")
|
|
119
390
|
|
|
120
|
-
console.log(
|
|
121
|
-
console.log(
|
|
122
|
-
|
|
391
|
+
console.log(`\n${GUTTER}${t.ok(glyph.done)} ${t.bold(`Built in ${duration(Date.now() - startedAt)}`)}`)
|
|
392
|
+
console.log(
|
|
393
|
+
`${GUTTER} ${t.dim("Run ")}${t.accent("catalyst serve")}${t.dim(" to start the production server")}\n`
|
|
394
|
+
)
|
|
123
395
|
}
|
|
124
396
|
|
|
125
397
|
build().catch((err) => {
|
|
@@ -53,6 +53,14 @@ export function collectOfflineRoutes(routes, basePath = "", parentOffline = fals
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
async function loadAppRoutes(appRoot) {
|
|
56
|
+
// The shared dev config's `server` block wins over the inline options
|
|
57
|
+
// below, and its isProduction is captured at module load so NODE_ENV
|
|
58
|
+
// cannot be flipped here. This flag is the supported opt-out: without it
|
|
59
|
+
// the manifest step opens an HMR websocket and prints "Port 24678 is
|
|
60
|
+
// already in use" whenever a dev server happens to be running.
|
|
61
|
+
const previousNoHmr = process.env.CATALYST_NO_HMR
|
|
62
|
+
process.env.CATALYST_NO_HMR = "true"
|
|
63
|
+
|
|
56
64
|
const vite = await createViteServer({
|
|
57
65
|
configFile: path.resolve(__dirname, "../vite/vite.config.js"),
|
|
58
66
|
root: appRoot,
|
|
@@ -73,6 +81,8 @@ async function loadAppRoutes(appRoot) {
|
|
|
73
81
|
return typeof routesModule.getRoutes === "function" ? routesModule.getRoutes() : []
|
|
74
82
|
} finally {
|
|
75
83
|
await vite.close()
|
|
84
|
+
if (previousNoHmr === undefined) delete process.env.CATALYST_NO_HMR
|
|
85
|
+
else process.env.CATALYST_NO_HMR = previousNoHmr
|
|
76
86
|
}
|
|
77
87
|
}
|
|
78
88
|
|
|
@@ -106,7 +116,7 @@ export async function generateOfflineManifest() {
|
|
|
106
116
|
copyIfExists(path.join(appRoot, "public/offline.html"), path.join(buildDir, "offline.html"))
|
|
107
117
|
fs.copyFileSync(path.join(__dirname, "../offline/catalyst-sw.js"), path.join(buildDir, "catalyst-sw.js"))
|
|
108
118
|
|
|
109
|
-
|
|
119
|
+
// The CLI reports this step; printing it here too duplicated the line.
|
|
110
120
|
}
|
|
111
121
|
|
|
112
122
|
if (process.argv[1] && path.resolve(process.argv[1]) === __filename) {
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import path from "path"
|
|
2
2
|
import fs from "fs"
|
|
3
|
+
import { createRequire } from "module"
|
|
4
|
+
|
|
5
|
+
const { diagnostic } = createRequire(import.meta.url)("../cli/diagnostic.js")
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
8
|
* @description stores all config.json key value into process.env before server starts.
|
|
6
9
|
*/
|
|
7
10
|
const loadEnvironmentVariables = async () => {
|
|
11
|
+
// Declared outside the try so the catch can point the diagnostic at it.
|
|
12
|
+
let appConfigPath
|
|
8
13
|
try {
|
|
9
14
|
// Resolved at call time, not import time, so preServerInit.js can report a
|
|
10
15
|
// missing src_path before any path is built from it.
|
|
11
|
-
|
|
16
|
+
appConfigPath = path.resolve(process.env.src_path, "config/config.json")
|
|
12
17
|
const filterKeys = JSON.parse(process.env.filterKeys)
|
|
13
18
|
const configContent = fs.readFileSync(appConfigPath, "utf8")
|
|
14
19
|
const appConfig = JSON.parse(configContent)
|
|
@@ -45,7 +50,24 @@ const loadEnvironmentVariables = async () => {
|
|
|
45
50
|
// still keeping every pre-existing process.env entry via the spread.
|
|
46
51
|
process.env = { ...process.env, ...newConfig }
|
|
47
52
|
} catch (error) {
|
|
48
|
-
|
|
53
|
+
// This runs before the server starts, for both `start` and `serve`, so
|
|
54
|
+
// a missing or malformed config used to greet the user with a raw
|
|
55
|
+
// ENOENT/SyntaxError stack trace as the very first thing they saw.
|
|
56
|
+
process.stderr.write(
|
|
57
|
+
diagnostic({
|
|
58
|
+
message:
|
|
59
|
+
error.code === "ENOENT"
|
|
60
|
+
? "Config file not found"
|
|
61
|
+
: error instanceof SyntaxError
|
|
62
|
+
? `Config file is not valid JSON: ${error.message}`
|
|
63
|
+
: `Could not load configuration: ${error.message}`,
|
|
64
|
+
file: appConfigPath,
|
|
65
|
+
hint:
|
|
66
|
+
error.code === "ENOENT"
|
|
67
|
+
? "Every Catalyst app needs a config/config.json at its root."
|
|
68
|
+
: undefined,
|
|
69
|
+
})
|
|
70
|
+
)
|
|
49
71
|
throw error
|
|
50
72
|
}
|
|
51
73
|
}
|
|
@@ -3,16 +3,32 @@ import { createRequire } from "module"
|
|
|
3
3
|
import loadEnvironmentVariables from "./loadEnvironmentVariables.js"
|
|
4
4
|
import { validatePreInitServer, handleError, safeCallNamed } from "../server/utils/validator.js"
|
|
5
5
|
|
|
6
|
+
const { diagnostic, registryFailure } = createRequire(import.meta.url)("../cli/diagnostic.js")
|
|
7
|
+
|
|
8
|
+
// A startup check that fails: one diagnostic, then exit. A throw here printed
|
|
9
|
+
// Node's stack for a problem whose fix is one install command. The CLI that
|
|
10
|
+
// spawned this process sees the diagnostic and does not add its own.
|
|
11
|
+
function failStartup(message, hint) {
|
|
12
|
+
process.stderr.write(diagnostic({ message, scope: "server", hint }))
|
|
13
|
+
process.exit(1)
|
|
14
|
+
}
|
|
15
|
+
|
|
6
16
|
// src_path is the application root, set by the CLI when it spawns this process.
|
|
7
17
|
// Without it every path below would be resolved against undefined.
|
|
8
18
|
const appRoot = process.env.src_path
|
|
9
19
|
if (!appRoot) {
|
|
10
|
-
|
|
11
|
-
"
|
|
12
|
-
|
|
20
|
+
failStartup(
|
|
21
|
+
"process.env.src_path is not set, so the application root cannot be resolved",
|
|
22
|
+
"Start the server with `catalyst start` or `catalyst serve` from the application root."
|
|
13
23
|
)
|
|
14
24
|
}
|
|
15
25
|
|
|
26
|
+
// validator.ts renders registry errors through this. It is also bundled into
|
|
27
|
+
// the app's SSR build, which cannot reach the CLI by a relative path, so the
|
|
28
|
+
// server process hands the renderer over here, in the first module it loads.
|
|
29
|
+
// eslint-disable-next-line no-undef -- the scripts lint env predates globalThis
|
|
30
|
+
globalThis.__catalystRenderRegistryError = (error, scope) => registryFailure(error, { scope, cwd: appRoot })
|
|
31
|
+
|
|
16
32
|
// react-router is a peer dependency: fail loudly at startup rather than on a
|
|
17
33
|
// missing export deep inside a render. Resolve from the app root, not from
|
|
18
34
|
// catalyst-core's own tree — the app's copy is the one vite and SSR load.
|
|
@@ -22,9 +38,9 @@ let reactRouterVersion
|
|
|
22
38
|
try {
|
|
23
39
|
reactRouterVersion = appRequire("react-router/package.json").version
|
|
24
40
|
} catch {
|
|
25
|
-
|
|
26
|
-
`catalyst-core
|
|
27
|
-
|
|
41
|
+
failStartup(
|
|
42
|
+
`catalyst-core needs react-router ${REQUIRED_REACT_ROUTER}, and it is not installed`,
|
|
43
|
+
`Install it with \`npm install react-router@${REQUIRED_REACT_ROUTER}\`.`
|
|
28
44
|
)
|
|
29
45
|
}
|
|
30
46
|
const [major, minor, patch] = reactRouterVersion.split("-")[0].split(".").map(Number)
|
|
@@ -33,9 +49,9 @@ const isValid =
|
|
|
33
49
|
major === 7 &&
|
|
34
50
|
(minor > 18 || (minor === 18 && patch >= 2))
|
|
35
51
|
if (!isValid) {
|
|
36
|
-
|
|
37
|
-
`catalyst-core
|
|
38
|
-
|
|
52
|
+
failStartup(
|
|
53
|
+
`catalyst-core needs react-router ${REQUIRED_REACT_ROUTER}, but the app has ${reactRouterVersion}`,
|
|
54
|
+
`Upgrade it with \`npm install react-router@${REQUIRED_REACT_ROUTER}\`.`
|
|
39
55
|
)
|
|
40
56
|
}
|
|
41
57
|
|
|
@@ -46,7 +62,13 @@ try {
|
|
|
46
62
|
} catch {
|
|
47
63
|
// No hooks file — preServerInit remains undefined
|
|
48
64
|
}
|
|
49
|
-
|
|
65
|
+
try {
|
|
66
|
+
await loadEnvironmentVariables()
|
|
67
|
+
} catch {
|
|
68
|
+
// loadEnvironmentVariables already printed an actionable message. Letting
|
|
69
|
+
// this propagate would print the raw stack again as an unhandled rejection.
|
|
70
|
+
process.exit(1)
|
|
71
|
+
}
|
|
50
72
|
|
|
51
73
|
// preServerInit is an OPTIONAL hook — an app with no server/index.js is
|
|
52
74
|
// valid, so PREFLIGHT-010 (missing) is deliberately not surfaced here. But
|