@gadmin2n/cli 0.0.157 → 0.0.159
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/actions/prisma.action.js
CHANGED
|
@@ -13,6 +13,7 @@ exports.PrismaAction = void 0;
|
|
|
13
13
|
const chalk = require("chalk");
|
|
14
14
|
const fs = require("fs");
|
|
15
15
|
const path_1 = require("path");
|
|
16
|
+
const sync_fs_1 = require("../lib/utils/sync-fs");
|
|
16
17
|
const abstract_action_1 = require("./abstract.action");
|
|
17
18
|
const shell = require("shelljs");
|
|
18
19
|
const readline = require("readline");
|
|
@@ -82,7 +83,7 @@ class PrismaAction extends abstract_action_1.AbstractAction {
|
|
|
82
83
|
new shell.ShellString(result).toEnd('server/prisma/schema.prisma');
|
|
83
84
|
}
|
|
84
85
|
handle(inputs, options) {
|
|
85
|
-
var _a, _b
|
|
86
|
+
var _a, _b;
|
|
86
87
|
return __awaiter(this, void 0, void 0, function* () {
|
|
87
88
|
// 检查 prisma schema 配置是否存在
|
|
88
89
|
const hasPrismaConfig = shell.test('-e', 'config/schema.prisma') ||
|
|
@@ -93,107 +94,203 @@ class PrismaAction extends abstract_action_1.AbstractAction {
|
|
|
93
94
|
process.exit(-1);
|
|
94
95
|
}
|
|
95
96
|
const devMode = !!((_a = options.find((option) => option.name === 'dev')) === null || _a === void 0 ? void 0 : _a.value);
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
const verbose = !!((_b = options.find((option) => option.name === 'verbose')) === null || _b === void 0 ? void 0 : _b.value);
|
|
98
|
+
// 分发过程按类别记账,收尾统一打摘要 / --verbose 打完整清单
|
|
99
|
+
const stats = {
|
|
100
|
+
serverModulesIndex: [],
|
|
101
|
+
serverModulesNew: [],
|
|
102
|
+
serverModulesSkipped: [],
|
|
103
|
+
routesNew: [],
|
|
104
|
+
routesSkipped: [],
|
|
105
|
+
generated: [],
|
|
106
|
+
generatedUnchanged: [],
|
|
107
|
+
orphansRemoved: [], // web/src/generated/** 孤儿清理
|
|
108
|
+
};
|
|
109
|
+
// 构建合并的 schema(两种模式都需要重建 schema.prisma)
|
|
99
110
|
this.buildMergedSchema();
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
else if (webOnly)
|
|
122
|
-
generateScript = 'npm run generate:web';
|
|
123
|
-
else if (serverOnly)
|
|
124
|
-
generateScript = 'npm run generate:server';
|
|
125
|
-
const { code } = shell.cd('server/').exec(generateScript);
|
|
111
|
+
// Staging 目录:generator 全量写入到 node_modules/.cache/*,CLI 再增量分发。
|
|
112
|
+
// 不放在 src/ 下,避免任何 watcher 感知到中间态的进出。
|
|
113
|
+
// 用绝对路径以便 (a) 在 shell.cd('server/') 之后仍指向正确位置;
|
|
114
|
+
// (b) 通过 env 传给 generator 子进程时无歧义。
|
|
115
|
+
const cwd = process.cwd();
|
|
116
|
+
const WEB_STAGING = (0, path_1.resolve)(cwd, 'web/node_modules/.cache/gadmin-web');
|
|
117
|
+
const SERVER_STAGING = (0, path_1.resolve)(cwd, 'server/node_modules/.cache/gadmin-server');
|
|
118
|
+
const WEB_ROUTES_DIR = (0, path_1.resolve)(cwd, 'web/src/routes');
|
|
119
|
+
// 每轮清空对应 staging,让「本轮 staging = 本轮完整产物」这个不变量成立,
|
|
120
|
+
// 后续孤儿清理直接依赖它。父目录(如 web/node_modules/.cache/)不存在时先创建,
|
|
121
|
+
// 避免 generator 首次运行时因缺目录报错。
|
|
122
|
+
fs.rmSync(WEB_STAGING, { recursive: true, force: true });
|
|
123
|
+
fs.mkdirSync(WEB_STAGING, { recursive: true });
|
|
124
|
+
fs.rmSync(SERVER_STAGING, { recursive: true, force: true });
|
|
125
|
+
fs.mkdirSync(SERVER_STAGING, { recursive: true });
|
|
126
|
+
// 执行代码生成(走 npm run 脚本)。通过 env 传 staging 路径,
|
|
127
|
+
// generator 会优先使用 env 覆盖 .prisma schema 里的 output 配置。
|
|
128
|
+
const generateScript = devMode ? 'npm run generate:dev' : 'npm run generate';
|
|
129
|
+
const { code } = shell.cd('server/').exec(generateScript, {
|
|
130
|
+
env: Object.assign(Object.assign({}, process.env), { GADMIN_WEB_STAGING: WEB_STAGING, GADMIN_SERVER_STAGING: SERVER_STAGING, GADMIN_WEB_ROUTES_DIR: WEB_ROUTES_DIR }),
|
|
131
|
+
});
|
|
126
132
|
if (code !== 0) {
|
|
127
133
|
process.exit(code);
|
|
128
134
|
}
|
|
129
135
|
shell.cd('../');
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
136
|
+
// 分发策略:先集中写 server/,再集中写 web/。
|
|
137
|
+
// 目的是把 nest --watch 的 2 波重启合成 1 波,把 Vite 的 4 波 HMR 合成 1 波。
|
|
138
|
+
//
|
|
139
|
+
// staging 结构参考:
|
|
140
|
+
// WEB_STAGING/
|
|
141
|
+
// routes/{model}/*.tsx → 首次落地到 web/src/routes/{model}
|
|
142
|
+
// props/{model}/*.ts, resources.tsx, models.index.tsx → diff-copy 到 web/src/generated/
|
|
143
|
+
// SERVER_STAGING/
|
|
144
|
+
// modules.index.ts → diff-copy 到 server/src/generated/modules.index.ts
|
|
145
|
+
// {model}/dto/{model}.form.validator.ts → diff-copy 到 web/src/generated/props/{model}/form.validator.ts(跨端)
|
|
146
|
+
// {model}/**(其余) → 首次落地到 server/src/modules/{model}/**
|
|
147
|
+
// 收集 server staging 里的 model 列表:凡有 dto/{name}.form.validator.ts 的一级子目录即视为 model
|
|
148
|
+
const stagingModels = fs.existsSync(SERVER_STAGING)
|
|
149
|
+
? fs
|
|
150
|
+
.readdirSync(SERVER_STAGING, { withFileTypes: true })
|
|
151
|
+
.filter((e) => e.isDirectory())
|
|
152
|
+
.map((e) => e.name)
|
|
153
|
+
.filter((name) => fs.existsSync((0, path_1.join)(SERVER_STAGING, name, 'dto', `${name}.form.validator.ts`)))
|
|
154
|
+
: [];
|
|
155
|
+
// ============ 阶段 1:server 集中写入 ============
|
|
156
|
+
if (fs.existsSync(SERVER_STAGING)) {
|
|
157
|
+
// 1.1 modules.index.ts —— 顶层入口
|
|
158
|
+
const modulesIndexSrc = (0, path_1.join)(SERVER_STAGING, 'modules.index.ts');
|
|
159
|
+
if (fs.existsSync(modulesIndexSrc)) {
|
|
160
|
+
const rel = 'server/src/generated/modules.index.ts';
|
|
161
|
+
if ((0, sync_fs_1.copyFileIfChanged)(modulesIndexSrc, rel)) {
|
|
162
|
+
stats.serverModulesIndex.push(rel);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// 1.2 逐 model 首次落地到 server/src/modules/{model}(跳过 form.validator,它归 web 阶段)
|
|
166
|
+
for (const modelName of stagingModels) {
|
|
167
|
+
const modelStaging = (0, path_1.join)(SERVER_STAGING, modelName);
|
|
168
|
+
const validatorInStaging = (0, path_1.join)('dto', `${modelName}.form.validator.ts`);
|
|
169
|
+
for (const rel of (0, sync_fs_1.listFilesRecursive)(modelStaging)) {
|
|
170
|
+
if (rel === validatorInStaging)
|
|
171
|
+
continue;
|
|
172
|
+
const dest = (0, path_1.join)('server/src/modules', modelName, rel);
|
|
173
|
+
if (fs.existsSync(dest)) {
|
|
174
|
+
stats.serverModulesSkipped.push(dest);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
fs.mkdirSync((0, path_1.dirname)(dest), { recursive: true });
|
|
178
|
+
fs.copyFileSync((0, path_1.join)(modelStaging, rel), dest);
|
|
179
|
+
stats.serverModulesNew.push(dest);
|
|
180
|
+
}
|
|
147
181
|
}
|
|
148
182
|
}
|
|
149
|
-
//
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
.
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
fs.rmdirSync(modulesSubDir);
|
|
175
|
-
}
|
|
183
|
+
// ============ 阶段 2:web 集中写入 ============
|
|
184
|
+
const webKeep = new Set();
|
|
185
|
+
// 2.1 web staging 主产物
|
|
186
|
+
if (fs.existsSync(WEB_STAGING)) {
|
|
187
|
+
for (const rel of (0, sync_fs_1.listFilesRecursive)(WEB_STAGING)) {
|
|
188
|
+
const src = (0, path_1.join)(WEB_STAGING, rel);
|
|
189
|
+
if (rel.startsWith('routes' + path_1.sep) || rel.startsWith('routes/')) {
|
|
190
|
+
// 首次落地:不覆盖用户已编辑的页面
|
|
191
|
+
const destRoutes = (0, path_1.join)('web/src/routes', rel.replace(/^routes[\/\\]/, ''));
|
|
192
|
+
if (!fs.existsSync(destRoutes)) {
|
|
193
|
+
fs.mkdirSync((0, path_1.dirname)(destRoutes), { recursive: true });
|
|
194
|
+
fs.copyFileSync(src, destRoutes);
|
|
195
|
+
stats.routesNew.push(destRoutes);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
stats.routesSkipped.push(destRoutes);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
const dest = (0, path_1.join)('web/src/generated', rel);
|
|
203
|
+
if ((0, sync_fs_1.copyFileIfChanged)(src, dest)) {
|
|
204
|
+
stats.generated.push(dest);
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
stats.generatedUnchanged.push(dest);
|
|
176
208
|
}
|
|
209
|
+
webKeep.add(rel);
|
|
177
210
|
}
|
|
178
|
-
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// 2.2 form.validator.ts(server generator 产出,需搬到 web/src/generated/props/{model}/)
|
|
214
|
+
for (const modelName of stagingModels) {
|
|
215
|
+
const validatorRel = `props/${modelName}/form.validator.ts`;
|
|
216
|
+
const dest = (0, path_1.join)('web/src/generated', validatorRel);
|
|
217
|
+
if ((0, sync_fs_1.copyFileIfChanged)((0, path_1.join)(SERVER_STAGING, modelName, 'dto', `${modelName}.form.validator.ts`), dest)) {
|
|
218
|
+
stats.generated.push(dest);
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
stats.generatedUnchanged.push(dest);
|
|
222
|
+
}
|
|
223
|
+
webKeep.add(validatorRel);
|
|
224
|
+
}
|
|
225
|
+
// 2.3 Prisma / config 类型
|
|
226
|
+
fs.mkdirSync('web/src/generated/types', { recursive: true });
|
|
227
|
+
const prismaSrc = 'server/node_modules/.prisma/client/index.d.ts';
|
|
228
|
+
const prismaRel = 'types/prisma.types.d.ts';
|
|
229
|
+
let prismaContent = fs.readFileSync(prismaSrc, 'utf8');
|
|
230
|
+
prismaContent = prismaContent.replace("import * as runtime from '@prisma/client/runtime/index';", 'declare const runtime : any');
|
|
231
|
+
prismaContent = prismaContent.replace(/bigint/g, 'number');
|
|
232
|
+
const prismaDest = (0, path_1.join)('web/src/generated', prismaRel);
|
|
233
|
+
if ((0, sync_fs_1.writeFileIfChanged)(prismaDest, prismaContent)) {
|
|
234
|
+
stats.generated.push(prismaDest);
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
stats.generatedUnchanged.push(prismaDest);
|
|
238
|
+
}
|
|
239
|
+
webKeep.add(prismaRel);
|
|
240
|
+
const configRel = 'types/config.types.d.ts';
|
|
241
|
+
const configDest = (0, path_1.join)('web/src/generated', configRel);
|
|
242
|
+
if ((0, sync_fs_1.copyFileIfChanged)('config/.types.d.ts', configDest)) {
|
|
243
|
+
stats.generated.push(configDest);
|
|
179
244
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
fs.mkdirSync('web/src/generated/types', { recursive: true });
|
|
183
|
-
fs.copyFileSync('server/node_modules/.prisma/client/index.d.ts', 'web/src/generated/types/prisma.types.d.ts');
|
|
184
|
-
const prismaRegex = new RegExp("import \\* as runtime from '@prisma/client/runtime/index';");
|
|
185
|
-
shell.sed('-i', prismaRegex, 'declare const runtime : any', 'web/src/generated/types/prisma.types.d.ts');
|
|
186
|
-
shell.sed('-i', new RegExp('bigint', 'g'), 'number', 'web/src/generated/types/prisma.types.d.ts');
|
|
187
|
-
fs.copyFileSync('config/.types.d.ts', 'web/src/generated/types/config.types.d.ts');
|
|
245
|
+
else {
|
|
246
|
+
stats.generatedUnchanged.push(configDest);
|
|
188
247
|
}
|
|
248
|
+
webKeep.add(configRel);
|
|
249
|
+
// 2.4 孤儿清理:删除 web/src/generated 下所有本轮未产出的文件(例如被删除的 model)
|
|
250
|
+
stats.orphansRemoved = (0, sync_fs_1.pruneOrphans)('web/src/generated', webKeep).map((r) => (0, path_1.join)('web/src/generated', r));
|
|
251
|
+
// 打印摘要 / 完整清单
|
|
252
|
+
this.printReport(stats, verbose);
|
|
189
253
|
// 分发全部完成后,若 server/package.json 定义了 postgadminGenerate,
|
|
190
254
|
// 触发它。项目侧可用来做 dev server 通知、缓存清理等自定义收尾。
|
|
191
|
-
|
|
192
|
-
this.runProjectPostHook({ webOnly, serverOnly });
|
|
255
|
+
this.runProjectPostHook();
|
|
193
256
|
process.exit(0);
|
|
194
257
|
});
|
|
195
258
|
}
|
|
196
|
-
|
|
259
|
+
printReport(stats, verbose) {
|
|
260
|
+
const serverWritten = stats.serverModulesIndex.length + stats.serverModulesNew.length;
|
|
261
|
+
const webWritten = stats.generated.length + stats.routesNew.length;
|
|
262
|
+
console.log('');
|
|
263
|
+
console.log(chalk.bold('gadmin2 g prisma:'));
|
|
264
|
+
console.log(` ${chalk.cyan('server')}: ${serverWritten} written` +
|
|
265
|
+
` (index=${stats.serverModulesIndex.length},` +
|
|
266
|
+
` modules-new=${stats.serverModulesNew.length}),` +
|
|
267
|
+
` ${stats.serverModulesSkipped.length} skipped`);
|
|
268
|
+
console.log(` ${chalk.cyan('web')}: ${webWritten} written` +
|
|
269
|
+
` (routes-new=${stats.routesNew.length},` +
|
|
270
|
+
` generated=${stats.generated.length}),` +
|
|
271
|
+
` ${stats.routesSkipped.length + stats.generatedUnchanged.length} unchanged,` +
|
|
272
|
+
` ${stats.orphansRemoved.length} orphan removed`);
|
|
273
|
+
if (!verbose) {
|
|
274
|
+
console.log(chalk.gray(' (use -v / --verbose to list files)'));
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const printGroup = (title, list, color) => {
|
|
278
|
+
if (list.length === 0)
|
|
279
|
+
return;
|
|
280
|
+
console.log(chalk.bold(`\n ${title} (${list.length}):`));
|
|
281
|
+
for (const p of list)
|
|
282
|
+
console.log(' ' + color(p));
|
|
283
|
+
};
|
|
284
|
+
printGroup('server modules.index (diff-write)', stats.serverModulesIndex, chalk.green);
|
|
285
|
+
printGroup('server modules (new)', stats.serverModulesNew, chalk.green);
|
|
286
|
+
printGroup('server modules (skipped, exists)', stats.serverModulesSkipped, chalk.gray);
|
|
287
|
+
printGroup('web routes (new)', stats.routesNew, chalk.green);
|
|
288
|
+
printGroup('web routes (skipped, exists)', stats.routesSkipped, chalk.gray);
|
|
289
|
+
printGroup('web generated (written)', stats.generated, chalk.green);
|
|
290
|
+
printGroup('web generated (unchanged)', stats.generatedUnchanged, chalk.gray);
|
|
291
|
+
printGroup('web orphans removed', stats.orphansRemoved, chalk.red);
|
|
292
|
+
}
|
|
293
|
+
runProjectPostHook() {
|
|
197
294
|
var _a;
|
|
198
295
|
const pkgPath = 'server/package.json';
|
|
199
296
|
let pkg;
|
|
@@ -205,12 +302,11 @@ class PrismaAction extends abstract_action_1.AbstractAction {
|
|
|
205
302
|
}
|
|
206
303
|
if (!((_a = pkg === null || pkg === void 0 ? void 0 : pkg.scripts) === null || _a === void 0 ? void 0 : _a.postgadminGenerate))
|
|
207
304
|
return;
|
|
208
|
-
|
|
209
|
-
console.log(chalk.gray(`Running postgadminGenerate hook (mode=${mode}) ...`));
|
|
305
|
+
console.log(chalk.gray('Running postgadminGenerate hook ...'));
|
|
210
306
|
const { code } = shell
|
|
211
307
|
.cd('server/')
|
|
212
308
|
.exec('npm run --silent postgadminGenerate', {
|
|
213
|
-
env: Object.assign(
|
|
309
|
+
env: Object.assign({}, process.env),
|
|
214
310
|
});
|
|
215
311
|
shell.cd('../');
|
|
216
312
|
if (code !== 0) {
|
|
@@ -32,6 +32,7 @@ class GenerateCommand extends abstract_command_1.AbstractCommand {
|
|
|
32
32
|
return { value: false, passedAsInput: true };
|
|
33
33
|
})
|
|
34
34
|
.option('-c, --collection [collectionName]', 'Schematics collection to use.')
|
|
35
|
+
.option('-v, --verbose', 'Print each affected src/ file (prisma).')
|
|
35
36
|
.action((schematic, name, path, command) => __awaiter(this, void 0, void 0, function* () {
|
|
36
37
|
const options = [];
|
|
37
38
|
options.push({ name: 'dry-run', value: !!command.dryRun });
|
|
@@ -64,6 +65,7 @@ class GenerateCommand extends abstract_command_1.AbstractCommand {
|
|
|
64
65
|
keepInputNameFormat: true,
|
|
65
66
|
},
|
|
66
67
|
});
|
|
68
|
+
options.push({ name: 'verbose', value: !!command.verbose });
|
|
67
69
|
const inputs = [];
|
|
68
70
|
inputs.push({ name: 'schematic', value: schematic });
|
|
69
71
|
inputs.push({ name: 'name', value: name });
|
|
@@ -29,15 +29,15 @@ class GadminCollection extends abstract_collection_1.AbstractCollection {
|
|
|
29
29
|
execute: { get: () => super.execute }
|
|
30
30
|
});
|
|
31
31
|
return __awaiter(this, void 0, void 0, function* () {
|
|
32
|
-
if (['prisma', 'prisma:dev'
|
|
32
|
+
if (['prisma', 'prisma:dev'].includes(name)) {
|
|
33
33
|
const prisma = new prisma_action_1.PrismaAction();
|
|
34
34
|
const opts = [];
|
|
35
35
|
if (name.includes(':dev'))
|
|
36
36
|
opts.push({ name: 'dev', value: true });
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
const verbose = options.find((o) => o.name === 'verbose');
|
|
38
|
+
if (verbose && verbose.value) {
|
|
39
|
+
opts.push({ name: 'verbose', value: true });
|
|
40
|
+
}
|
|
41
41
|
return yield prisma.handle([], opts);
|
|
42
42
|
}
|
|
43
43
|
const schematic = this.validate(name);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
/**
|
|
3
|
+
* 增量文件同步工具,专为「Prisma 生成产物 → dev server 目录」场景设计。
|
|
4
|
+
*
|
|
5
|
+
* 目标:只在内容真正变化时触发写操作,避免 HMR/dev server 因 mtime 抖动而
|
|
6
|
+
* 全量重扫。所有工具都基于「按字节比对旧内容」实现,不依赖 hash 与 manifest。
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* 内容一致时不写入。
|
|
10
|
+
* @returns true 表示实际写入了文件;false 表示内容未变、跳过写入
|
|
11
|
+
*/
|
|
12
|
+
export declare function writeFileIfChanged(filePath: string, content: string | Buffer): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* 内容一致时不写入。用于二进制/文本无差别的文件复制。
|
|
15
|
+
* @returns true 表示实际复制;false 表示内容未变、跳过
|
|
16
|
+
*/
|
|
17
|
+
export declare function copyFileIfChanged(src: string, dest: string): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* 递归列出目录下所有文件(相对路径)。目录不存在时返回空数组。
|
|
20
|
+
*/
|
|
21
|
+
export declare function listFilesRecursive(dir: string): string[];
|
|
22
|
+
/**
|
|
23
|
+
* 删除文件;若父目录变空则一并删除(向上递归到 stopAt 为止,不含 stopAt)。
|
|
24
|
+
*/
|
|
25
|
+
export declare function removeFileAndPruneEmptyDirs(filePath: string, stopAt: string): void;
|
|
26
|
+
/**
|
|
27
|
+
* 把 srcDir 的内容增量同步到 destDir:
|
|
28
|
+
* - 新增/修改:按 diff copy 写入
|
|
29
|
+
* - 删除:destDir 中不在 srcDir 里的文件会被删除(以及空父目录)
|
|
30
|
+
*
|
|
31
|
+
* 注意:destDir 只由本函数管理时才可安全调用(例如 web/src/generated)。
|
|
32
|
+
*/
|
|
33
|
+
export declare function syncDir(srcDir: string, destDir: string): {
|
|
34
|
+
written: number;
|
|
35
|
+
deleted: number;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* 删除 destDir 下所有不在 keepFiles 里的文件(以及随之空掉的父目录)。
|
|
39
|
+
* 用于「本轮 model 目录被删」的孤儿清理场景。
|
|
40
|
+
* @returns 被删除的文件相对路径列表(相对 destDir)
|
|
41
|
+
*/
|
|
42
|
+
export declare function pruneOrphans(destDir: string, keepFiles: Set<string>): string[];
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.pruneOrphans = exports.syncDir = exports.removeFileAndPruneEmptyDirs = exports.listFilesRecursive = exports.copyFileIfChanged = exports.writeFileIfChanged = void 0;
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
/**
|
|
7
|
+
* 增量文件同步工具,专为「Prisma 生成产物 → dev server 目录」场景设计。
|
|
8
|
+
*
|
|
9
|
+
* 目标:只在内容真正变化时触发写操作,避免 HMR/dev server 因 mtime 抖动而
|
|
10
|
+
* 全量重扫。所有工具都基于「按字节比对旧内容」实现,不依赖 hash 与 manifest。
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* 内容一致时不写入。
|
|
14
|
+
* @returns true 表示实际写入了文件;false 表示内容未变、跳过写入
|
|
15
|
+
*/
|
|
16
|
+
function writeFileIfChanged(filePath, content) {
|
|
17
|
+
try {
|
|
18
|
+
const prev = fs.readFileSync(filePath);
|
|
19
|
+
const next = Buffer.isBuffer(content) ? content : Buffer.from(content);
|
|
20
|
+
if (prev.equals(next))
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
catch (_a) {
|
|
24
|
+
// 目标文件不存在 → 走写入分支
|
|
25
|
+
}
|
|
26
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
27
|
+
fs.writeFileSync(filePath, content);
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
exports.writeFileIfChanged = writeFileIfChanged;
|
|
31
|
+
/**
|
|
32
|
+
* 内容一致时不写入。用于二进制/文本无差别的文件复制。
|
|
33
|
+
* @returns true 表示实际复制;false 表示内容未变、跳过
|
|
34
|
+
*/
|
|
35
|
+
function copyFileIfChanged(src, dest) {
|
|
36
|
+
const buf = fs.readFileSync(src);
|
|
37
|
+
return writeFileIfChanged(dest, buf);
|
|
38
|
+
}
|
|
39
|
+
exports.copyFileIfChanged = copyFileIfChanged;
|
|
40
|
+
/**
|
|
41
|
+
* 递归列出目录下所有文件(相对路径)。目录不存在时返回空数组。
|
|
42
|
+
*/
|
|
43
|
+
function listFilesRecursive(dir) {
|
|
44
|
+
if (!fs.existsSync(dir))
|
|
45
|
+
return [];
|
|
46
|
+
const out = [];
|
|
47
|
+
const walk = (rel) => {
|
|
48
|
+
const abs = path.join(dir, rel);
|
|
49
|
+
for (const entry of fs.readdirSync(abs, { withFileTypes: true })) {
|
|
50
|
+
const nextRel = rel ? path.join(rel, entry.name) : entry.name;
|
|
51
|
+
if (entry.isDirectory()) {
|
|
52
|
+
walk(nextRel);
|
|
53
|
+
}
|
|
54
|
+
else if (entry.isFile()) {
|
|
55
|
+
out.push(nextRel);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
walk('');
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
exports.listFilesRecursive = listFilesRecursive;
|
|
63
|
+
/**
|
|
64
|
+
* 删除文件;若父目录变空则一并删除(向上递归到 stopAt 为止,不含 stopAt)。
|
|
65
|
+
*/
|
|
66
|
+
function removeFileAndPruneEmptyDirs(filePath, stopAt) {
|
|
67
|
+
try {
|
|
68
|
+
fs.rmSync(filePath, { force: true });
|
|
69
|
+
}
|
|
70
|
+
catch (_a) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
let dir = path.dirname(filePath);
|
|
74
|
+
const stopAbs = path.resolve(stopAt);
|
|
75
|
+
while (path.resolve(dir).startsWith(stopAbs) && path.resolve(dir) !== stopAbs) {
|
|
76
|
+
try {
|
|
77
|
+
const entries = fs.readdirSync(dir);
|
|
78
|
+
if (entries.length > 0)
|
|
79
|
+
return;
|
|
80
|
+
fs.rmdirSync(dir);
|
|
81
|
+
}
|
|
82
|
+
catch (_b) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
dir = path.dirname(dir);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
exports.removeFileAndPruneEmptyDirs = removeFileAndPruneEmptyDirs;
|
|
89
|
+
/**
|
|
90
|
+
* 把 srcDir 的内容增量同步到 destDir:
|
|
91
|
+
* - 新增/修改:按 diff copy 写入
|
|
92
|
+
* - 删除:destDir 中不在 srcDir 里的文件会被删除(以及空父目录)
|
|
93
|
+
*
|
|
94
|
+
* 注意:destDir 只由本函数管理时才可安全调用(例如 web/src/generated)。
|
|
95
|
+
*/
|
|
96
|
+
function syncDir(srcDir, destDir) {
|
|
97
|
+
const srcFiles = new Set(listFilesRecursive(srcDir));
|
|
98
|
+
const destFiles = listFilesRecursive(destDir);
|
|
99
|
+
let written = 0;
|
|
100
|
+
for (const rel of srcFiles) {
|
|
101
|
+
if (copyFileIfChanged(path.join(srcDir, rel), path.join(destDir, rel))) {
|
|
102
|
+
written++;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
let deleted = 0;
|
|
106
|
+
for (const rel of destFiles) {
|
|
107
|
+
if (!srcFiles.has(rel)) {
|
|
108
|
+
removeFileAndPruneEmptyDirs(path.join(destDir, rel), destDir);
|
|
109
|
+
deleted++;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return { written, deleted };
|
|
113
|
+
}
|
|
114
|
+
exports.syncDir = syncDir;
|
|
115
|
+
/**
|
|
116
|
+
* 删除 destDir 下所有不在 keepFiles 里的文件(以及随之空掉的父目录)。
|
|
117
|
+
* 用于「本轮 model 目录被删」的孤儿清理场景。
|
|
118
|
+
* @returns 被删除的文件相对路径列表(相对 destDir)
|
|
119
|
+
*/
|
|
120
|
+
function pruneOrphans(destDir, keepFiles) {
|
|
121
|
+
const removed = [];
|
|
122
|
+
for (const rel of listFilesRecursive(destDir)) {
|
|
123
|
+
if (!keepFiles.has(rel)) {
|
|
124
|
+
removeFileAndPruneEmptyDirs(path.join(destDir, rel), destDir);
|
|
125
|
+
removed.push(rel);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return removed;
|
|
129
|
+
}
|
|
130
|
+
exports.pruneOrphans = pruneOrphans;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gadmin2n/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.159",
|
|
4
4
|
"description": "Gadmin - modern, fast, powerful node.js web framework (@cli)",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"@angular-devkit/core": "13.3.2",
|
|
48
48
|
"@angular-devkit/schematics": "13.3.2",
|
|
49
49
|
"@angular-devkit/schematics-cli": "13.3.2",
|
|
50
|
-
"@gadmin2n/schematics": "^0.0.
|
|
50
|
+
"@gadmin2n/schematics": "^0.0.128",
|
|
51
51
|
"abc": "^0.6.1",
|
|
52
52
|
"chalk": "3.0.0",
|
|
53
53
|
"chokidar": "3.5.3",
|