@starpak/error 0.0.0-stage → 0.1.4
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/LICENSE +22 -0
- package/lib/codes.d.ts +90 -0
- package/lib/codes.d.ts.map +1 -0
- package/lib/codes.js +108 -0
- package/lib/codes.js.map +1 -0
- package/lib/definitions.d.ts +31 -0
- package/lib/definitions.d.ts.map +1 -0
- package/lib/definitions.js +114 -0
- package/lib/definitions.js.map +1 -0
- package/lib/error.d.ts +61 -0
- package/lib/error.d.ts.map +1 -0
- package/lib/error.js +89 -0
- package/lib/error.js.map +1 -0
- package/lib/format.d.ts +18 -0
- package/lib/format.d.ts.map +1 -0
- package/lib/format.js +164 -0
- package/lib/format.js.map +1 -0
- package/lib/index.d.ts +16 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +29 -0
- package/lib/index.js.map +1 -0
- package/lib/native.d.ts +20 -0
- package/lib/native.d.ts.map +1 -0
- package/lib/native.js +97 -0
- package/lib/native.js.map +1 -0
- package/package.json +11 -3
- package/src/codes.ts +115 -0
- package/src/definitions.ts +232 -0
- package/src/error.ts +123 -0
- package/src/format.ts +183 -0
- package/src/index.ts +20 -0
- package/src/native.ts +108 -0
- package/README.md +0 -3
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spak Error - error definitions
|
|
3
|
+
*
|
|
4
|
+
* Every code must answer: 哪里错 → 为什么 → 怎么改. Keep `why` and `fix`
|
|
5
|
+
* actionable; `ui` is what the Spak OS error panel renders.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { CODES } from './codes'
|
|
9
|
+
|
|
10
|
+
export type Severity = 'info' | 'warning' | 'error' | 'fatal'
|
|
11
|
+
export type Category =
|
|
12
|
+
| 'language' | 'build' | 'mod' | 'runtime'
|
|
13
|
+
| 'permission' | 'fs' | 'network' | 'env' | 'fatal'
|
|
14
|
+
|
|
15
|
+
export interface ErrorUI {
|
|
16
|
+
icon?: string
|
|
17
|
+
color?: string
|
|
18
|
+
/** One-click actions shown by the OS panel. */
|
|
19
|
+
actions?: string[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ErrorDefinition {
|
|
23
|
+
code: string
|
|
24
|
+
title: string
|
|
25
|
+
severity: Severity
|
|
26
|
+
category: Category
|
|
27
|
+
why: string
|
|
28
|
+
fix: string[]
|
|
29
|
+
docs?: string
|
|
30
|
+
ui?: ErrorUI
|
|
31
|
+
related?: string[]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const D = (
|
|
35
|
+
code: string,
|
|
36
|
+
title: string,
|
|
37
|
+
severity: Severity,
|
|
38
|
+
category: Category,
|
|
39
|
+
why: string,
|
|
40
|
+
fix: string[],
|
|
41
|
+
extra: Partial<ErrorDefinition> = {},
|
|
42
|
+
): ErrorDefinition => ({ code, title, severity, category, why, fix, ...extra })
|
|
43
|
+
|
|
44
|
+
const LIST: ErrorDefinition[] = [
|
|
45
|
+
// ---------------- 1xxx language ----------------
|
|
46
|
+
D(CODES.SYNTAX, '语法错误', 'error', 'language', '源码不符合 Spak 语法,解析器在报错位置无法继续。',
|
|
47
|
+
['看报错列出的行与列,重点检查该位置前一个 token', '用 `spm run <file>` 复现,最小化到能触发错误的最短代码']),
|
|
48
|
+
D(CODES.UNEXPECTED_TOKEN, '意外的 token', 'error', 'language', '在只能出现特定 token 的位置出现了别的符号,常见于漏写 `end`、多写逗号。',
|
|
49
|
+
['检查该块是否缺少 `end`', '检查上一行是否多写 `)`、`,` 或 `then`'], { related: [CODES.SYNTAX] }),
|
|
50
|
+
D(CODES.UNTERMINATED_STRING, '字符串没有闭合', 'error', 'language', '字符串缺少结束引号,或跨行字符串没有使用 `"""`。',
|
|
51
|
+
['补齐引号', '多行文本请用 `"""..."""`']),
|
|
52
|
+
D(CODES.TYPE_MISMATCH, '类型不匹配', 'error', 'language', '注解声明的类型与实际值的类型不一致。',
|
|
53
|
+
['看两侧类型,确认是值写错还是注解写错', '需要字符串时用 `tostring(x)` 或 `f"{x}"`']),
|
|
54
|
+
D(CODES.POSSIBLY_NIL, '可能为 nil', 'error', 'language', '值来自可空类型,使用前没有收窄。',
|
|
55
|
+
['用 `if x then` 收窄', '或用 `x ?? 默认值` 提供兜底']),
|
|
56
|
+
D(CODES.UNKNOWN_TYPE, '未知类型名', 'error', 'language', '注解引用了不存在的类型或没写 `type` 别名。',
|
|
57
|
+
['检查拼写与大小写', '先 `type X = {...}` 声明再使用'], { related: [CODES.TYPE_ALIAS_CYCLE] }),
|
|
58
|
+
D(CODES.ARGUMENT_MISMATCH, '参数数量或类型不符', 'error', 'language', '调用处传入的实参与函数声明的形参对不上。',
|
|
59
|
+
['核对函数签名', '可变参数用 `...` 接收']),
|
|
60
|
+
D(CODES.RETURN_MISMATCH, '返回值与注解不符', 'error', 'language', '函数声明了返回类型,但实际返回了别的类型。',
|
|
61
|
+
['检查所有 `return` 分支', '没有返回值时把返回类型改成 `nil` 或不写']),
|
|
62
|
+
D(CODES.DUPLICATE_DECLARATION, '重复声明', 'error', 'language', '同一作用域里同名绑定被声明了两次。',
|
|
63
|
+
['改掉其中一个名字', '确实要覆盖就先赋值而不是重新声明']),
|
|
64
|
+
D(CODES.TYPE_ALIAS_CYCLE, '类型别名循环', 'error', 'language', '类型别名直接或间接引用了自己,无法展开。',
|
|
65
|
+
['打断循环:至少一处使用具体类型(如 `[T]` 或 `{k: v}`)']),
|
|
66
|
+
D(CODES.MISSING_FIELD, '字段不存在', 'error', 'language', '访问了记录类型上不存在的字段。',
|
|
67
|
+
['检查字段拼写', '先给对象补上该字段,或把类型放宽为 `any`']),
|
|
68
|
+
D(CODES.ZERO_BASED_INDEX, '1 基索引误用', 'error', 'language', 'Spak 的序列下标从 1 开始,`list[0]` 取不到元素。',
|
|
69
|
+
['把 `list[0]` 改成 `list[1]`', '遍历优先用 `for x in list` 而不是手写下标']),
|
|
70
|
+
|
|
71
|
+
// ---------------- 2xxx build ----------------
|
|
72
|
+
D(CODES.BUILD_FAILED, '构建失败', 'error', 'build', '打包器(esbuild/SEA/容器封装)在某个阶段返回了错误。',
|
|
73
|
+
['看构建日志里的第一条错误,后面通常是连锁反应', '`spm build` 加 `--target` 明确目标']),
|
|
74
|
+
D(CODES.ENTRY_NOT_FOUND, '入口文件不存在', 'error', 'build', '给定路径不存在,或目录里没有清单与默认入口文件。',
|
|
75
|
+
['检查 `-i` 路径是否存在(文件或目录都可以)', '传目录时会自动查找清单与 index.* 入口', '构建当前目录可直接用 `spm build -i .`']),
|
|
76
|
+
D(CODES.UNKNOWN_TARGET, '未知构建目标', 'error', 'build', '`--target` 传了当前构建器不支持的名字。',
|
|
77
|
+
['可用目标:node / server / app / web / gal', '`spm build --help` 查看当次构建器支持列表']),
|
|
78
|
+
D(CODES.PAK_MAGIC_INVALID, '不是有效的 SPAK 包', 'error', 'build', '文件头不是 `SPAK`,可能不是包或下载被截断。',
|
|
79
|
+
['确认文件来源', '重新下载;必要时用 `parsePak` 校验']),
|
|
80
|
+
D(CODES.PAK_VERSION_UNSUPPORTED, '包版本不支持', 'error', 'build', '容器版本高于当前运行时能读的版本。',
|
|
81
|
+
['升级 Spak 运行时', '或让打包方使用 v2 格式']),
|
|
82
|
+
D(CODES.PAK_MANIFEST_INVALID, '包清单损坏', 'error', 'build', '容器里的 manifest 不是合法 JSON 或缺少必填字段。',
|
|
83
|
+
['重新打包', '检查打包脚本是否写入了空清单']),
|
|
84
|
+
D(CODES.PAK_TRUNCATED, '包被截断', 'error', 'build', '文件长度小于 manifest 声明的长度,传输不完整。',
|
|
85
|
+
['重新下载', '校验文件大小与哈希']),
|
|
86
|
+
D(CODES.INTEGRITY_MISMATCH, '完整性校验失败', 'error', 'build', '下载内容的 sha512 与索引声明不一致,可能被篡改或传输损坏。',
|
|
87
|
+
['重新下载;仍失败则联系发布者', '不要跳过校验强行安装']),
|
|
88
|
+
D(CODES.OUTPUT_NOT_WRITABLE, '输出目录不可写', 'error', 'build', '目标目录不存在或当前用户没有写权限。',
|
|
89
|
+
['换到有写权限的目录', '检查挂载点是否只读']),
|
|
90
|
+
D(CODES.DISK_FULL, '磁盘空间不足', 'error', 'build', '写入产物时文件系统返回 ENOSPC。',
|
|
91
|
+
['清理 `dist/`、缓存与旧版本产物', '换更大的磁盘或挂载点']),
|
|
92
|
+
|
|
93
|
+
// ---------------- 3xxx mods / apps ----------------
|
|
94
|
+
D(CODES.MANIFEST_INVALID, '清单字段非法', 'error', 'mod', '`mod.json` / `spak.app.json` 缺字段或类型不对。',
|
|
95
|
+
['按文档补齐 id / name / version / entry', '错误信息会指出具体字段名']),
|
|
96
|
+
D(CODES.DEPENDENCY_MISSING, '依赖缺失', 'error', 'mod', '`requires` 里声明的模组没有被安装或 id 拼错。',
|
|
97
|
+
['`spm mod add <id>` 安装依赖', '检查依赖 id 是否与对方 `mod.json` 一致']),
|
|
98
|
+
D(CODES.DEPENDENCY_CYCLE, '循环依赖', 'error', 'mod', '模组之间互相依赖,无法确定装载顺序。',
|
|
99
|
+
['把公共部分抽成第三个模组', '打断其中一条 `requires`']),
|
|
100
|
+
D(CODES.DUPLICATE_MOD_ID, '模组 id 重复', 'error', 'mod', '两个目录声明了同一个 id。',
|
|
101
|
+
['改掉其中一个 id', '删掉旧版本残留目录']),
|
|
102
|
+
D(CODES.ENTRY_UNSUPPORTED, '入口类型不支持', 'error', 'mod', '入口扩展名不在支持列表内。',
|
|
103
|
+
['支持 .spk / .js / .mjs / .cjs', '把入口改成 `index.spk` 或编译为 `.mjs`']),
|
|
104
|
+
D(CODES.SPK_COMPILE_FAILED, '模组 .spk 编译失败', 'error', 'mod', '模组源码没通过类型检查或代码生成。',
|
|
105
|
+
['按诊断修源码', '先用 `spm run <file>` 单独跑通']),
|
|
106
|
+
D(CODES.VERSION_MISMATCH, '版本不兼容', 'error', 'mod', '模组要求的 SDK / OS 版本与当前宿主不符。',
|
|
107
|
+
['升级宿主或降低模组要求', '检查 `sdk` / `os.min` 字段']),
|
|
108
|
+
D(CODES.CONFIG_INVALID, '配置无效', 'error', 'mod', '配置文件无法解析,或字段类型/取值不合法。',
|
|
109
|
+
['检查 `spak.config.yml` 的缩进与字段名', '端口必须是 0-65535 的整数']),
|
|
110
|
+
D(CODES.SIGNATURE_INVALID, '签名校验失败', 'error', 'mod', '官方条目的 ed25519 签名与内置公钥不匹配,或签名缺失。',
|
|
111
|
+
['确认来源是官方索引', '不要安装未通过验签的官方包']),
|
|
112
|
+
D(CODES.INSTALL_FAILED, '安装或升级失败', 'error', 'mod', 'npm 安装/升级命令返回了非零状态,或目标包不可用。',
|
|
113
|
+
['检查 npm 登录状态:`npm whoami`', '确认包名与版本存在,或改用镜像重试']),
|
|
114
|
+
|
|
115
|
+
// ---------------- 4xxx runtime ----------------
|
|
116
|
+
D(CODES.UNCAUGHT_EXCEPTION, '未捕获异常', 'fatal', 'runtime', '异常逃出了所有 try/catch,进程即将退出。',
|
|
117
|
+
['按堆栈定位第一行业务代码', '在该层加 `try/catch` 并给出可读错误']),
|
|
118
|
+
D(CODES.UNHANDLED_REJECTION, '未处理的 Promise 拒绝', 'fatal', 'runtime', '异步调用失败但没有任何 catch。',
|
|
119
|
+
['给 Promise 加 `.catch()` 或用 `try/await`', '加全局 `process.on("unhandledRejection")` 兜底上报']),
|
|
120
|
+
D(CODES.PORT_IN_USE, '端口已被占用', 'error', 'runtime', '另一个进程已经监听该端口。',
|
|
121
|
+
['换端口(`spak.config.yml` 的 `server.port`)', '或结束占用进程:`lsof -i :<port>`']),
|
|
122
|
+
D(CODES.LISTEN_FAILED, '监听失败', 'error', 'runtime', '地址不可用、权限不足或端口被系统保留。',
|
|
123
|
+
['改用 `0.0.0.0` 或 `127.0.0.1`', '小于 1024 的端口需要额外权限']),
|
|
124
|
+
D(CODES.ROUTE_DUPLICATE, '路由重复注册', 'warning', 'runtime', '同一 method + path 被注册两次,后注册的会覆盖前者。',
|
|
125
|
+
['合并处理函数', '给不同模组的路由加前缀']),
|
|
126
|
+
D(CODES.HANDLER_THREW, '请求处理函数抛错', 'error', 'runtime', '路由 handler 内部异常,已返回 500。',
|
|
127
|
+
['看服务日志里的原始堆栈', '把可能失败的 IO 放进 try/catch']),
|
|
128
|
+
D(CODES.BODY_TOO_LARGE, '请求体过大', 'warning', 'runtime', '请求体超过 1 MB 上限,已被拒绝(413)。',
|
|
129
|
+
['改传文件而不是塞进 JSON', '或调大宿主的上限配置']),
|
|
130
|
+
D(CODES.UNKNOWN_COMMAND, '未知命令', 'error', 'runtime', '输入的命令不在当前 CLI 的注册表里。',
|
|
131
|
+
['运行 `spm --help` 查看全部命令', '或用 `spm help <命令>` 查看某个命令的用法']),
|
|
132
|
+
D(CODES.MISSING_ARGUMENT, '缺少参数', 'error', 'runtime', '命令需要的参数或参数值没有提供。',
|
|
133
|
+
['按提示补齐参数', '用 `<命令> --help` 查看必填项与示例']),
|
|
134
|
+
D(CODES.INVALID_OPTION, '未知参数', 'error', 'runtime', '传入了当前命令不认识的选项。',
|
|
135
|
+
['检查拼写与短选项', '用 `<命令> --help` 查看可用选项']),
|
|
136
|
+
D(CODES.SERVICE_STOP_FAILED, '停止服务失败', 'error', 'runtime', '终止服务进程时出错(pid 不存在、无权限或信号发送失败)。',
|
|
137
|
+
['确认进程是否还在:`ps -p <pid>`', '权限不足时用相同用户或管理员身份重试']),
|
|
138
|
+
D(CODES.AUTH_FAILED, '凭据验证失败', 'error', 'permission', 'npm token 无效、被吊销,或权限不足(需 Read and write + Bypass 2FA)。',
|
|
139
|
+
['重新生成 token 并更新 ~/.config/starpak/credentials.json', '确认 scope 属于当前账号']),
|
|
140
|
+
D(CODES.INVALID_PATH, '路径不合法', 'error', 'fs', '传入了相对路径、不存在的位置,或不满足命令要求的路径形式。',
|
|
141
|
+
['改用绝对路径(以 / 开头)', '确认文件/目录存在且有权限']),
|
|
142
|
+
|
|
143
|
+
// ---------------- 5xxx permission ----------------
|
|
144
|
+
D(CODES.PERMISSION_UNDECLARED, '权限未声明', 'error', 'permission', '调用了清单里没有声明的能力。',
|
|
145
|
+
['在 `permissions` 里显式声明(fs / network / childProcess)', '最小权限原则:只开需要的']),
|
|
146
|
+
D(CODES.FS_DENIED, '文件访问被拒绝', 'error', 'permission', '模组/应用没有 fs 权限,或只读却尝试写入。',
|
|
147
|
+
['申请 `fs: read` 或 `fs: write`', '把要读的文件放进允许的目录']),
|
|
148
|
+
D(CODES.NETWORK_DENIED, '网络访问被拒绝', 'error', 'permission', '清单没有开启网络,或只允许 localhost。',
|
|
149
|
+
['声明 `network: localhost` 或 `all`', '避免在启动阶段发起外网请求']),
|
|
150
|
+
D(CODES.CHILD_PROCESS_DENIED, '子进程被禁止', 'error', 'permission', '清单的 `childProcess` 为 false。',
|
|
151
|
+
['改用内置能力实现', '确实需要就声明 `childProcess: true` 并说明原因']),
|
|
152
|
+
D(CODES.DANGEROUS_API, '危险 API 调用', 'fatal', 'permission', '调用了沙箱明确禁止的接口(eval / 动态执行 / 原生模块)。',
|
|
153
|
+
['换成安全替代实现', '如确需使用,走宿主提供的受控接口']),
|
|
154
|
+
|
|
155
|
+
// ---------------- 6xxx filesystem ----------------
|
|
156
|
+
D(CODES.FS_NOT_FOUND, '文件或目录不存在', 'error', 'fs', '路径拼错、文件被删,或工作目录不是预期的那个。',
|
|
157
|
+
['打印 `process.cwd()` 与绝对路径确认', '用 `fs.existsSync` 先判断再读']),
|
|
158
|
+
D(CODES.FS_DENIED_BY_OS, '权限不足(EACCES/EPERM)', 'error', 'fs', '当前用户对目标路径没有读/写/执行权限。',
|
|
159
|
+
['检查文件 mode 与属主', '换有权限的目录,或提升权限后再试']),
|
|
160
|
+
D(CODES.FS_OPERATION_NOT_PERMITTED, '操作不被允许(EPERM)', 'error', 'fs', '文件系统或安全策略禁止该操作(如 chmod/chown)。',
|
|
161
|
+
['避免对挂载盘做权限变更', '改用复制到本地盘再操作']),
|
|
162
|
+
D(CODES.FS_NO_SPACE, '磁盘已满(ENOSPC)', 'fatal', 'fs', '写入时空间耗尽,进程可能已产生半个文件。',
|
|
163
|
+
['清理临时目录与旧产物', '检查是否写进了内存盘']),
|
|
164
|
+
D(CODES.FS_TOO_MANY_OPEN_FILES, '打开文件过多(EMFILE)', 'error', 'fs', '文件句柄泄漏,或超过 `ulimit -n`。',
|
|
165
|
+
['确保每个 open 都有 close', '批量处理时分批并复用句柄']),
|
|
166
|
+
D(CODES.FS_IS_DIRECTORY, '目标是目录(EISDIR)', 'error', 'fs', '把目录当成文件读取或覆盖。',
|
|
167
|
+
['先 `stat` 判断类型', '目录操作用 `readdir` 而不是 `readFile`']),
|
|
168
|
+
D(CODES.FS_RENAME_UNSUPPORTED, '重命名/原子写失败(FUSE)', 'error', 'fs', '在 Android FUSE 挂载盘上,重命名会残留 `.l2s.tmp_*` 并报 ENOENT。',
|
|
169
|
+
['把 git 仓库/.git 或构建产物放到本地盘', '避免在挂载盘上做原子写与符号链接']),
|
|
170
|
+
|
|
171
|
+
// ---------------- 7xxx network ----------------
|
|
172
|
+
D(CODES.NET_CONNECTION_REFUSED, '连接被拒绝', 'error', 'network', '目标端口没有服务在监听,或防火墙直接拒绝。',
|
|
173
|
+
['确认服务已启动并监听该端口', '检查地址是否写成了内网/回环']),
|
|
174
|
+
D(CODES.NET_CONNECTION_RESET, '连接被重置', 'error', 'network', '对端或中间设备主动断开(常见于被墙/代理超时)。',
|
|
175
|
+
['重试并退避', '换镜像/代理;检查 TLS 版本']),
|
|
176
|
+
D(CODES.NET_TIMEOUT, '请求超时', 'error', 'network', '在超时时间内没有收到完整响应。',
|
|
177
|
+
['调大超时并加分类重试', '先测同域名的小请求定位是网络还是服务慢']),
|
|
178
|
+
D(CODES.NET_DNS, '域名解析失败', 'error', 'network', '域名不存在或 DNS 不可用。',
|
|
179
|
+
['`nslookup` 验证域名', '换 DNS 或临时用 IP']),
|
|
180
|
+
D(CODES.NET_TLS, 'TLS 握手失败', 'error', 'network', '证书链、协议版本或系统时间不正确。',
|
|
181
|
+
['检查系统时间', '更新 CA 证书;企业代理需额外信任根证书']),
|
|
182
|
+
D(CODES.NET_PROXY_AUTH, '代理认证失败', 'error', 'network', '代理要求凭据,或凭据已过期。',
|
|
183
|
+
['更新代理用户名/密码或 token', '确认 `HTTP(S)_PROXY` 环境变量正确']),
|
|
184
|
+
D(CODES.NET_HTTP_STATUS, 'HTTP 状态码异常', 'error', 'network', '对端返回 4xx/5xx。',
|
|
185
|
+
['先看响应体里的错误信息', '4xx 查请求参数,5xx 看对端日志']),
|
|
186
|
+
|
|
187
|
+
// ---------------- 8xxx environment ----------------
|
|
188
|
+
D(CODES.NODE_VERSION, 'Node 版本不满足要求', 'error', 'env', '当前运行时低于所需版本(构建目标为 node24)。',
|
|
189
|
+
['升级 Node 到 24+', '或把构建目标降到你实际运行的版本']),
|
|
190
|
+
D(CODES.NATIVE_MODULE, '原生模块加载失败', 'error', 'env', 'ABI 不匹配、缺少系统库,或挂载盘无法 mmap。',
|
|
191
|
+
['用对应 Node 版本重新编译', '装缺失的 .so;避免从 FUSE 盘加载 .node']),
|
|
192
|
+
D(CODES.PLATFORM_UNSUPPORTED, '平台不支持', 'error', 'env', '目标平台不在当前构建器支持列表内。',
|
|
193
|
+
['查看 `docs/spak-os.md` 的支持矩阵', '用交叉构建或换平台']),
|
|
194
|
+
D(CODES.ENCODING, '编码/终端问题', 'warning', 'env', '输出包含终端无法显示的字符,或文件不是 UTF-8。',
|
|
195
|
+
['统一用 UTF-8', '`LANG=C.UTF-8` 后再运行']),
|
|
196
|
+
D(CODES.CLOCK_SKEW, '系统时间偏移', 'warning', 'env', '本机时间与服务器差距过大,导致证书/签名校验失败。',
|
|
197
|
+
['开启自动对时', '检查时区设置']),
|
|
198
|
+
D(CODES.MODULE_NOT_FOUND, '模块找不到(MODULE_NOT_FOUND)', 'error', 'env', 'import/require 的包没有安装,或路径解析到了工作区之外。',
|
|
199
|
+
['`pnpm install` 补齐依赖', '检查包名拼写与 `exports` 映射', '相对路径要用 `./` 开头']),
|
|
200
|
+
|
|
201
|
+
// ---------------- 9xxx fatal ----------------
|
|
202
|
+
D(CODES.OUT_OF_MEMORY, '内存耗尽', 'fatal', 'fatal', '堆内存超过 V8 上限,进程被终止。',
|
|
203
|
+
['定位大对象/无限累积的数组', '临时用 `--max-old-space-size=4096` 顶住,再修根因']),
|
|
204
|
+
D(CODES.STACK_OVERFLOW, '调用栈溢出', 'fatal', 'fatal', '无限递归或循环引用触发深度遍历。',
|
|
205
|
+
['找递归出口', '深树结构改用显式栈遍历']),
|
|
206
|
+
D(CODES.NATIVE_CRASH, '原生崩溃/段错误', 'fatal', 'fatal', '原生插件或运行时自身崩溃,JS 层 catch 不到。',
|
|
207
|
+
['用最小复现定位是哪个 addon', '禁用可疑原生模块后重试']),
|
|
208
|
+
D(CODES.CORRUPT_PACKAGE, '包数据损坏', 'fatal', 'fatal', '`.app` / `.pak` 内容与清单不一致或校验失败。',
|
|
209
|
+
['重新下载并核对哈希', '不要手工改包内文件']),
|
|
210
|
+
D(CODES.HANDLE_REVOKED, '句柄中途失效', 'fatal', 'fatal', '文件/网络句柄被外部关闭(权限撤销、设备拔出)。',
|
|
211
|
+
['捕获 EBADF 并重建句柄', '不要把句柄长期跨作用域缓存']),
|
|
212
|
+
D(CODES.UNKNOWN, '未识别的错误', 'error', 'fatal', '错误不匹配任何已知模式。',
|
|
213
|
+
['保留完整堆栈与复现步骤', '附上 `spm -v` 与系统信息再反馈']),
|
|
214
|
+
]
|
|
215
|
+
|
|
216
|
+
export const DEFINITIONS: Record<string, ErrorDefinition> = Object.fromEntries(
|
|
217
|
+
LIST.map(definition => [definition.code, definition]),
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
export function getDefinition(code: string): ErrorDefinition | undefined {
|
|
221
|
+
return DEFINITIONS[code]
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function allDefinitions(): ErrorDefinition[] {
|
|
225
|
+
return Object.values(DEFINITIONS)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Codes that only warn; used by `format` to pick colors and exit codes. */
|
|
229
|
+
export function isFatal(code: string): boolean {
|
|
230
|
+
const definition = getDefinition(code)
|
|
231
|
+
return definition?.severity === 'fatal'
|
|
232
|
+
}
|
package/src/error.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spak Error - error class and normalization
|
|
3
|
+
*
|
|
4
|
+
* `SpakError` always carries a code, its definition, and the original cause.
|
|
5
|
+
* `toInfo()` is the single shape every renderer consumes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { CODES } from './codes'
|
|
9
|
+
import { DEFINITIONS, getDefinition, type Category, type ErrorDefinition, type ErrorUI, type Severity } from './definitions'
|
|
10
|
+
|
|
11
|
+
export interface ErrorContext {
|
|
12
|
+
[key: string]: unknown
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ErrorInfo {
|
|
16
|
+
code: string
|
|
17
|
+
title: string
|
|
18
|
+
severity: Severity
|
|
19
|
+
category: Category
|
|
20
|
+
why: string
|
|
21
|
+
fix: string[]
|
|
22
|
+
docs?: string
|
|
23
|
+
ui?: ErrorUI
|
|
24
|
+
/** Human message for this occurrence (falls back to the definition title). */
|
|
25
|
+
message: string
|
|
26
|
+
/** Where it happened: file:line:col, module id, route, etc. */
|
|
27
|
+
where?: string
|
|
28
|
+
context?: ErrorContext
|
|
29
|
+
cause?: Error
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface SpakErrorOptions {
|
|
33
|
+
cause?: unknown
|
|
34
|
+
where?: string
|
|
35
|
+
context?: ErrorContext
|
|
36
|
+
/** Occurrence-specific message; the definition title is used when omitted. */
|
|
37
|
+
message?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const FALLBACK = DEFINITIONS[CODES.UNKNOWN]
|
|
41
|
+
|
|
42
|
+
function asError(cause: unknown): Error | undefined {
|
|
43
|
+
if (!cause) return undefined
|
|
44
|
+
if (cause instanceof Error) return cause
|
|
45
|
+
return new Error(String(cause))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class SpakError extends Error {
|
|
49
|
+
readonly code: string
|
|
50
|
+
readonly definition: ErrorDefinition
|
|
51
|
+
readonly where?: string
|
|
52
|
+
readonly context?: ErrorContext
|
|
53
|
+
readonly cause?: Error
|
|
54
|
+
|
|
55
|
+
constructor(code: string, options: SpakErrorOptions = {}) {
|
|
56
|
+
const definition = getDefinition(code) ?? FALLBACK
|
|
57
|
+
super(options.message ?? definition.title)
|
|
58
|
+
this.name = 'SpakError'
|
|
59
|
+
this.code = definition.code
|
|
60
|
+
this.definition = definition
|
|
61
|
+
this.where = options.where
|
|
62
|
+
this.context = options.context
|
|
63
|
+
this.cause = asError(options.cause)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
get severity(): Severity {
|
|
67
|
+
return this.definition.severity
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get fix(): string[] {
|
|
71
|
+
return this.definition.fix
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
toInfo(): ErrorInfo {
|
|
75
|
+
const definition = this.definition
|
|
76
|
+
return {
|
|
77
|
+
code: definition.code,
|
|
78
|
+
title: definition.title,
|
|
79
|
+
severity: definition.severity,
|
|
80
|
+
category: definition.category,
|
|
81
|
+
why: definition.why,
|
|
82
|
+
fix: definition.fix,
|
|
83
|
+
docs: definition.docs,
|
|
84
|
+
ui: definition.ui,
|
|
85
|
+
message: this.message,
|
|
86
|
+
where: this.where,
|
|
87
|
+
context: this.context,
|
|
88
|
+
cause: this.cause,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function isSpakError(value: unknown): value is SpakError {
|
|
94
|
+
return value instanceof SpakError
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Build an `ErrorInfo` for a definition without throwing. */
|
|
98
|
+
export function describe(code: string, options: { where?: string, context?: ErrorContext, message?: string } = {}): ErrorInfo {
|
|
99
|
+
return new SpakError(code, options).toInfo()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Language diagnostics → Spak errors. Keeps the original position. */
|
|
103
|
+
export function fromDiagnostic(diagnostic: {
|
|
104
|
+
code: string
|
|
105
|
+
message: string
|
|
106
|
+
loc?: { line: number, column: number }
|
|
107
|
+
}): SpakError {
|
|
108
|
+
const mapping: Record<string, string> = {
|
|
109
|
+
E001: CODES.TYPE_MISMATCH,
|
|
110
|
+
E002: CODES.POSSIBLY_NIL,
|
|
111
|
+
E003: CODES.UNKNOWN_TYPE,
|
|
112
|
+
E004: CODES.ARGUMENT_MISMATCH,
|
|
113
|
+
E005: CODES.RETURN_MISMATCH,
|
|
114
|
+
E006: CODES.TYPE_ALIAS_CYCLE,
|
|
115
|
+
E007: CODES.MISSING_FIELD,
|
|
116
|
+
W001: CODES.UNKNOWN,
|
|
117
|
+
W002: CODES.UNKNOWN,
|
|
118
|
+
W003: CODES.UNKNOWN,
|
|
119
|
+
}
|
|
120
|
+
const code = mapping[diagnostic.code] ?? CODES.UNKNOWN
|
|
121
|
+
const where = diagnostic.loc ? `line ${diagnostic.loc.line}:${diagnostic.loc.column}` : undefined
|
|
122
|
+
return new SpakError(code, { message: diagnostic.message, where, context: { diagnostic: diagnostic.code } })
|
|
123
|
+
}
|
package/src/format.ts
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spak Error - renderers
|
|
3
|
+
*
|
|
4
|
+
* One `ErrorInfo` → four surfaces: terminal text, JSON (CI/logs), HTML
|
|
5
|
+
* (Spak OS panel) and Markdown (docs/issues).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ErrorInfo } from './error'
|
|
9
|
+
|
|
10
|
+
export type FormatKind = 'text' | 'json' | 'html' | 'markdown'
|
|
11
|
+
|
|
12
|
+
export interface FormatOptions {
|
|
13
|
+
/** ANSI colors in `text`; defaults to TTY detection. */
|
|
14
|
+
color?: boolean
|
|
15
|
+
/** Prefix for the location line, e.g. a file path. */
|
|
16
|
+
file?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const ANSI = {
|
|
20
|
+
reset: '\u001b[0m',
|
|
21
|
+
bold: '\u001b[1m',
|
|
22
|
+
dim: '\u001b[2m',
|
|
23
|
+
red: '\u001b[31m',
|
|
24
|
+
yellow: '\u001b[33m',
|
|
25
|
+
cyan: '\u001b[36m',
|
|
26
|
+
magenta: '\u001b[35m',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const SEVERITY_COLOR: Record<string, string> = {
|
|
30
|
+
info: ANSI.cyan,
|
|
31
|
+
warning: ANSI.yellow,
|
|
32
|
+
error: ANSI.red,
|
|
33
|
+
fatal: ANSI.magenta,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const SEVERITY_CSS: Record<string, string> = {
|
|
37
|
+
info: '#2f81f7',
|
|
38
|
+
warning: '#d29922',
|
|
39
|
+
error: '#f85149',
|
|
40
|
+
fatal: '#a371f7',
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function paint(text: string, color: string, enabled: boolean): string {
|
|
44
|
+
return enabled ? `${color}${text}${ANSI.reset}` : text
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function escapeHtml(text: string): string {
|
|
48
|
+
return text
|
|
49
|
+
.replace(/&/g, '&')
|
|
50
|
+
.replace(/</g, '<')
|
|
51
|
+
.replace(/>/g, '>')
|
|
52
|
+
.replace(/"/g, '"')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function location(info: ErrorInfo, file?: string): string | undefined {
|
|
56
|
+
if (info.where) return file ? `${file}:${info.where}` : info.where
|
|
57
|
+
if (file) return file
|
|
58
|
+
return undefined
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function format(info: ErrorInfo, kind: FormatKind = 'text', options: FormatOptions = {}): string {
|
|
62
|
+
switch (kind) {
|
|
63
|
+
case 'json':
|
|
64
|
+
return formatJson(info)
|
|
65
|
+
case 'html':
|
|
66
|
+
return formatHtml(info)
|
|
67
|
+
case 'markdown':
|
|
68
|
+
return formatMarkdown(info)
|
|
69
|
+
default:
|
|
70
|
+
return formatText(info, options)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function formatText(info: ErrorInfo, options: FormatOptions): string {
|
|
75
|
+
const color = options.color ?? Boolean(process.stdout.isTTY)
|
|
76
|
+
const tone = SEVERITY_COLOR[info.severity] ?? ANSI.red
|
|
77
|
+
const label = paint(info.severity, tone, color)
|
|
78
|
+
const lines: string[] = []
|
|
79
|
+
|
|
80
|
+
lines.push(`${label}${paint(`[${info.code}]`, ANSI.dim, color)} ${paint(info.title, ANSI.bold, color)}`)
|
|
81
|
+
if (info.message && info.message !== info.title) {
|
|
82
|
+
lines.push(` ${info.message}`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const where = location(info, options.file)
|
|
86
|
+
if (where) lines.push(` ${paint('-->', ANSI.cyan, color)} ${where}`)
|
|
87
|
+
|
|
88
|
+
lines.push(` ${paint('为什么', ANSI.dim, color)}:${info.why}`)
|
|
89
|
+
if (info.fix.length) {
|
|
90
|
+
lines.push(` ${paint('怎么改', ANSI.dim, color)}:`)
|
|
91
|
+
info.fix.forEach((step, index) => lines.push(` ${index + 1}. ${step}`))
|
|
92
|
+
}
|
|
93
|
+
if (info.docs) lines.push(` ${paint('文档', ANSI.dim, color)}:${info.docs}`)
|
|
94
|
+
|
|
95
|
+
if (info.cause?.stack) {
|
|
96
|
+
const first = info.cause.stack.split('\n').slice(0, 3).join('\n ')
|
|
97
|
+
lines.push(` ${paint('原始错误', ANSI.dim, color)}:\n ${first}`)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return lines.join('\n')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function formatJson(info: ErrorInfo): string {
|
|
104
|
+
return JSON.stringify({
|
|
105
|
+
code: info.code,
|
|
106
|
+
title: info.title,
|
|
107
|
+
severity: info.severity,
|
|
108
|
+
category: info.category,
|
|
109
|
+
message: info.message,
|
|
110
|
+
where: info.where,
|
|
111
|
+
why: info.why,
|
|
112
|
+
fix: info.fix,
|
|
113
|
+
docs: info.docs,
|
|
114
|
+
ui: info.ui,
|
|
115
|
+
context: info.context,
|
|
116
|
+
cause: info.cause
|
|
117
|
+
? { name: info.cause.name, message: info.cause.message, stack: info.cause.stack }
|
|
118
|
+
: undefined,
|
|
119
|
+
}, null, 2)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function formatHtml(info: ErrorInfo): string {
|
|
123
|
+
const color = info.ui?.color ?? SEVERITY_CSS[info.severity] ?? SEVERITY_CSS.error
|
|
124
|
+
const actions = (info.ui?.actions ?? []).map(action =>
|
|
125
|
+
` <button type="button" class="spak-error-action">${escapeHtml(action)}</button>`).join('\n')
|
|
126
|
+
const steps = info.fix.map(step => ` <li>${escapeHtml(step)}</li>`).join('\n')
|
|
127
|
+
|
|
128
|
+
return `<section class="spak-error" data-code="${escapeHtml(info.code)}" data-severity="${escapeHtml(info.severity)}" style="--spak-error-color:${color}">
|
|
129
|
+
<header>
|
|
130
|
+
<span class="spak-error-icon">${escapeHtml(info.ui?.icon ?? '!')}</span>
|
|
131
|
+
<h2>${escapeHtml(info.title)}</h2>
|
|
132
|
+
<code>${escapeHtml(info.code)}</code>
|
|
133
|
+
</header>
|
|
134
|
+
<p class="spak-error-message">${escapeHtml(info.message)}</p>
|
|
135
|
+
${info.where ? `<p class="spak-error-where">${escapeHtml(info.where)}</p>` : ''}
|
|
136
|
+
<p class="spak-error-why"><strong>为什么:</strong>${escapeHtml(info.why)}</p>
|
|
137
|
+
<div class="spak-error-fix">
|
|
138
|
+
<strong>怎么改:</strong>
|
|
139
|
+
<ol>
|
|
140
|
+
${steps}
|
|
141
|
+
</ol>
|
|
142
|
+
</div>
|
|
143
|
+
${actions ? ` <footer>\n${actions}\n </footer>` : ''}
|
|
144
|
+
</section>`
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function formatMarkdown(info: ErrorInfo): string {
|
|
148
|
+
const lines: string[] = []
|
|
149
|
+
lines.push(`### ${info.code} · ${info.title}`)
|
|
150
|
+
lines.push('')
|
|
151
|
+
lines.push(`**严重程度**:${info.severity} **分类**:${info.category}`)
|
|
152
|
+
if (info.where) lines.push(`**位置**:\`${info.where}\``)
|
|
153
|
+
lines.push('')
|
|
154
|
+
if (info.message && info.message !== info.title) {
|
|
155
|
+
lines.push(info.message)
|
|
156
|
+
lines.push('')
|
|
157
|
+
}
|
|
158
|
+
lines.push(`**为什么**:${info.why}`)
|
|
159
|
+
lines.push('')
|
|
160
|
+
lines.push('**怎么改**:')
|
|
161
|
+
info.fix.forEach((step, index) => lines.push(`${index + 1}. ${step}`))
|
|
162
|
+
if (info.docs) {
|
|
163
|
+
lines.push('')
|
|
164
|
+
lines.push(`**文档**:${info.docs}`)
|
|
165
|
+
}
|
|
166
|
+
if (info.cause) {
|
|
167
|
+
lines.push('')
|
|
168
|
+
lines.push('<details><summary>原始错误</summary>')
|
|
169
|
+
lines.push('')
|
|
170
|
+
lines.push('```')
|
|
171
|
+
lines.push(`${info.cause.name}: ${info.cause.message}`)
|
|
172
|
+
if (info.cause.stack) lines.push(info.cause.stack)
|
|
173
|
+
lines.push('```')
|
|
174
|
+
lines.push('')
|
|
175
|
+
lines.push('</details>')
|
|
176
|
+
}
|
|
177
|
+
return lines.join('\n')
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Exit code convention: fatal/error → 1, warning/info → 0. */
|
|
181
|
+
export function exitCodeFor(info: ErrorInfo): number {
|
|
182
|
+
return info.severity === 'fatal' || info.severity === 'error' ? 1 : 0
|
|
183
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spak Error - Public API
|
|
3
|
+
*
|
|
4
|
+
* Every error has a stable code, an actionable explanation and four renderers.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { CODES, codeNumber, isKnownCode } from './codes'
|
|
8
|
+
export type { ErrorCode } from './codes'
|
|
9
|
+
|
|
10
|
+
export { DEFINITIONS, allDefinitions, getDefinition, isFatal } from './definitions'
|
|
11
|
+
export type { Category, ErrorDefinition, ErrorUI, Severity } from './definitions'
|
|
12
|
+
|
|
13
|
+
export { SpakError, describe, fromDiagnostic, isSpakError } from './error'
|
|
14
|
+
export type { ErrorContext, ErrorInfo, SpakErrorOptions } from './error'
|
|
15
|
+
|
|
16
|
+
export { ERRNO_MAP, fromNative } from './native'
|
|
17
|
+
export type { NativeOptions } from './native'
|
|
18
|
+
|
|
19
|
+
export { exitCodeFor, format } from './format'
|
|
20
|
+
export type { FormatKind, FormatOptions } from './format'
|