create-catalyst-app 0.3.0-beta.5 → 0.3.0-canary.1
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/codemod/new-route/index.js +1 -1
- package/codemod/new-route/template.js +1 -1
- package/codemod/new-route/transformers/createReducer.js +1 -1
- package/codemod/new-route/transformers/createRoute.js +1 -1
- package/codemod/new-route/utils.js +1 -1
- package/package.json +15 -5
- package/scripts/cli.cjs +33 -14
- package/scripts/errors.cjs +307 -0
- package/scripts/errors.d.cts +55 -0
- package/scripts/generateDocs.cjs +78 -0
- package/templates/none-js/client/index.js +2 -1
- package/templates/none-js/package.json +3 -2
- package/templates/none-js/src/js/containers/App/index.js +1 -1
- package/templates/none-ts/client/index.js +2 -1
- package/templates/none-ts/package.json +3 -2
- package/templates/none-ts/src/js/containers/App/index.js +1 -1
- package/templates/redux-js/client/index.js +2 -1
- package/templates/redux-js/package.json +2 -1
- package/templates/redux-js/src/js/containers/App/index.js +1 -1
- package/templates/redux-ts/client/index.js +2 -1
- package/templates/redux-ts/package.json +2 -1
- package/templates/redux-ts/src/js/containers/App/index.js +1 -1
- package/templates/rtk-js/client/index.js +2 -1
- package/templates/rtk-js/package.json +2 -1
- package/templates/rtk-js/src/js/containers/App/index.js +1 -1
- package/templates/rtk-ts/client/index.js +2 -1
- package/templates/rtk-ts/package.json +2 -1
- package/templates/rtk-ts/src/js/containers/App/index.js +1 -1
- package/test/errors.test.ts +219 -0
- package/tsconfig.json +23 -0
- package/vitest.config.mjs +24 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const fs = require("fs")
|
|
2
2
|
const path = require("path")
|
|
3
|
-
const pc = require("
|
|
3
|
+
const pc = require("ansis")
|
|
4
4
|
const { execFileSync } = require("child_process")
|
|
5
5
|
|
|
6
6
|
const executable = (command) => (process.platform === "win32" ? `${command}.cmd` : command)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-catalyst-app",
|
|
3
3
|
"bin": "scripts/cli.cjs",
|
|
4
|
-
"version": "0.3.0-
|
|
4
|
+
"version": "0.3.0-canary.1",
|
|
5
5
|
"description": "cli package to scaffold Catalyst application",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -9,20 +9,30 @@
|
|
|
9
9
|
"directory": "packages/create-catalyst-app"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
|
-
"prettify": "prettier . --write"
|
|
12
|
+
"prettify": "prettier . --write",
|
|
13
|
+
"test": "npm run test:types && vitest run",
|
|
14
|
+
"test:watch": "vitest",
|
|
15
|
+
"test:types": "tsc --noEmit"
|
|
13
16
|
},
|
|
14
17
|
"dependencies": {
|
|
18
|
+
"ansis": "^4.3.1",
|
|
15
19
|
"commander": "^8.2.0",
|
|
16
20
|
"fs": "^0.0.1-security",
|
|
17
21
|
"https": "^1.0.0",
|
|
18
22
|
"jscodeshift": "^0.16.0",
|
|
19
|
-
"picocolors": "^0.1.0",
|
|
20
23
|
"prompts": "^2.4.0",
|
|
21
24
|
"tar": "^7.5.22",
|
|
22
25
|
"validate-npm-package-name": "^5.0.0"
|
|
23
26
|
},
|
|
24
27
|
"devDependencies": {
|
|
25
|
-
"
|
|
28
|
+
"@types/node": "^22.20.1",
|
|
29
|
+
"@vitest/coverage-v8": "4.1.9",
|
|
30
|
+
"prettier": "^3.2.5",
|
|
31
|
+
"typescript": "^7.0.2",
|
|
32
|
+
"vitest": "4.1.9"
|
|
26
33
|
},
|
|
27
|
-
"license": "MIT"
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"overrides": {
|
|
36
|
+
"vitest": "4.1.9"
|
|
37
|
+
}
|
|
28
38
|
}
|
package/scripts/cli.cjs
CHANGED
|
@@ -3,12 +3,21 @@ const { execFileSync, execSync } = require("child_process")
|
|
|
3
3
|
const Commander = require("commander")
|
|
4
4
|
const { Option } = require("commander")
|
|
5
5
|
const prompts = require("prompts")
|
|
6
|
-
const { red, green, cyan, bold } = require("
|
|
6
|
+
const { red, green, cyan, bold } = require("ansis")
|
|
7
7
|
const tar = require("tar")
|
|
8
8
|
const path = require("path")
|
|
9
9
|
const fs = require("fs")
|
|
10
10
|
var validate = require("validate-npm-package-name")
|
|
11
11
|
const packageJson = require("../package.json")
|
|
12
|
+
const { CCAError, createError, wrapForeignError, formatError, resolveOutputMode, getDebugEnvInfo } = require("./errors.cjs")
|
|
13
|
+
const outputMode = resolveOutputMode(process.argv)
|
|
14
|
+
// Colored (red) prefix only applies in default mode — verbose/debug boxes
|
|
15
|
+
// own their coloring internally (see errors.cjs#box).
|
|
16
|
+
const printError = (err) => {
|
|
17
|
+
const debugEnv = outputMode === "debug" ? getDebugEnvInfo() : undefined
|
|
18
|
+
const formatted = formatError(err, outputMode, debugEnv)
|
|
19
|
+
console.error(outputMode === "default" ? red(formatted) : formatted)
|
|
20
|
+
}
|
|
12
21
|
const packageRoot = path.join(__dirname, "..")
|
|
13
22
|
const executable = (command) => (process.platform === "win32" ? `${command}.cmd` : command)
|
|
14
23
|
|
|
@@ -65,13 +74,21 @@ const program = new Commander.Command()
|
|
|
65
74
|
const projectName = config.folderName || (await promptProjectName())
|
|
66
75
|
let isNameValid = validate(projectName)
|
|
67
76
|
if (!isNameValid.validForNewPackages) {
|
|
68
|
-
isNameValid?.warnings
|
|
69
|
-
|
|
77
|
+
const reasons = [...(isNameValid?.warnings || []), ...(isNameValid?.errors || [])]
|
|
78
|
+
printError(
|
|
79
|
+
createError("CCA-001", {
|
|
80
|
+
details: reasons.length ? reasons.join("; ") : undefined,
|
|
81
|
+
})
|
|
82
|
+
)
|
|
70
83
|
process.exit(1)
|
|
71
84
|
}
|
|
72
85
|
let projectPath = path.join(process.cwd(), projectName)
|
|
73
86
|
if (fs.existsSync(projectPath)) {
|
|
74
|
-
|
|
87
|
+
printError(
|
|
88
|
+
createError("CCA-002", {
|
|
89
|
+
message: `${projectName} already exists, try again.`,
|
|
90
|
+
})
|
|
91
|
+
)
|
|
75
92
|
process.exit(1)
|
|
76
93
|
}
|
|
77
94
|
const projectDescription = config.description || (await promptDescription())
|
|
@@ -153,7 +170,8 @@ const program = new Commander.Command()
|
|
|
153
170
|
runMcpSetup(newMcpDir, path.join(process.cwd(), projectName))
|
|
154
171
|
}
|
|
155
172
|
} catch (error) {
|
|
156
|
-
|
|
173
|
+
const catalystError = error instanceof CCAError ? error : wrapForeignError(error)
|
|
174
|
+
printError(catalystError)
|
|
157
175
|
process.exit(1)
|
|
158
176
|
} finally {
|
|
159
177
|
deleteDirectory(tempDir)
|
|
@@ -184,8 +202,7 @@ const program = new Commander.Command()
|
|
|
184
202
|
|
|
185
203
|
return tarballFilePath
|
|
186
204
|
} catch (error) {
|
|
187
|
-
|
|
188
|
-
throw error
|
|
205
|
+
throw createError("CCA-006", { cause: error })
|
|
189
206
|
}
|
|
190
207
|
}
|
|
191
208
|
|
|
@@ -219,13 +236,14 @@ const program = new Commander.Command()
|
|
|
219
236
|
},
|
|
220
237
|
})
|
|
221
238
|
} catch (e) {
|
|
222
|
-
|
|
239
|
+
throw createError("CCA-007", { cause: e })
|
|
223
240
|
}
|
|
224
241
|
|
|
225
242
|
console.log(cyan(`Run cd ${projectName} && npm start to get started.`))
|
|
226
243
|
}
|
|
227
244
|
} catch (error) {
|
|
228
|
-
|
|
245
|
+
const catalystError = error instanceof CCAError ? error : wrapForeignError(error)
|
|
246
|
+
printError(catalystError)
|
|
229
247
|
process.exit(1)
|
|
230
248
|
}
|
|
231
249
|
})
|
|
@@ -260,7 +278,8 @@ program
|
|
|
260
278
|
|
|
261
279
|
runMcpSetup(mcpDir)
|
|
262
280
|
} catch (error) {
|
|
263
|
-
|
|
281
|
+
const catalystError = error instanceof CCAError ? error : createError("CCA-008", { cause: error })
|
|
282
|
+
printError(catalystError)
|
|
264
283
|
process.exit(1)
|
|
265
284
|
}
|
|
266
285
|
})
|
|
@@ -388,16 +407,16 @@ async function promptMcp() {
|
|
|
388
407
|
function validateOptions(cmd) {
|
|
389
408
|
// Validate language option
|
|
390
409
|
if (cmd.lang && !["js", "ts"].includes(cmd.lang.toLowerCase())) {
|
|
391
|
-
throw
|
|
410
|
+
throw createError("CCA-003")
|
|
392
411
|
}
|
|
393
412
|
|
|
394
413
|
// Validate state management option
|
|
395
414
|
if (cmd.stateManagement && !["rtk", "redux", "none"].includes(cmd.stateManagement.toLowerCase())) {
|
|
396
|
-
throw
|
|
415
|
+
throw createError("CCA-004")
|
|
397
416
|
}
|
|
398
417
|
|
|
399
418
|
if (cmd.yes && typeof cmd.yes !== "boolean") {
|
|
400
|
-
throw
|
|
419
|
+
throw createError("CCA-005")
|
|
401
420
|
}
|
|
402
421
|
}
|
|
403
422
|
|
|
@@ -422,7 +441,7 @@ function createGitignore(projectName) {
|
|
|
422
441
|
const gitignorePath = `${process.cwd()}${path.sep}${projectName}${path.sep}.gitignore`
|
|
423
442
|
|
|
424
443
|
if (fs.existsSync(gitignorePath)) {
|
|
425
|
-
|
|
444
|
+
printError(createError("CCA-009"))
|
|
426
445
|
return
|
|
427
446
|
}
|
|
428
447
|
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
const pc = require("ansis")
|
|
2
|
+
|
|
3
|
+
const REPO_BLOB_BASE = "https://github.com/tata1mg/catalyst-core/blob/main/errors"
|
|
4
|
+
|
|
5
|
+
function docUrl(category, code) {
|
|
6
|
+
return `${REPO_BLOB_BASE}/${category}/${code}.md`
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const ERROR_CODES = {
|
|
10
|
+
CCA_UPSTREAM_ERROR: "CCA-000",
|
|
11
|
+
CCA_INVALID_NAME: "CCA-001",
|
|
12
|
+
CCA_DIRECTORY_EXISTS: "CCA-002",
|
|
13
|
+
CCA_INVALID_LANGUAGE_OPTION: "CCA-003",
|
|
14
|
+
CCA_INVALID_STATE_MANAGEMENT_OPTION: "CCA-004",
|
|
15
|
+
CCA_INVALID_YES_OPTION: "CCA-005",
|
|
16
|
+
CCA_PACK_FAILED: "CCA-006",
|
|
17
|
+
CCA_EXTRACTION_FAILED: "CCA-007",
|
|
18
|
+
CCA_MCP_SETUP_FAILED: "CCA-008",
|
|
19
|
+
CCA_GITIGNORE_EXISTS: "CCA-009",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// One entry per CCA-owned code, including the CCA-000 wrapper used by
|
|
23
|
+
// wrapForeignError. Foreign errors themselves are never reinterpreted —
|
|
24
|
+
// their original message/code always surfaces via `cause`.
|
|
25
|
+
const ERROR_DEFINITIONS = {
|
|
26
|
+
[ERROR_CODES.CCA_UPSTREAM_ERROR]: {
|
|
27
|
+
category: "CCA",
|
|
28
|
+
defaultMessage: "An upstream command failed",
|
|
29
|
+
defaultDetails:
|
|
30
|
+
"This wraps an error from npm, tar, or another upstream tool invoked by create-catalyst-app. See the printed upstream message for the actual cause.",
|
|
31
|
+
suggestedAction: "Read the upstream error message printed above and fix the underlying issue.",
|
|
32
|
+
},
|
|
33
|
+
[ERROR_CODES.CCA_INVALID_NAME]: {
|
|
34
|
+
category: "CCA",
|
|
35
|
+
defaultMessage: "Invalid project name",
|
|
36
|
+
defaultDetails: "The project name is not a valid npm package name.",
|
|
37
|
+
suggestedAction: "Choose a project name that is a valid npm package name and try again.",
|
|
38
|
+
},
|
|
39
|
+
[ERROR_CODES.CCA_DIRECTORY_EXISTS]: {
|
|
40
|
+
category: "CCA",
|
|
41
|
+
defaultMessage: "Target directory already exists",
|
|
42
|
+
defaultDetails: "A file or directory with the project name already exists in the current directory.",
|
|
43
|
+
suggestedAction: "Choose a different project name, or remove/rename the existing directory.",
|
|
44
|
+
},
|
|
45
|
+
[ERROR_CODES.CCA_INVALID_LANGUAGE_OPTION]: {
|
|
46
|
+
category: "CCA",
|
|
47
|
+
defaultMessage: "Invalid language option",
|
|
48
|
+
defaultDetails: 'The --lang option must be "js" or "ts".',
|
|
49
|
+
suggestedAction: 'Pass --lang js or --lang ts.',
|
|
50
|
+
},
|
|
51
|
+
[ERROR_CODES.CCA_INVALID_STATE_MANAGEMENT_OPTION]: {
|
|
52
|
+
category: "CCA",
|
|
53
|
+
defaultMessage: "Invalid state management option",
|
|
54
|
+
defaultDetails: 'The --state-management option must be "rtk", "redux", or "none".',
|
|
55
|
+
suggestedAction: "Pass a supported --state-management value.",
|
|
56
|
+
},
|
|
57
|
+
[ERROR_CODES.CCA_INVALID_YES_OPTION]: {
|
|
58
|
+
category: "CCA",
|
|
59
|
+
defaultMessage: "Invalid --yes option",
|
|
60
|
+
defaultDetails: "The -y/--yes flag does not accept a non-boolean value.",
|
|
61
|
+
suggestedAction: "Use -y or --yes with no value to accept defaults.",
|
|
62
|
+
},
|
|
63
|
+
[ERROR_CODES.CCA_PACK_FAILED]: {
|
|
64
|
+
category: "CCA",
|
|
65
|
+
defaultMessage: "Failed to pack create-catalyst-app",
|
|
66
|
+
defaultDetails: "npm pack could not produce the tarball used to scaffold the new project.",
|
|
67
|
+
suggestedAction: "Check your npm registry access/network connection and try again.",
|
|
68
|
+
},
|
|
69
|
+
[ERROR_CODES.CCA_EXTRACTION_FAILED]: {
|
|
70
|
+
category: "CCA",
|
|
71
|
+
defaultMessage: "Failed to extract template files",
|
|
72
|
+
defaultDetails: "The packed tarball could not be extracted into the new project directory.",
|
|
73
|
+
suggestedAction: "Ensure the target directory is writable and try again.",
|
|
74
|
+
},
|
|
75
|
+
[ERROR_CODES.CCA_MCP_SETUP_FAILED]: {
|
|
76
|
+
category: "CCA",
|
|
77
|
+
defaultMessage: "MCP server setup failed",
|
|
78
|
+
defaultDetails: "Downloading, installing, or configuring the MCP server failed.",
|
|
79
|
+
suggestedAction: "Check your network connection and re-run `catalyst-mcp` inside the project.",
|
|
80
|
+
},
|
|
81
|
+
[ERROR_CODES.CCA_GITIGNORE_EXISTS]: {
|
|
82
|
+
category: "CCA",
|
|
83
|
+
defaultMessage: ".gitignore already exists",
|
|
84
|
+
defaultDetails: "The scaffolded project already contains a .gitignore file.",
|
|
85
|
+
suggestedAction: "Remove or rename the existing .gitignore before running again, or ignore this warning.",
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getDefinition(code) {
|
|
90
|
+
return ERROR_DEFINITIONS[code] || ERROR_DEFINITIONS[ERROR_CODES.CCA_UPSTREAM_ERROR]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function getDocUrl(code) {
|
|
94
|
+
const def = ERROR_DEFINITIONS[code]
|
|
95
|
+
if (!def) return docUrl(getDefinition(code).category, ERROR_CODES.CCA_UPSTREAM_ERROR)
|
|
96
|
+
return docUrl(def.category, code)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
class CCAError extends Error {
|
|
100
|
+
constructor(code, { message, details, suggestedAction, category, docUrl, cause } = {}) {
|
|
101
|
+
super(message)
|
|
102
|
+
this.name = "CCAError"
|
|
103
|
+
this.code = code
|
|
104
|
+
this.category = category
|
|
105
|
+
this.details = details
|
|
106
|
+
this.suggestedAction = suggestedAction
|
|
107
|
+
this.docUrl = docUrl
|
|
108
|
+
if (cause !== undefined) this.cause = cause
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Create a CCAError for a code we own, using the registry defaults
|
|
114
|
+
* unless overridden.
|
|
115
|
+
*/
|
|
116
|
+
function createError(code, overrides = {}) {
|
|
117
|
+
const def = getDefinition(code)
|
|
118
|
+
return new CCAError(code, {
|
|
119
|
+
category: def.category,
|
|
120
|
+
message: overrides.message || def.defaultMessage,
|
|
121
|
+
details: overrides.details || def.defaultDetails,
|
|
122
|
+
suggestedAction: overrides.suggestedAction || def.suggestedAction,
|
|
123
|
+
docUrl: getDocUrl(code),
|
|
124
|
+
cause: overrides.cause,
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Wrap a foreign error (npm/tar/git/network) that already carries its own
|
|
130
|
+
* message. We do not replace or reinterpret it — we attach the generic
|
|
131
|
+
* CCA-000 wrapper code and preserve the original as `cause`, so the
|
|
132
|
+
* upstream message is always what gets shown to the user.
|
|
133
|
+
*/
|
|
134
|
+
function wrapForeignError(err) {
|
|
135
|
+
// Unlike catalyst-core's wrapForeignError (BUNDLE/IOS/ANDROID, each with a
|
|
136
|
+
// fixed upstream tool name), CCA wraps arbitrary tools (npm/tar/git/network)
|
|
137
|
+
// with no single name to always show — so the "(upstream: ...)" suffix is
|
|
138
|
+
// omitted entirely when there's no named code, rather than left dangling.
|
|
139
|
+
// Trimmed and checked for emptiness too — a blank/whitespace-only .code
|
|
140
|
+
// (still typeof "string") must not produce a dangling "(upstream: )".
|
|
141
|
+
const upstreamCode = err && typeof err.code === "string" ? err.code.trim() : ""
|
|
142
|
+
const message = upstreamCode ? `An upstream command failed (upstream: ${upstreamCode})` : "An upstream command failed"
|
|
143
|
+
return createError(ERROR_CODES.CCA_UPSTREAM_ERROR, {
|
|
144
|
+
message,
|
|
145
|
+
cause: err,
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function formatDefault(err) {
|
|
150
|
+
const lines = [`[${err.code}] ${err.message}`]
|
|
151
|
+
if (err.cause) {
|
|
152
|
+
const causeMessage = err.cause.message || String(err.cause)
|
|
153
|
+
lines.push(`→ ${causeMessage}`)
|
|
154
|
+
} else if (err.details) {
|
|
155
|
+
lines.push(err.details)
|
|
156
|
+
}
|
|
157
|
+
if (err.suggestedAction) {
|
|
158
|
+
lines.push(`Suggested action: ${err.suggestedAction}`)
|
|
159
|
+
}
|
|
160
|
+
if (err.docUrl) {
|
|
161
|
+
lines.push(`Docs: ${err.docUrl}`)
|
|
162
|
+
}
|
|
163
|
+
return lines.join("\n")
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const BOX_WIDTH = 70
|
|
167
|
+
|
|
168
|
+
function box(bodyLines) {
|
|
169
|
+
const top = pc.red(`┌${"─".repeat(BOX_WIDTH)}`)
|
|
170
|
+
const bottom = pc.red(`└${"─".repeat(BOX_WIDTH)}`)
|
|
171
|
+
const body = bodyLines.map((line) => (line === "" ? pc.red("│") : `${pc.red("│")} ${line}`))
|
|
172
|
+
return [top, ...body, bottom].join("\n")
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function formatCauseChain(cause) {
|
|
176
|
+
const lines = []
|
|
177
|
+
let current = cause
|
|
178
|
+
let depth = 0
|
|
179
|
+
while (current) {
|
|
180
|
+
const prefix = depth === 0 ? "Caused by:" : ` ${" ".repeat(depth - 1)}Caused by:`
|
|
181
|
+
lines.push(`${prefix} ${current.message || String(current)}`)
|
|
182
|
+
current = current.cause
|
|
183
|
+
depth += 1
|
|
184
|
+
}
|
|
185
|
+
return lines
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Verbose output: boxed per RFC #155 §3.2 — code, category, timestamp,
|
|
190
|
+
* problem, suggested action, docs link. Mirrors catalyst-core's
|
|
191
|
+
* errors/index.js#formatVerbose (kept as a separate CJS implementation
|
|
192
|
+
* since this package can't depend on catalyst-core's ESM module).
|
|
193
|
+
*/
|
|
194
|
+
function formatVerbose(err) {
|
|
195
|
+
const lines = [
|
|
196
|
+
pc.bold(`❌ ${err.message}`),
|
|
197
|
+
`Code: ${err.code}`,
|
|
198
|
+
`Category: ${err.category || "UNKNOWN"}`,
|
|
199
|
+
`Time: ${new Date().toISOString()}`,
|
|
200
|
+
"",
|
|
201
|
+
]
|
|
202
|
+
if (err.cause) {
|
|
203
|
+
lines.push(`Problem: ${err.cause.message || String(err.cause)}`)
|
|
204
|
+
lines.push("")
|
|
205
|
+
} else if (err.details) {
|
|
206
|
+
lines.push(`Problem: ${err.details}`)
|
|
207
|
+
lines.push("")
|
|
208
|
+
}
|
|
209
|
+
if (err.suggestedAction) {
|
|
210
|
+
lines.push("Solution:", ` ${err.suggestedAction}`, "")
|
|
211
|
+
}
|
|
212
|
+
if (err.docUrl) {
|
|
213
|
+
lines.push(`Docs: ${err.docUrl}`)
|
|
214
|
+
}
|
|
215
|
+
return box(lines)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Debug output: verbose + full cause chain + stack trace + environment
|
|
220
|
+
* info. Mirrors catalyst-core's errors/index.js#formatDebug.
|
|
221
|
+
*/
|
|
222
|
+
function formatDebug(err, env) {
|
|
223
|
+
const lines = [
|
|
224
|
+
pc.bold(`❌ ${err.message}`),
|
|
225
|
+
`Code: ${err.code}`,
|
|
226
|
+
`Category: ${err.category || "UNKNOWN"}`,
|
|
227
|
+
`Time: ${new Date().toISOString()}`,
|
|
228
|
+
"",
|
|
229
|
+
]
|
|
230
|
+
if (err.cause) {
|
|
231
|
+
lines.push(...formatCauseChain(err.cause), "")
|
|
232
|
+
} else if (err.details) {
|
|
233
|
+
lines.push(`Problem: ${err.details}`, "")
|
|
234
|
+
}
|
|
235
|
+
if (err.suggestedAction) {
|
|
236
|
+
lines.push("Solution:", ` ${err.suggestedAction}`, "")
|
|
237
|
+
}
|
|
238
|
+
if (err.docUrl) {
|
|
239
|
+
lines.push(`Docs: ${err.docUrl}`, "")
|
|
240
|
+
}
|
|
241
|
+
if (env) {
|
|
242
|
+
lines.push("Environment:")
|
|
243
|
+
for (const [key, value] of Object.entries(env)) {
|
|
244
|
+
lines.push(` ${key}: ${value}`)
|
|
245
|
+
}
|
|
246
|
+
lines.push("")
|
|
247
|
+
}
|
|
248
|
+
lines.push("Stack trace:")
|
|
249
|
+
const stack = (err.cause && err.cause.stack) || err.stack || "(no stack available)"
|
|
250
|
+
lines.push(...stack.split("\n").map((l) => ` ${l}`))
|
|
251
|
+
return box(lines)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Format a CCAError for terminal output. Foreign-wrapped errors always show
|
|
256
|
+
* the original upstream message/code, never a paraphrase of it.
|
|
257
|
+
*
|
|
258
|
+
* `mode` mirrors catalyst-core's formatError(err, mode, env) signature —
|
|
259
|
+
* defaults to "default" so existing single-arg call sites are unaffected.
|
|
260
|
+
*/
|
|
261
|
+
function formatError(err, mode = "default", env) {
|
|
262
|
+
if (mode === "debug") return formatDebug(err, env)
|
|
263
|
+
if (mode === "verbose") return formatVerbose(err)
|
|
264
|
+
return formatDefault(err)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Resolve output mode from CLI args, mirroring catalyst-core's
|
|
269
|
+
* scripts/scriptUtils.js#resolveOutputMode (bare --verbose/--debug flags,
|
|
270
|
+
* not key=value pairs).
|
|
271
|
+
*/
|
|
272
|
+
function resolveOutputMode(argv) {
|
|
273
|
+
const args = argv || process.argv
|
|
274
|
+
if (args.includes("--debug")) return "debug"
|
|
275
|
+
if (args.includes("--verbose")) return "verbose"
|
|
276
|
+
return "default"
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Environment info for debug-mode output: CCA's own installed version, Node
|
|
281
|
+
* version, and platform. Mirrors catalyst-core's scriptUtils.js#getDebugEnvInfo.
|
|
282
|
+
*/
|
|
283
|
+
function getDebugEnvInfo() {
|
|
284
|
+
let ccaVersion = "unknown"
|
|
285
|
+
try {
|
|
286
|
+
ccaVersion = require("../package.json").version
|
|
287
|
+
} catch {
|
|
288
|
+
// package.json not resolvable — leave as "unknown"
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
node: process.version,
|
|
292
|
+
platform: process.platform,
|
|
293
|
+
createCatalystApp: ccaVersion,
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
module.exports = {
|
|
298
|
+
ERROR_CODES,
|
|
299
|
+
ERROR_DEFINITIONS,
|
|
300
|
+
CCAError,
|
|
301
|
+
createError,
|
|
302
|
+
wrapForeignError,
|
|
303
|
+
formatError,
|
|
304
|
+
resolveOutputMode,
|
|
305
|
+
getDebugEnvInfo,
|
|
306
|
+
getDocUrl,
|
|
307
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Hand-written type declaration for errors.cjs — that file stays plain CJS
|
|
2
|
+
// until a future source conversion (see issue #420); this exists only so
|
|
3
|
+
// require("./errors.cjs") gets real types in test/errors.test.cts instead of
|
|
4
|
+
// `unknown`. Keep in sync with errors.cjs's actual shape by hand for now.
|
|
5
|
+
|
|
6
|
+
export interface ErrorDefinition {
|
|
7
|
+
category: string
|
|
8
|
+
defaultMessage: string
|
|
9
|
+
defaultDetails: string
|
|
10
|
+
suggestedAction: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type ErrorCodes = Record<string, string>
|
|
14
|
+
export type ErrorDefinitions = Record<string, ErrorDefinition>
|
|
15
|
+
|
|
16
|
+
export interface CCAErrorOverrides {
|
|
17
|
+
message?: string
|
|
18
|
+
details?: string
|
|
19
|
+
suggestedAction?: string
|
|
20
|
+
category?: string
|
|
21
|
+
docUrl?: string
|
|
22
|
+
cause?: unknown
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class CCAError extends Error {
|
|
26
|
+
code: string
|
|
27
|
+
category?: string
|
|
28
|
+
details?: string
|
|
29
|
+
suggestedAction?: string
|
|
30
|
+
docUrl?: string
|
|
31
|
+
cause?: unknown
|
|
32
|
+
constructor(code: string, overrides?: CCAErrorOverrides)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type OutputMode = "default" | "verbose" | "debug"
|
|
36
|
+
|
|
37
|
+
// formatError's debug mode just does Object.entries(env) and prints each
|
|
38
|
+
// key/value — getDebugEnvInfo() always returns all three fields, but any
|
|
39
|
+
// caller (including tests) may legitimately pass a partial record.
|
|
40
|
+
export type DebugEnvInfo = Partial<{
|
|
41
|
+
node: string
|
|
42
|
+
platform: string
|
|
43
|
+
createCatalystApp: string
|
|
44
|
+
}> &
|
|
45
|
+
Record<string, string>
|
|
46
|
+
|
|
47
|
+
export const ERROR_CODES: ErrorCodes
|
|
48
|
+
export const ERROR_DEFINITIONS: ErrorDefinitions
|
|
49
|
+
|
|
50
|
+
export function createError(code: string, overrides?: CCAErrorOverrides): CCAError
|
|
51
|
+
export function wrapForeignError(err: unknown): CCAError
|
|
52
|
+
export function formatError(err: CCAError, mode?: OutputMode, env?: DebugEnvInfo): string
|
|
53
|
+
export function resolveOutputMode(argv?: string[]): OutputMode
|
|
54
|
+
export function getDebugEnvInfo(): DebugEnvInfo
|
|
55
|
+
export function getDocUrl(code: string): string | null
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const { mkdirSync, writeFileSync, readFileSync, readdirSync, rmSync, existsSync } = require("fs")
|
|
2
|
+
const path = require("path")
|
|
3
|
+
const { ERROR_DEFINITIONS } = require("./errors.cjs")
|
|
4
|
+
|
|
5
|
+
// packages/create-catalyst-app/scripts -> repo root /errors
|
|
6
|
+
const ERRORS_DIR = path.resolve(__dirname, "../../../errors")
|
|
7
|
+
const INDEX_PATH = path.join(ERRORS_DIR, "index.json")
|
|
8
|
+
|
|
9
|
+
function renderMarkdown(code, def) {
|
|
10
|
+
return `# ${code}
|
|
11
|
+
|
|
12
|
+
**Category:** ${def.category}
|
|
13
|
+
|
|
14
|
+
## Message
|
|
15
|
+
|
|
16
|
+
${def.defaultMessage}
|
|
17
|
+
|
|
18
|
+
## Details
|
|
19
|
+
|
|
20
|
+
${def.defaultDetails}
|
|
21
|
+
|
|
22
|
+
## Suggested action
|
|
23
|
+
|
|
24
|
+
${def.suggestedAction}
|
|
25
|
+
`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function clearGeneratedCategoryDirs() {
|
|
29
|
+
const dir = path.join(ERRORS_DIR, "CCA")
|
|
30
|
+
if (!existsSync(dir)) return
|
|
31
|
+
for (const file of readdirSync(dir)) {
|
|
32
|
+
if (file.endsWith(".md")) rmSync(path.join(dir, file))
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function readExistingIndex() {
|
|
37
|
+
if (!existsSync(INDEX_PATH)) return {}
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(readFileSync(INDEX_PATH, "utf8"))
|
|
40
|
+
} catch {
|
|
41
|
+
return {}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function generateDocs() {
|
|
46
|
+
clearGeneratedCategoryDirs()
|
|
47
|
+
|
|
48
|
+
// Merge into the shared errors/index.json rather than overwriting it —
|
|
49
|
+
// catalyst-core's own generateDocs.js owns every other category.
|
|
50
|
+
const index = readExistingIndex()
|
|
51
|
+
|
|
52
|
+
for (const [code, def] of Object.entries(ERROR_DEFINITIONS)) {
|
|
53
|
+
const dir = path.join(ERRORS_DIR, def.category)
|
|
54
|
+
mkdirSync(dir, { recursive: true })
|
|
55
|
+
const filePath = path.join(dir, `${code}.md`)
|
|
56
|
+
writeFileSync(filePath, renderMarkdown(code, def))
|
|
57
|
+
|
|
58
|
+
index[code] = {
|
|
59
|
+
category: def.category,
|
|
60
|
+
message: def.defaultMessage,
|
|
61
|
+
details: def.defaultDetails,
|
|
62
|
+
suggestedAction: def.suggestedAction,
|
|
63
|
+
docUrl: `https://github.com/tata1mg/catalyst-core/blob/main/errors/${def.category}/${code}.md`,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
mkdirSync(ERRORS_DIR, { recursive: true })
|
|
68
|
+
writeFileSync(INDEX_PATH, JSON.stringify(index, null, 4))
|
|
69
|
+
|
|
70
|
+
return { count: Object.keys(ERROR_DEFINITIONS).length, errorsDir: ERRORS_DIR }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (require.main === module) {
|
|
74
|
+
const { count, errorsDir } = generateDocs()
|
|
75
|
+
console.log(`Generated ${count} error doc(s) and merged into index.json in ${errorsDir}`)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = { generateDocs }
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import React from "react"
|
|
2
2
|
import "./styles"
|
|
3
3
|
import { hydrateRoot } from "react-dom/client"
|
|
4
|
-
import {
|
|
4
|
+
import { hydrationReady } from "catalyst-core"
|
|
5
|
+
import { RouterProvider } from "react-router/dom"
|
|
5
6
|
import clientRouter from "catalyst-core/router/ClientRouter"
|
|
6
7
|
|
|
7
8
|
window.addEventListener("load", () => {
|
|
@@ -22,9 +22,10 @@
|
|
|
22
22
|
"@routes": "src/js/routes/"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"catalyst-core": "0.3.0-
|
|
25
|
+
"catalyst-core": "0.3.0-canary.1",
|
|
26
26
|
"react": "19.0.0",
|
|
27
|
-
"react-dom": "19.0.0"
|
|
27
|
+
"react-dom": "19.0.0",
|
|
28
|
+
"react-router": "^7.18.2"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
30
31
|
"eslint": "^8.26.0",
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import React from "react"
|
|
2
2
|
import "./styles"
|
|
3
3
|
import { hydrateRoot } from "react-dom/client"
|
|
4
|
-
import {
|
|
4
|
+
import { hydrationReady } from "catalyst-core"
|
|
5
|
+
import { RouterProvider } from "react-router/dom"
|
|
5
6
|
import clientRouter from "catalyst-core/router/ClientRouter"
|
|
6
7
|
|
|
7
8
|
window.addEventListener("load", () => {
|
|
@@ -23,9 +23,10 @@
|
|
|
23
23
|
"@routes": "src/js/routes/"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"catalyst-core": "0.3.0-
|
|
26
|
+
"catalyst-core": "0.3.0-canary.1",
|
|
27
27
|
"react": "19.0.0",
|
|
28
|
-
"react-dom": "19.0.0"
|
|
28
|
+
"react-dom": "19.0.0",
|
|
29
|
+
"react-router": "^7.18.2"
|
|
29
30
|
},
|
|
30
31
|
"devDependencies": {
|
|
31
32
|
"eslint": "^8.26.0",
|
|
@@ -2,7 +2,8 @@ import React from "react"
|
|
|
2
2
|
import "./styles"
|
|
3
3
|
import { hydrateRoot } from "react-dom/client"
|
|
4
4
|
import { Provider } from "react-redux"
|
|
5
|
-
import {
|
|
5
|
+
import { hydrationReady } from "catalyst-core"
|
|
6
|
+
import { RouterProvider } from "react-router/dom"
|
|
6
7
|
import clientRouter from "catalyst-core/router/ClientRouter"
|
|
7
8
|
import configureStore from "@store"
|
|
8
9
|
|
|
@@ -25,9 +25,10 @@
|
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@reduxjs/toolkit": "^2.9.0",
|
|
27
27
|
"react-redux": "^9.2.0",
|
|
28
|
-
"catalyst-core": "0.3.0-
|
|
28
|
+
"catalyst-core": "0.3.0-canary.1",
|
|
29
29
|
"react": "19.0.0",
|
|
30
30
|
"react-dom": "19.0.0",
|
|
31
|
+
"react-router": "^7.18.2",
|
|
31
32
|
"redux": "^5.0.1"
|
|
32
33
|
},
|
|
33
34
|
"devDependencies": {
|
|
@@ -2,7 +2,8 @@ import React from "react"
|
|
|
2
2
|
import "./styles"
|
|
3
3
|
import { hydrateRoot } from "react-dom/client"
|
|
4
4
|
import { Provider } from "react-redux"
|
|
5
|
-
import {
|
|
5
|
+
import { hydrationReady } from "catalyst-core"
|
|
6
|
+
import { RouterProvider } from "react-router/dom"
|
|
6
7
|
import clientRouter from "catalyst-core/router/ClientRouter"
|
|
7
8
|
import configureStore from "@store"
|
|
8
9
|
|
|
@@ -26,9 +26,10 @@
|
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@reduxjs/toolkit": "^2.9.0",
|
|
28
28
|
"react-redux": "^9.2.0",
|
|
29
|
-
"catalyst-core": "0.3.0-
|
|
29
|
+
"catalyst-core": "0.3.0-canary.1",
|
|
30
30
|
"react": "19.0.0",
|
|
31
31
|
"react-dom": "19.0.0",
|
|
32
|
+
"react-router": "^7.18.2",
|
|
32
33
|
"redux": "^5.0.1"
|
|
33
34
|
},
|
|
34
35
|
"devDependencies": {
|
|
@@ -2,7 +2,8 @@ import React from "react"
|
|
|
2
2
|
import "./styles"
|
|
3
3
|
import { hydrateRoot } from "react-dom/client"
|
|
4
4
|
import { Provider } from "react-redux"
|
|
5
|
-
import {
|
|
5
|
+
import { hydrationReady } from "catalyst-core"
|
|
6
|
+
import { RouterProvider } from "react-router/dom"
|
|
6
7
|
import clientRouter from "catalyst-core/router/ClientRouter"
|
|
7
8
|
import configureStore from "@store"
|
|
8
9
|
|
|
@@ -25,9 +25,10 @@
|
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@reduxjs/toolkit": "^2.9.0",
|
|
27
27
|
"react-redux": "^9.2.0",
|
|
28
|
-
"catalyst-core": "0.3.0-
|
|
28
|
+
"catalyst-core": "0.3.0-canary.1",
|
|
29
29
|
"react": "19.0.0",
|
|
30
30
|
"react-dom": "19.0.0",
|
|
31
|
+
"react-router": "^7.18.2",
|
|
31
32
|
"redux": "^5.0.1"
|
|
32
33
|
},
|
|
33
34
|
"devDependencies": {
|
|
@@ -2,7 +2,8 @@ import React from "react"
|
|
|
2
2
|
import "./styles"
|
|
3
3
|
import { hydrateRoot } from "react-dom/client"
|
|
4
4
|
import { Provider } from "react-redux"
|
|
5
|
-
import {
|
|
5
|
+
import { hydrationReady } from "catalyst-core"
|
|
6
|
+
import { RouterProvider } from "react-router/dom"
|
|
6
7
|
import clientRouter from "catalyst-core/router/ClientRouter"
|
|
7
8
|
import configureStore from "@store"
|
|
8
9
|
|
|
@@ -26,9 +26,10 @@
|
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@reduxjs/toolkit": "^2.9.0",
|
|
28
28
|
"react-redux": "^9.2.0",
|
|
29
|
-
"catalyst-core": "0.3.0-
|
|
29
|
+
"catalyst-core": "0.3.0-canary.1",
|
|
30
30
|
"react": "19.0.0",
|
|
31
31
|
"react-dom": "19.0.0",
|
|
32
|
+
"react-router": "^7.18.2",
|
|
32
33
|
"redux": "^5.0.1"
|
|
33
34
|
},
|
|
34
35
|
"devDependencies": {
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// describe/it/expect come from vitest's `globals: true` config (see
|
|
2
|
+
// vitest.config.mjs) — vitest's own exports can't be require()'d, only
|
|
3
|
+
// import'd, so globals injects them instead.
|
|
4
|
+
//
|
|
5
|
+
// errors.cjs itself is loaded via require() rather than import — it's a
|
|
6
|
+
// CommonJS module and this keeps the runtime behavior identical to how
|
|
7
|
+
// every other require("./errors.cjs") call site in this package loads it.
|
|
8
|
+
// require(...) is untyped by default, and errors.cjs stays plain JS until a
|
|
9
|
+
// future source conversion (see issue #420), so we annotate it against the
|
|
10
|
+
// hand-written declaration in ../scripts/errors.d.cts rather than let every
|
|
11
|
+
// usage below infer as `unknown`.
|
|
12
|
+
const cca: typeof import("../scripts/errors.cjs") = require("../scripts/errors.cjs")
|
|
13
|
+
|
|
14
|
+
// Framework-level (Tier 1) contract + parity tests for the CCA error mirror
|
|
15
|
+
// (packages/create-catalyst-app/scripts/errors.cjs). This is a standalone
|
|
16
|
+
// CJS module — CCA packs/extracts itself before catalyst-core is ever
|
|
17
|
+
// installed, so it can't import catalyst-core/errors and instead re-implements
|
|
18
|
+
// the same shape/conventions by hand (see decision #6 in the story-6
|
|
19
|
+
// checkpoint). These tests assert it stays structurally in sync with those
|
|
20
|
+
// conventions, not that it shares code with catalyst-core (see issue #340/#411).
|
|
21
|
+
|
|
22
|
+
const CODE_FORMAT_RE = /^[A-Z][A-Z-]*-\d{3}$/
|
|
23
|
+
|
|
24
|
+
describe("CCA ERROR_CODES / ERROR_DEFINITIONS shape invariants (mirrors registry.test.js's rules)", () => {
|
|
25
|
+
const { ERROR_CODES, ERROR_DEFINITIONS } = cca
|
|
26
|
+
const codeEntries = Object.entries(ERROR_CODES)
|
|
27
|
+
const codeValues = Object.values(ERROR_CODES)
|
|
28
|
+
|
|
29
|
+
it("has at least one code defined", () => {
|
|
30
|
+
expect(codeEntries.length).toBeGreaterThan(0)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it("every ERROR_CODES value has a matching ERROR_DEFINITIONS entry", () => {
|
|
34
|
+
const missing = codeValues.filter((code) => !(code in ERROR_DEFINITIONS))
|
|
35
|
+
expect(missing).toEqual([])
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it("code values are unique", () => {
|
|
39
|
+
const seen = new Set()
|
|
40
|
+
const duplicates = []
|
|
41
|
+
for (const code of codeValues) {
|
|
42
|
+
if (seen.has(code)) duplicates.push(code)
|
|
43
|
+
seen.add(code)
|
|
44
|
+
}
|
|
45
|
+
expect(duplicates).toEqual([])
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it("every code matches the CATEGORY-NNN format", () => {
|
|
49
|
+
const malformed = codeValues.filter((code) => !CODE_FORMAT_RE.test(code))
|
|
50
|
+
expect(malformed).toEqual([])
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it("every code's category prefix matches its definition's category field", () => {
|
|
54
|
+
const mismatches = []
|
|
55
|
+
for (const [key, code] of codeEntries) {
|
|
56
|
+
const def = ERROR_DEFINITIONS[code]
|
|
57
|
+
if (!def) continue
|
|
58
|
+
const prefix = code.slice(0, code.lastIndexOf("-"))
|
|
59
|
+
if (prefix !== def.category) mismatches.push(`${key} (${code}): prefix "${prefix}" !== category "${def.category}"`)
|
|
60
|
+
}
|
|
61
|
+
expect(mismatches).toEqual([])
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it("every definition has all required fields, non-empty — same required-field set as the core registry", () => {
|
|
65
|
+
const REQUIRED_FIELDS = ["category", "defaultMessage", "defaultDetails", "suggestedAction"] as const
|
|
66
|
+
const problems: string[] = []
|
|
67
|
+
for (const [code, def] of Object.entries(ERROR_DEFINITIONS)) {
|
|
68
|
+
for (const field of REQUIRED_FIELDS) {
|
|
69
|
+
const value = def[field]
|
|
70
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
71
|
+
problems.push(`${code}: missing or empty "${field}"`)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
expect(problems).toEqual([])
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it("no definition carries a `recoverable` field (dropped repo-wide, including this mirror)", () => {
|
|
79
|
+
const withRecoverable = Object.entries(ERROR_DEFINITIONS)
|
|
80
|
+
.filter(([, def]) => "recoverable" in def)
|
|
81
|
+
.map(([code]) => code)
|
|
82
|
+
expect(withRecoverable).toEqual([])
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it("owns exactly one category (CCA) — doesn't collide with any core-owned category", () => {
|
|
86
|
+
const CORE_CATEGORIES = new Set(["PREFLIGHT", "PROCESS", "BUNDLE", "IOS", "ANDROID", "RUNTIME-NATIVE", "AI", "RUNTIME-WEB"])
|
|
87
|
+
const ccaCategories = new Set(Object.values(ERROR_DEFINITIONS).map((d) => d.category))
|
|
88
|
+
const collisions = [...ccaCategories].filter((c) => CORE_CATEGORIES.has(c))
|
|
89
|
+
expect(collisions).toEqual([])
|
|
90
|
+
expect(ccaCategories).toEqual(new Set(["CCA"]))
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
describe("createError() / getDefinition() — same override + fallback contract as catalyst-core's version", () => {
|
|
95
|
+
it("uses registry defaults when no overrides are given", () => {
|
|
96
|
+
const err = cca.createError(cca.ERROR_CODES.CCA_DIRECTORY_EXISTS)
|
|
97
|
+
expect(err).toBeInstanceOf(cca.CCAError)
|
|
98
|
+
expect(err.code).toBe("CCA-002")
|
|
99
|
+
expect(err.category).toBe("CCA")
|
|
100
|
+
expect(err.message).toBe("Target directory already exists")
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it("overrides details while falling back to the default message", () => {
|
|
104
|
+
const err = cca.createError(cca.ERROR_CODES.CCA_UPSTREAM_ERROR, { details: "npm pack exited 1" })
|
|
105
|
+
expect(err.details).toBe("npm pack exited 1")
|
|
106
|
+
expect(err.message).toBe("An upstream command failed")
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it("falls back to the CCA_UPSTREAM_ERROR definition for an unrecognized code (same shape as core's UNKNOWN fallback, different concrete fallback)", () => {
|
|
110
|
+
const err = cca.createError("NOT-A-REAL-CODE")
|
|
111
|
+
expect(err.category).toBe("CCA")
|
|
112
|
+
expect(err.message).toBe("An upstream command failed")
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it("attaches cause only when explicitly provided", () => {
|
|
116
|
+
const withoutCause = cca.createError(cca.ERROR_CODES.CCA_DIRECTORY_EXISTS)
|
|
117
|
+
expect("cause" in withoutCause).toBe(false)
|
|
118
|
+
const original = new Error("EEXIST")
|
|
119
|
+
const withCause = cca.createError(cca.ERROR_CODES.CCA_DIRECTORY_EXISTS, { cause: original })
|
|
120
|
+
expect(withCause.cause).toBe(original)
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
describe("wrapForeignError() — same never-reinterpret contract as catalyst-core's wrapForeignError", () => {
|
|
125
|
+
it("always uses the CCA-000 wrapper code and preserves the original as cause", () => {
|
|
126
|
+
const original = new Error("ENOENT: no such file or directory")
|
|
127
|
+
const wrapped = cca.wrapForeignError(original)
|
|
128
|
+
expect(wrapped.code).toBe(cca.ERROR_CODES.CCA_UPSTREAM_ERROR)
|
|
129
|
+
expect(wrapped.cause).toBe(original)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it("shows the upstream named code when present", () => {
|
|
133
|
+
// .code isn't part of the standard Error interface — it's a Node
|
|
134
|
+
// convention (see NodeJS.ErrnoException).
|
|
135
|
+
const original: NodeJS.ErrnoException = new Error("failed")
|
|
136
|
+
original.code = "ENOENT"
|
|
137
|
+
const wrapped = cca.wrapForeignError(original)
|
|
138
|
+
expect(wrapped.message).toContain("ENOENT")
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it("omits the \"(upstream: ...)\" suffix entirely when there is no named code, rather than leaving it dangling", () => {
|
|
142
|
+
const wrapped = cca.wrapForeignError(new Error("generic failure"))
|
|
143
|
+
expect(wrapped.message).toBe("An upstream command failed")
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it("omits the suffix for an empty or whitespace-only .code too — still typeof \"string\", but not a real name", () => {
|
|
147
|
+
const emptyCode: NodeJS.ErrnoException = new Error("failed")
|
|
148
|
+
emptyCode.code = ""
|
|
149
|
+
expect(cca.wrapForeignError(emptyCode).message).toBe("An upstream command failed")
|
|
150
|
+
|
|
151
|
+
const blankCode: NodeJS.ErrnoException = new Error("failed")
|
|
152
|
+
blankCode.code = " "
|
|
153
|
+
expect(cca.wrapForeignError(blankCode).message).toBe("An upstream command failed")
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it("trims surrounding whitespace off a real named code", () => {
|
|
157
|
+
const padded: NodeJS.ErrnoException = new Error("failed")
|
|
158
|
+
padded.code = " ENOENT "
|
|
159
|
+
expect(cca.wrapForeignError(padded).message).toBe("An upstream command failed (upstream: ENOENT)")
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
describe("getDocUrl() — same repo-blob URL convention as catalyst-core's getDocUrl", () => {
|
|
164
|
+
it("builds a URL under errors/<category>/<code>.md for a known code", () => {
|
|
165
|
+
expect(cca.getDocUrl(cca.ERROR_CODES.CCA_INVALID_NAME)).toBe(
|
|
166
|
+
"https://github.com/tata1mg/catalyst-core/blob/main/errors/CCA/CCA-001.md"
|
|
167
|
+
)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it("falls back to the CCA-000 doc URL for an unrecognized code, rather than a broken link", () => {
|
|
171
|
+
expect(cca.getDocUrl("NOT-A-REAL-CODE")).toBe(
|
|
172
|
+
"https://github.com/tata1mg/catalyst-core/blob/main/errors/CCA/CCA-000.md"
|
|
173
|
+
)
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
describe("formatError() — same 3-mode contract (default/verbose/debug) as catalyst-core's formatError", () => {
|
|
178
|
+
it("defaults to \"default\" mode", () => {
|
|
179
|
+
const err = cca.createError(cca.ERROR_CODES.CCA_DIRECTORY_EXISTS)
|
|
180
|
+
expect(cca.formatError(err)).toBe(cca.formatError(err, "default"))
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it("default mode includes code, message, and suggested action", () => {
|
|
184
|
+
const err = cca.createError(cca.ERROR_CODES.CCA_DIRECTORY_EXISTS)
|
|
185
|
+
const output = cca.formatError(err, "default")
|
|
186
|
+
expect(output).toContain("[CCA-002]")
|
|
187
|
+
expect(output).toContain("Suggested action:")
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it("verbose mode is boxed and includes Code/Category/Solution/Docs", () => {
|
|
191
|
+
const err = cca.createError(cca.ERROR_CODES.CCA_DIRECTORY_EXISTS)
|
|
192
|
+
const output = cca.formatError(err, "verbose")
|
|
193
|
+
expect(output).toContain("Code: CCA-002")
|
|
194
|
+
expect(output).toContain("Category: CCA")
|
|
195
|
+
expect(output).toContain("Solution:")
|
|
196
|
+
expect(output).toContain("┌")
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it("debug mode includes the cause chain and a Stack trace section", () => {
|
|
200
|
+
const original = new Error("root cause")
|
|
201
|
+
const err = cca.createError(cca.ERROR_CODES.CCA_UPSTREAM_ERROR, { cause: original })
|
|
202
|
+
const output = cca.formatError(err, "debug", { node: "v20.4.0" })
|
|
203
|
+
expect(output).toContain("Caused by: root cause")
|
|
204
|
+
expect(output).toContain("Environment:")
|
|
205
|
+
expect(output).toContain("Stack trace:")
|
|
206
|
+
})
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
describe("resolveOutputMode() — same bare-flag contract as scriptUtils.js#resolveOutputMode", () => {
|
|
210
|
+
it("detects --debug and --verbose from an argv array", () => {
|
|
211
|
+
expect(cca.resolveOutputMode(["node", "cli.js", "--debug"])).toBe("debug")
|
|
212
|
+
expect(cca.resolveOutputMode(["node", "cli.js", "--verbose"])).toBe("verbose")
|
|
213
|
+
expect(cca.resolveOutputMode(["node", "cli.js"])).toBe("default")
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it("--debug takes priority when both flags are present", () => {
|
|
217
|
+
expect(cca.resolveOutputMode(["--verbose", "--debug"])).toBe("debug")
|
|
218
|
+
})
|
|
219
|
+
})
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
/* Type-checks test/ only (via `tsc --noEmit`) — scripts/errors.cjs and
|
|
4
|
+
* the rest of create-catalyst-app's source stay plain JS/CJS until a
|
|
5
|
+
* future source conversion (see issue #420). This exists for
|
|
6
|
+
* editor/CI type-checking; vitest transpiles .ts test files via
|
|
7
|
+
* esbuild at run time and doesn't need this config to execute them. */
|
|
8
|
+
"target": "es2022",
|
|
9
|
+
"lib": ["es2022"],
|
|
10
|
+
"module": "node16",
|
|
11
|
+
"moduleResolution": "node16",
|
|
12
|
+
"allowJs": true,
|
|
13
|
+
"checkJs": false,
|
|
14
|
+
"strict": true,
|
|
15
|
+
"esModuleInterop": true,
|
|
16
|
+
"forceConsistentCasingInFileNames": true,
|
|
17
|
+
"skipLibCheck": true,
|
|
18
|
+
"types": ["node", "vitest/globals"],
|
|
19
|
+
"noEmit": true
|
|
20
|
+
},
|
|
21
|
+
"include": ["test"],
|
|
22
|
+
"exclude": ["node_modules"]
|
|
23
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { defineConfig } from "vitest/config"
|
|
2
|
+
|
|
3
|
+
// Framework-level (Tier 1) unit tests for create-catalyst-app's own
|
|
4
|
+
// internals — see test/*.test.ts. errors.cjs is a standalone CJS mirror of
|
|
5
|
+
// catalyst-core's error registry shape (it can't depend on catalyst-core —
|
|
6
|
+
// CCA packs/extracts itself before catalyst-core is ever installed), so its
|
|
7
|
+
// parity test lives here rather than in catalyst-core's test suite.
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
test: {
|
|
10
|
+
environment: "node",
|
|
11
|
+
include: ["test/**/*.test.ts"],
|
|
12
|
+
// errors.cjs itself is CommonJS (see its own file header), loaded
|
|
13
|
+
// via require() from the .ts test file below — TypeScript allows
|
|
14
|
+
// require() in a .ts file (it's still just a global Node function),
|
|
15
|
+
// it just doesn't get CJS *emit* semantics the way .cts would.
|
|
16
|
+
// .cts was tried first and rejected: this Vite/Rollup version's SSR
|
|
17
|
+
// transform doesn't strip TS syntax from .cts files at all (fails
|
|
18
|
+
// to parse even a single `const x: number` type annotation) — a
|
|
19
|
+
// real tooling gap, not a config mistake. vitest's own describe/it/
|
|
20
|
+
// expect exports can't be require()'d, only import'd — globals
|
|
21
|
+
// injects them instead.
|
|
22
|
+
globals: true,
|
|
23
|
+
},
|
|
24
|
+
})
|