create-ssnode-cli 0.2.6 → 0.2.7
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/package.json +1 -1
- package/templates/backend/scripts/server.config.example.cjs +9 -0
- package/templates/web/package.json +4 -1
- package/templates/web/scripts/compress-src-images.cjs +332 -0
- package/templates/web/scripts/publish.cjs +280 -241
- package/templates/web/src/layouts/Background.module.less +0 -82
- package/templates/web/src/layouts/Background.tsx +26 -113
- package/templates/web/src/layouts/components/PageTabs/SortableTab.tsx +133 -0
- package/templates/web/src/layouts/components/PageTabs/index.module.less +105 -0
- package/templates/web/src/layouts/components/PageTabs/index.tsx +110 -0
- package/templates/web/src/layouts/components/PageTabs/usePageTabs.ts +224 -0
package/package.json
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Author Murphy.xie
|
|
3
|
+
* Date 2026-10-23 15:15:32
|
|
4
|
+
* LastEditors Murphy.xie
|
|
5
|
+
* LastEditTime 2026-11-04 14:19:13
|
|
6
|
+
* Description
|
|
7
|
+
*/
|
|
1
8
|
/*
|
|
2
9
|
* Description
|
|
3
10
|
*/
|
|
@@ -31,6 +38,7 @@ module.exports = {
|
|
|
31
38
|
// ── 后端服务目录(服务端 APP_ROOT:current/releases/data/logs 所在) ──
|
|
32
39
|
// 首次部署由 install.sh 在此目录下建 current/releases/data/logs 结构。
|
|
33
40
|
backendServiceDir: "/data/server/fullstack-template",
|
|
41
|
+
keepVersions: 1,
|
|
34
42
|
},
|
|
35
43
|
|
|
36
44
|
"machine-b": {
|
|
@@ -43,6 +51,7 @@ module.exports = {
|
|
|
43
51
|
},
|
|
44
52
|
// authMode=privateKey 时:server.privateKey = { privateKeyPath: '~/.ssh/your-key', passphrase: '' }
|
|
45
53
|
backendServiceDir: "/data/server/fullstack-template",
|
|
54
|
+
keepVersions: 1,
|
|
46
55
|
},
|
|
47
56
|
},
|
|
48
57
|
};
|
|
@@ -27,6 +27,9 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@ant-design/icons": "5.3.0",
|
|
30
|
+
"@dnd-kit/core": "^6.3.1",
|
|
31
|
+
"@dnd-kit/sortable": "^10.0.0",
|
|
32
|
+
"@dnd-kit/utilities": "^3.2.2",
|
|
30
33
|
"antd": "5.14.2",
|
|
31
34
|
"axios": "1.20.0",
|
|
32
35
|
"dayjs": "1.11.10",
|
|
@@ -75,4 +78,4 @@
|
|
|
75
78
|
"vite-plugin-chunk-split": "0.5.0",
|
|
76
79
|
"vite-plugin-svgr": "^5.2.0"
|
|
77
80
|
}
|
|
78
|
-
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* web/src 存量图片批压缩(视觉无损,构建产物瘦身)
|
|
4
|
+
*
|
|
5
|
+
* 背景:src 内设计稿类 PNG 大量超展示需求(最大 12.7MB、多张 1-3MB 级),
|
|
6
|
+
* 上传链已有 compress-image.ts 压缩(2026-10-15)管不到 src 静态资源,本脚本补存量。
|
|
7
|
+
*
|
|
8
|
+
* 用法(在 web/ 下运行):
|
|
9
|
+
* node scripts/compress-src-images.cjs # 交互询问是否保留原图(默认保留)
|
|
10
|
+
* node scripts/compress-src-images.cjs --dry-run # 只算不写,预览前后大小
|
|
11
|
+
* node scripts/compress-src-images.cjs --no-keep # 免交互,不留原图备份
|
|
12
|
+
* node scripts/compress-src-images.cjs --limit N # 只处理前 N 张(渐进验证,计实际解码尝试)
|
|
13
|
+
* node scripts/compress-src-images.cjs --dir <path> # 换扫描根(默认 src/;沙箱回归用)
|
|
14
|
+
*
|
|
15
|
+
* 前提:sharp 为 NATIVE 依赖,禁入 web 依赖(浏览器侧包),隔离安装(同 backend
|
|
16
|
+
* compress-column-images.cjs 先例):
|
|
17
|
+
* npm i --prefix /tmp/compress-libs sharp
|
|
18
|
+
* 脚本按 node_modules → /tmp/compress-libs 顺序找,找不到打印装法退出。
|
|
19
|
+
*
|
|
20
|
+
* 压缩参数与 web/src/utils/compress-image.ts、backend/scripts/compress-column-images.cjs
|
|
21
|
+
* 同语义,勿单边改:长边钳制 3840 / EXIF 方向烘焙 + 元数据剥离 / WebP q0.92 /
|
|
22
|
+
* 只有更小(原 95% 以下)才替换。「无损」按仓库既有口径 = 视觉无损(严格数学无损
|
|
23
|
+
* 对设计稿类位图收益≈0,两条既有链同取舍);编码器是 sharp(本地批处理)非浏览器
|
|
24
|
+
* canvas——参数一致、观测级等价。
|
|
25
|
+
*
|
|
26
|
+
* 换名方案(引用代码零改动):
|
|
27
|
+
* 压缩结果(WebP 字节)落**原文件名**(BG.png 名不变,import / css url() 全不动;
|
|
28
|
+
* <img>/css 背景按内容嗅探渲染,浏览器对图片不按扩展名强制 MIME);
|
|
29
|
+
* 原图改名 BG.png.orig 同目录保留(幂等标记 + 本机回退件,已 gitignore 勿入库)。
|
|
30
|
+
*
|
|
31
|
+
* 幂等 / 跳过规则(跳过只报告不动文件):
|
|
32
|
+
* 内容已是 WebP(RIFF..WEBP 魔数——不依赖 .orig 标记,--no-keep 模式重跑也安全)
|
|
33
|
+
* / 陈旧 .orig 标记(.orig 在但主文件非 WebP,多为 git 还原后残留,提示人工确认)
|
|
34
|
+
* / 非静态位图(gif 动图丢帧 / svg 非位图,扫描即过滤)/ 不超尺寸小图(≤300KB 且
|
|
35
|
+
* 长边 ≤3840)/ 压缩无收益(≥原图 95%)。
|
|
36
|
+
*
|
|
37
|
+
* 安全:
|
|
38
|
+
* - 开跑交互询问是否保留原图,默认保留;回退单张 = mv xxx.png.orig xxx.png,
|
|
39
|
+
* 整批走 git checkout(原图入库过,git 历史即真备份);
|
|
40
|
+
* - 写入走同目录 .new 临时文件 + 两步 rename(原图先改名、新图后顶上),崩溃不产
|
|
41
|
+
* 生半张图;开跑前先扫中断残留自修复(.orig 在主文件不在 = 接续换名或回滚原图,
|
|
42
|
+
* .new 残留 = 清理陈旧临时件);
|
|
43
|
+
* - 每张压缩结果落盘前重新解码校验尺寸一致(防截断/坏写)。
|
|
44
|
+
*/
|
|
45
|
+
"use strict";
|
|
46
|
+
|
|
47
|
+
const fs = require("node:fs");
|
|
48
|
+
const path = require("node:path");
|
|
49
|
+
const readline = require("node:readline");
|
|
50
|
+
|
|
51
|
+
// ── 与 compress-image.ts / compress-column-images.cjs 对齐的常量(勿单边改)──
|
|
52
|
+
const MAX_EDGE = 3840;
|
|
53
|
+
const WEBP_QUALITY = 92;
|
|
54
|
+
const SKIP_SIZE = 300 * 1024; // 不超尺寸的小图不重编码
|
|
55
|
+
const ADOPT_RATIO = 0.95; // 压缩结果须小于原图此比例才替换
|
|
56
|
+
const RASTER_RE = /\.(png|jpe?g|webp|bmp)$/i; // gif 动图 / svg 非位图,一律不动
|
|
57
|
+
const ORIG_SUFFIX = ".orig"; // 原图备份后缀(幂等标记 + 回退件)
|
|
58
|
+
const NEW_SUFFIX = ".new"; // 落盘临时后缀(同目录 rename 原子换名)
|
|
59
|
+
const DEFAULT_SCAN_DIR = path.resolve(__dirname, "..", "src");
|
|
60
|
+
|
|
61
|
+
// ── sharp 隔离加载(不入仓库)──
|
|
62
|
+
function loadSharp() {
|
|
63
|
+
const candidates = [
|
|
64
|
+
"sharp", // 万一通过 node_modules 全局可达(如本机 pnpm 已装)
|
|
65
|
+
path.resolve("/tmp/compress-libs/node_modules/sharp"), // 隔离安装位(同 backend 批迁模式)
|
|
66
|
+
];
|
|
67
|
+
for (const candidate of candidates) {
|
|
68
|
+
try {
|
|
69
|
+
// eslint-disable-next-line global-require, import/no-dynamic-require
|
|
70
|
+
return require(candidate);
|
|
71
|
+
} catch {
|
|
72
|
+
// 找不到换下一个候选位
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
console.error(
|
|
76
|
+
"缺 sharp(NATIVE 依赖,禁止入 web package.json——浏览器侧包)。隔离安装:\n" +
|
|
77
|
+
" npm i --prefix /tmp/compress-libs sharp",
|
|
78
|
+
);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 内容魔数是否已是 WebP('RIFF'....'WEBP')——幂等判据,不依赖文件名/备份标记 */
|
|
83
|
+
function isWebP(buf) {
|
|
84
|
+
return (
|
|
85
|
+
buf.length > 12 &&
|
|
86
|
+
buf[0] === 0x52 &&
|
|
87
|
+
buf[1] === 0x49 &&
|
|
88
|
+
buf[2] === 0x46 &&
|
|
89
|
+
buf[3] === 0x46 && // RIFF
|
|
90
|
+
buf[8] === 0x57 &&
|
|
91
|
+
buf[9] === 0x45 &&
|
|
92
|
+
buf[10] === 0x42 &&
|
|
93
|
+
buf[11] === 0x50 // WEBP
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 递归收集扫描树内全部文件(位图 / 备份残留 / 临时件分类在调用侧) */
|
|
98
|
+
function walkFiles(dir, out = []) {
|
|
99
|
+
for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
100
|
+
const p = path.join(dir, ent.name);
|
|
101
|
+
if (ent.isDirectory()) walkFiles(p, out);
|
|
102
|
+
else if (ent.isFile()) out.push(p);
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 开跑前自修复上次中断残留(顺序敏感:先 .orig 后 .new):
|
|
109
|
+
* .orig 在主文件不在:.new 在 = 压缩已落盘只差最后换名,接续完成;
|
|
110
|
+
* 否则崩在「原图已改名、新图未顶上」,回滚原图。
|
|
111
|
+
* .new 残留且主文件在:陈旧临时件,清掉。
|
|
112
|
+
*/
|
|
113
|
+
function repairInterrupted(allFiles) {
|
|
114
|
+
for (const f of allFiles.filter((x) => x.endsWith(ORIG_SUFFIX))) {
|
|
115
|
+
const main = f.slice(0, -ORIG_SUFFIX.length);
|
|
116
|
+
if (fs.existsSync(main)) continue; // 正常已压缩留备份态
|
|
117
|
+
const staged = main + NEW_SUFFIX;
|
|
118
|
+
if (fs.existsSync(staged)) {
|
|
119
|
+
fs.renameSync(staged, main);
|
|
120
|
+
console.log(`REPAIR 接续换名 ${path.basename(main)}`);
|
|
121
|
+
} else {
|
|
122
|
+
fs.renameSync(f, main);
|
|
123
|
+
console.log(`REPAIR 回滚原图 ${path.basename(main)}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
for (const f of allFiles.filter((x) => x.endsWith(NEW_SUFFIX))) {
|
|
127
|
+
if (!fs.existsSync(f)) continue; // 上一轮修复已消费
|
|
128
|
+
const main = f.slice(0, -NEW_SUFFIX.length);
|
|
129
|
+
if (fs.existsSync(main) || fs.existsSync(main + ORIG_SUFFIX)) {
|
|
130
|
+
fs.unlinkSync(f);
|
|
131
|
+
console.log(`REPAIR 清理临时件 ${path.basename(f)}`);
|
|
132
|
+
} else {
|
|
133
|
+
console.log(
|
|
134
|
+
`WARN 孤立临时件 ${path.basename(f)}(主文件/备份均不在,人工确认后删)`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** 交互询问是否保留原图备份;回车/非 n 均视为保留(默认保留);非 TTY 默认保留 */
|
|
141
|
+
function askKeepOriginal() {
|
|
142
|
+
return new Promise((resolve) => {
|
|
143
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return resolve(true);
|
|
144
|
+
const rl = readline.createInterface({
|
|
145
|
+
input: process.stdin,
|
|
146
|
+
output: process.stdout,
|
|
147
|
+
});
|
|
148
|
+
rl.question("是否保留原图备份(同目录 *.orig)?[Y/n] ", (ans) => {
|
|
149
|
+
rl.close();
|
|
150
|
+
resolve(!/^\s*n(o)?\s*$/i.test(ans || ""));
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const argv = process.argv.slice(2);
|
|
156
|
+
const flag = (name) => argv.includes(`--${name}`);
|
|
157
|
+
const opt = (name, def) => {
|
|
158
|
+
const i = argv.indexOf(`--${name}`);
|
|
159
|
+
return i >= 0 ? argv[i + 1] : def;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const dryRun = flag("dry-run");
|
|
163
|
+
const limit = parseInt(opt("limit", "0"), 10) || 0;
|
|
164
|
+
const scanDir = path.resolve(opt("dir", DEFAULT_SCAN_DIR));
|
|
165
|
+
|
|
166
|
+
(async () => {
|
|
167
|
+
if (!fs.existsSync(scanDir)) {
|
|
168
|
+
console.error(`扫描目录不存在:${scanDir}`);
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
const sharp = loadSharp();
|
|
172
|
+
|
|
173
|
+
const allFiles = walkFiles(scanDir);
|
|
174
|
+
repairInterrupted(allFiles);
|
|
175
|
+
const images = allFiles.filter((f) => RASTER_RE.test(f));
|
|
176
|
+
|
|
177
|
+
let keepOrig = true;
|
|
178
|
+
if (!dryRun) {
|
|
179
|
+
if (flag("no-keep")) keepOrig = false;
|
|
180
|
+
else if (flag("keep")) keepOrig = true;
|
|
181
|
+
else keepOrig = await askKeepOriginal();
|
|
182
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
183
|
+
console.log(
|
|
184
|
+
"[src-compress] 非交互环境(非 TTY),按默认保留原图(免交互不留备份用 --no-keep)",
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const rel = (f) => path.relative(scanDir, f);
|
|
190
|
+
const fmtKB = (n) => `${(n / 1024).toFixed(0)}KB`;
|
|
191
|
+
console.log(
|
|
192
|
+
`[src-compress] ${scanDir} 位图 ${images.length} 张 ${
|
|
193
|
+
dryRun ? "DRY-RUN" : keepOrig ? "保留原图(.orig)" : "不留原图"
|
|
194
|
+
}${limit ? ` limit=${limit}` : ""}`,
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const stats = {
|
|
198
|
+
compressed: 0,
|
|
199
|
+
noGain: 0,
|
|
200
|
+
smallSkip: 0,
|
|
201
|
+
webpDone: 0,
|
|
202
|
+
staleMark: 0,
|
|
203
|
+
failed: 0,
|
|
204
|
+
};
|
|
205
|
+
let beforeTotal = 0;
|
|
206
|
+
let saved = 0;
|
|
207
|
+
let processed = 0;
|
|
208
|
+
|
|
209
|
+
for (const file of images) {
|
|
210
|
+
if (limit && processed >= limit) {
|
|
211
|
+
console.log(`[src-compress] --limit ${limit} 已处理满,停止(其余未动)`);
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
// ── 幂等跳过 ──
|
|
216
|
+
const buf = fs.readFileSync(file);
|
|
217
|
+
if (isWebP(buf)) {
|
|
218
|
+
stats.webpDone += 1;
|
|
219
|
+
console.log(`SKIP 已压缩 ${fmtKB(buf.length)} ${rel(file)}`);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (fs.existsSync(file + ORIG_SUFFIX)) {
|
|
223
|
+
stats.staleMark += 1;
|
|
224
|
+
console.log(
|
|
225
|
+
`SKIP 陈旧标记 ${rel(
|
|
226
|
+
file,
|
|
227
|
+
)}(.orig 在但主文件非 WebP——git 还原后残留?确认后删 .orig 重跑)`,
|
|
228
|
+
);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
processed += 1;
|
|
232
|
+
|
|
233
|
+
// ── 压缩(语义同 compress-image.ts)──
|
|
234
|
+
const meta = await sharp(buf).metadata();
|
|
235
|
+
const scale = Math.min(
|
|
236
|
+
1,
|
|
237
|
+
MAX_EDGE / Math.max(meta.width || 1, meta.height || 1),
|
|
238
|
+
);
|
|
239
|
+
if (scale === 1 && buf.length <= SKIP_SIZE) {
|
|
240
|
+
stats.smallSkip += 1;
|
|
241
|
+
console.log(`SKIP 小图 ${fmtKB(buf.length)} ${rel(file)}`);
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const width = Math.max(1, Math.round((meta.width || 1) * scale));
|
|
245
|
+
const height = Math.max(1, Math.round((meta.height || 1) * scale));
|
|
246
|
+
// rotate() 无参 = 按 EXIF 方向摆正(等效 canvas 管线的 from-image),sharp 输出默认剥元数据
|
|
247
|
+
const webp = await sharp(buf)
|
|
248
|
+
.rotate()
|
|
249
|
+
.resize({
|
|
250
|
+
width,
|
|
251
|
+
height,
|
|
252
|
+
fit: "inside",
|
|
253
|
+
withoutEnlargement: true,
|
|
254
|
+
kernel: "lanczos3",
|
|
255
|
+
})
|
|
256
|
+
.webp({ quality: WEBP_QUALITY })
|
|
257
|
+
.toBuffer();
|
|
258
|
+
if (webp.length >= buf.length * ADOPT_RATIO) {
|
|
259
|
+
stats.noGain += 1;
|
|
260
|
+
console.log(
|
|
261
|
+
`SKIP 无收益 原${fmtKB(buf.length)} → ${fmtKB(
|
|
262
|
+
webp.length,
|
|
263
|
+
)} ${rel(file)}`,
|
|
264
|
+
);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// 落盘前自校验:压缩结果能重新解码且长边一致(防截断/坏写;宽高允许 ±4px
|
|
269
|
+
// 的 WebP 有损重编码舍入误差——除以质量 q 的像素块取整,实测 3840→3836)
|
|
270
|
+
const verify = await sharp(webp).metadata();
|
|
271
|
+
const verifyLong = Math.max(verify.width || 0, verify.height || 0);
|
|
272
|
+
const expectLong = Math.max(width, height);
|
|
273
|
+
if (
|
|
274
|
+
!verify.width ||
|
|
275
|
+
!verify.height ||
|
|
276
|
+
Math.abs(verifyLong - expectLong) > 4
|
|
277
|
+
) {
|
|
278
|
+
throw new Error(
|
|
279
|
+
`压缩结果自校验失败 ${verify.width}×${verify.height}(期望 ~${width}×${height})`,
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const line = `${fmtKB(buf.length)} → ${fmtKB(webp.length)}${
|
|
284
|
+
scale < 1 ? ` (${meta.width}×${meta.height} → ${width}×${height})` : ""
|
|
285
|
+
} ${rel(file)}`;
|
|
286
|
+
if (dryRun) {
|
|
287
|
+
stats.compressed += 1;
|
|
288
|
+
beforeTotal += buf.length;
|
|
289
|
+
saved += buf.length - webp.length;
|
|
290
|
+
console.log(`DRY ${line}`);
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ── 同目录 .new + 两步 rename 原子换名(崩溃不产生半张图)──
|
|
295
|
+
fs.writeFileSync(file + NEW_SUFFIX, webp);
|
|
296
|
+
if (keepOrig) fs.renameSync(file, file + ORIG_SUFFIX);
|
|
297
|
+
fs.renameSync(file + NEW_SUFFIX, file);
|
|
298
|
+
stats.compressed += 1;
|
|
299
|
+
beforeTotal += buf.length;
|
|
300
|
+
saved += buf.length - webp.length;
|
|
301
|
+
console.log(
|
|
302
|
+
`OK ${line}${
|
|
303
|
+
keepOrig ? `(原图留 ${path.basename(file)}${ORIG_SUFFIX})` : ""
|
|
304
|
+
}`,
|
|
305
|
+
);
|
|
306
|
+
} catch (err) {
|
|
307
|
+
stats.failed += 1;
|
|
308
|
+
console.error(`FAIL ${rel(file)} —— ${err.message}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const fmtMB = (n) => `${(n / 1024 / 1024).toFixed(1)}MB`;
|
|
313
|
+
console.log(`\n[src-compress] 完成:${JSON.stringify(stats)}`);
|
|
314
|
+
if (stats.compressed) {
|
|
315
|
+
console.log(
|
|
316
|
+
`[src-compress] 收益:${fmtMB(beforeTotal)} → ${fmtMB(
|
|
317
|
+
beforeTotal - saved,
|
|
318
|
+
)}(省 ${fmtMB(saved)})`,
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
if (!dryRun && stats.compressed) {
|
|
322
|
+
console.log(
|
|
323
|
+
keepOrig
|
|
324
|
+
? "[src-compress] 回退:单张 mv xxx.png.orig xxx.png;整批 git checkout -- <目录>。.orig 已 gitignore,勿入库。"
|
|
325
|
+
: "[src-compress] 未留备份,回退只能走 git checkout -- <目录>。",
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
if (stats.failed) process.exitCode = 1;
|
|
329
|
+
})().catch((e) => {
|
|
330
|
+
console.error("FATAL", e);
|
|
331
|
+
process.exit(1);
|
|
332
|
+
});
|