@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.
@@ -3,5 +3,6 @@ import { AbstractAction } from './abstract.action';
3
3
  export declare class PrismaAction extends AbstractAction {
4
4
  private buildMergedSchema;
5
5
  handle(inputs: Input[], options: Input[]): Promise<void>;
6
+ private printReport;
6
7
  private runProjectPostHook;
7
8
  }
@@ -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, _c;
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 webOnly = !!((_b = options.find((option) => option.name === 'webOnly')) === null || _b === void 0 ? void 0 : _b.value);
97
- const serverOnly = !!((_c = options.find((option) => option.name === 'serverOnly')) === null || _c === void 0 ? void 0 : _c.value);
98
- // 构建合并的 schema(三种模式都需要重建 schema.prisma)
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
- // 清空 generated 目录(按模式选择)
101
- if (webOnly) {
102
- // 只清 server/src/generated/.react(清理旧 react 输出)
103
- fs.rmSync('server/src/generated/.react', {
104
- recursive: true,
105
- force: true,
106
- });
107
- }
108
- else if (serverOnly) {
109
- // 只清 server/src/generated/
110
- fs.rmSync('server/src/generated', { recursive: true, force: true });
111
- }
112
- else {
113
- // 全清两侧
114
- fs.rmSync('server/src/generated', { recursive: true, force: true });
115
- fs.rmSync('web/src/generated', { recursive: true, force: true });
116
- }
117
- // 执行代码生成(走 npm run 脚本)
118
- let generateScript = 'npm run generate';
119
- if (devMode)
120
- generateScript = 'npm run generate:dev';
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
- // 分发前端文件(!serverOnly)
131
- if (!serverOnly) {
132
- fs.cpSync('server/src/generated/.react', 'web/src/generated', {
133
- recursive: true,
134
- force: true,
135
- });
136
- fs.rmSync('server/src/generated/.react', {
137
- recursive: true,
138
- force: true,
139
- });
140
- // pages 目录可能因所有模型都已存在而未生成,需判断后再复制
141
- if (fs.existsSync('web/src/generated/routes')) {
142
- fs.cpSync('web/src/generated/routes', 'web/src/routes', {
143
- recursive: true,
144
- force: false,
145
- });
146
- fs.rmSync('web/src/generated/routes', { recursive: true, force: true });
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
- // 分发后端文件(!webOnly)
150
- if (!webOnly) {
151
- shell.cd('server/src/generated/');
152
- const models = shell
153
- .ls('-d', '*')
154
- .filter((file) => shell.test('-d', file))
155
- .filter((dir) => shell.test('-e', `${dir}/dto/${dir}.form.validator.ts`));
156
- shell.cd('../../..');
157
- models.forEach((modelName) => {
158
- fs.mkdirSync(`web/src/generated/props/${modelName}`, {
159
- recursive: true,
160
- });
161
- shell.mv('-f', `server/src/generated/${modelName}/dto/${modelName}.form.validator.ts`, `web/src/generated/props/${modelName}/form.validator.ts`);
162
- fs.cpSync(`server/src/generated/${modelName}`, `server/src/modules/${modelName}`, { recursive: true, force: false });
163
- shell.rm('-f', `server/src/generated/${modelName}/*.ts`);
164
- // 只删除生成器产生的 dto/entities 文件,保留用户自定义的文件(如 canvas.dto.ts)
165
- for (const subDir of ['dto', 'entities']) {
166
- const generatedSubDir = `server/src/generated/${modelName}/${subDir}`;
167
- const modulesSubDir = `server/src/modules/${modelName}/${subDir}`;
168
- if (fs.existsSync(modulesSubDir) && fs.existsSync(generatedSubDir)) {
169
- for (const file of fs.readdirSync(generatedSubDir)) {
170
- fs.rmSync((0, path_1.join)(modulesSubDir, file), { force: true });
171
- }
172
- // 目录已空则一并删除
173
- if (fs.readdirSync(modulesSubDir).length === 0) {
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
- // 更新 Prisma 类型文件(!webOnly)
181
- if (!webOnly) {
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
- // 通过 env 传递本次生成模式,脚本可根据模式决定跳过后端或前端。
192
- this.runProjectPostHook({ webOnly, serverOnly });
255
+ this.runProjectPostHook();
193
256
  process.exit(0);
194
257
  });
195
258
  }
196
- runProjectPostHook({ webOnly, serverOnly, }) {
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
- const mode = webOnly ? 'web' : serverOnly ? 'server' : 'full';
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(Object.assign({}, process.env), { GADMIN_MODE: mode }),
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', 'prisma:web', 'prisma:server'].includes(name)) {
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
- if (name.includes(':web'))
38
- opts.push({ name: 'webOnly', value: true });
39
- if (name.includes(':server'))
40
- opts.push({ name: 'serverOnly', value: true });
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.157",
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.127",
50
+ "@gadmin2n/schematics": "^0.0.128",
51
51
  "abc": "^0.6.1",
52
52
  "chalk": "3.0.0",
53
53
  "chokidar": "3.5.3",