@gadmin2n/schematics 0.0.130 → 0.0.132

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.
@@ -16,10 +16,10 @@
16
16
  "paths": {
17
17
  "@/*": ["src/*"]
18
18
  },
19
- "target": "es2017"
19
+ "target": "es2020"
20
20
  },
21
21
  "module": {
22
22
  "type": "commonjs"
23
23
  },
24
- "exclude": ["\\.spec\\.ts$", "\\.test\\.ts$"]
24
+ "exclude": ["node_modules", ".*\\.spec\\.ts$", ".*\\.test\\.ts$"]
25
25
  }
@@ -0,0 +1,28 @@
1
+ // pm2 dev 场景专用配置:直接跑 nest CLI(SWC watch),不套 npm/dotenv-cli 壳。
2
+ // 由 branch.service.ts 通过 pm2 CLI 传入 PM2_NAME / NEST_PORT / DATABASE_URL 等环境变量。
3
+ const path = require('path');
4
+ const dotenv = require('dotenv');
5
+
6
+ const baseEnv = dotenv.config({ path: path.join(__dirname, '.env') }).parsed || {};
7
+ const localEnv = dotenv.config({ path: path.join(__dirname, '.env.local') }).parsed || {};
8
+
9
+ module.exports = {
10
+ apps: [
11
+ {
12
+ name: process.env.PM2_NAME,
13
+ script: './node_modules/@nestjs/cli/bin/nest.js',
14
+ args: 'start -c gadmin-cli.json --watch --builder swc',
15
+ interpreter: 'node',
16
+ cwd: __dirname,
17
+ autorestart: false,
18
+ env: {
19
+ ...baseEnv,
20
+ ...localEnv,
21
+ // 由 branch.service.ts 注入的分支专属变量优先(覆盖 .env* 里的默认值)
22
+ ...(process.env.NEST_PORT ? { NEST_PORT: process.env.NEST_PORT } : {}),
23
+ ...(process.env.DATABASE_URL ? { DATABASE_URL: process.env.DATABASE_URL } : {}),
24
+ PATH: `/data/home/kavenma/.yarn/bin:${process.env.PATH}`,
25
+ },
26
+ },
27
+ ],
28
+ };
@@ -3,7 +3,6 @@
3
3
  "collection": "@gadmin2n/schematics",
4
4
  "sourceRoot": "src",
5
5
  "compilerOptions": {
6
- "builder": "swc",
7
6
  "plugins": [
8
7
  {
9
8
  "name": "@nestjs/swagger",
@@ -0,0 +1,12 @@
1
+ {
2
+ "watch": ["src", "gadmin-cli.json", "tsconfig.json", "tsconfig.build.json"],
3
+ "ext": "ts,json",
4
+ "ignore": [
5
+ "dist",
6
+ "node_modules",
7
+ "**/*.spec.ts",
8
+ "**/*.test.ts"
9
+ ],
10
+ "delay": 300,
11
+ "exec": "nest build -c gadmin-cli.json"
12
+ }
@@ -12,6 +12,7 @@
12
12
  "prepare": "cd .. && husky install || true",
13
13
  "start": "dotenv -e .env.local -e .env -- nest start -c gadmin-cli.json",
14
14
  "start:dev": "dotenv -e .env.local -e .env -- nest start -c gadmin-cli.json --watch",
15
+ "start:dev:lite": "dotenv -e .env.local -e .env -- sh -c 'nest build -cgadmin-cli.json && concurrently -k -n build,run \"nodemon --config nodemon.lite.json\" \"node --enable-source-maps --watch dist/src/main\"'",
15
16
  "start:debug": "dotenv -e .env.local -e .env -- nest start -c gadmin-cli.json --debug --watch",
16
17
  "start:prod": "/bin/sh start-prod.sh",
17
18
  "typecheck": "tsc --noEmit",
@@ -28,8 +29,6 @@
28
29
  "generate": "dotenv -e .env.local -e .env -- npx prisma generate",
29
30
  "generate:dev": "npm run migrate:dev && dotenv -e .env.local -e .env -- npx prisma generate",
30
31
  "postgenerate:dev": "dotenv -e .env.local -e .env -- ts-node seed/data-mngt.seed.ts",
31
- "generate:web": "dotenv -e .env.local -e .env -- npx prisma generate --generator react",
32
- "generate:server": "dotenv -e .env.local -e .env -- npx prisma generate --generator client --generator nestjs",
33
32
  "postgadminGenerate": "node scripts/notify-dev-servers.js",
34
33
  "push:db": "dotenv -e .env.local -e .env -- prisma db push --skip-generate",
35
34
  "seed": "dotenv -e .env.local -e .env -- ts-node seed/index.ts",
@@ -105,12 +104,14 @@
105
104
  "@types/supertest": "^2.0.11",
106
105
  "@typescript-eslint/eslint-plugin": "^5.0.0",
107
106
  "@typescript-eslint/parser": "^5.0.0",
107
+ "concurrently": "^9.1.0",
108
108
  "eslint": "^8.0.1",
109
109
  "eslint-config-prettier": "^8.3.0",
110
110
  "eslint-plugin-prettier": "^4.0.0",
111
111
  "husky": "^8.0.0",
112
112
  "jest": "^29.0.0",
113
113
  "lint-staged": "^13.0.0",
114
+ "nodemon": "^3.1.7",
114
115
  "prettier": "^2.3.2",
115
116
  "prettier-plugin-organize-imports": "^3.0.3",
116
117
  "prisma-dbml-generator": "^0.9.1",
@@ -29,6 +29,3 @@ generator react {
29
29
  pagesDir = "../../web/src/routes"
30
30
  }
31
31
 
32
- generator dbml {
33
- provider = "prisma-dbml-generator"
34
- }
@@ -6,13 +6,29 @@
6
6
  * 1. 释放 Vite dev server 内存(server.restart() 会清空 module graph)
7
7
  * 2. 让浏览器自动 full-reload,无需手工刷新
8
8
  *
9
- * 时序:
10
- * nest --watch 会因为 server/src/modules/{model} 变化自动重启,
11
- * 此脚本先 poll 它的 /api/health/live 直到 ready,再 touch web/.vite-restart,
12
- * 保证浏览器恢复时后端 API 已就绪,避免 502/ECONNREFUSED。
9
+ * 时序策略(历史与当前):
10
+ * nest --watch 会因为 server/src/{generated,modules} 变化自动重启,此脚本
11
+ * 先 poll 它的 /api/health/live 直到"稳定就绪",再 touch web/.vite-restart,
12
+ * 保证浏览器 iframe 复活时后端 API 已稳定,避免 500/ECONNREFUSED。
13
13
  *
14
- * 环境变量(由 CLI 注入):
15
- * GADMIN_MODE = full | web | server
14
+ * 旧实现只等"第一次 down → 第一次 up"就返回,遇到 gadmin g prisma 触发的
15
+ * 多批文件写入(generated/** 与 modules/** 两个子树)+ tsc-watch 的短
16
+ * debounce,会连续触发 2+ 次 rebuild。第一次 up 后随即又被 kill,vite
17
+ * 触发 iframe 复活时 nest 已再次不可达,vite proxy fallback 返回 500 空 body
18
+ * (形状:Vary: Origin / Content-Type: text/plain / Transfer-Encoding: chunked
19
+ * / 无 body),页面白屏;nest 侧无堆栈日志,因为请求没走到应用层。
20
+ *
21
+ * 现改用 waitForNestReady 的自适应稳定窗口:观察到 up 后要求持续
22
+ * stableRequiredMs 内保持 up;一旦观察到新一轮 up→down 就按实际 rebuild
23
+ * 周期动态扩展窗口;有稳定窗口上限 + 硬超时兜底避免异常时卡死。
24
+ *
25
+ * 环境变量:
26
+ * NEST_PORT 后端端口,默认 8000
27
+ * POSTGEN_POLL_MS 健康 poll 间隔,默认 250ms
28
+ * POSTGEN_INITIAL_STABLE_MS 初始稳定窗口,默认 3000ms
29
+ * POSTGEN_MAX_STABLE_MS 稳定窗口上限,默认 15000ms
30
+ * POSTGEN_OBSERVE_DOWN_TIMEOUT_MS 等待第一次 down 的超时,默认 15000ms
31
+ * POSTGEN_TOTAL_TIMEOUT_MS Phase 2 硬超时,默认 60000ms
16
32
  *
17
33
  * 与 CLI 通信约定:
18
34
  * - CWD 是 server/(CLI shell.cd('server/') 后调用)
@@ -21,10 +37,7 @@
21
37
  const fs = require('fs');
22
38
  const http = require('http');
23
39
  const path = require('path');
24
-
25
- const MODE = process.env.GADMIN_MODE || 'full';
26
- const shouldWaitNest = MODE === 'full' || MODE === 'server';
27
- const shouldTouchWeb = MODE === 'full' || MODE === 'web';
40
+ const { waitForNestReady } = require('./wait-for-nest-ready');
28
41
 
29
42
  const NEST_PORT = Number(process.env.NEST_PORT) || 8000;
30
43
  const HEALTH_URL = `http://127.0.0.1:${NEST_PORT}/api/health/live`;
@@ -50,40 +63,11 @@ function pingOnce(url) {
50
63
 
51
64
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
52
65
 
53
- async function waitForNestReady() {
54
- const initiallyAlive = await pingOnce(HEALTH_URL);
55
- if (!initiallyAlive) {
56
- log(`nest not running at ${HEALTH_URL}, skip waiting.`);
57
- return;
58
- }
59
- log(`waiting for nest restart at ${HEALTH_URL} ...`);
60
-
61
- // Phase 1: 等 nest --watch 检测到文件变化并开始重编译(服务短暂 down)
62
- const started = Date.now();
63
- const observeDownTimeoutMs = 15_000;
64
- let observedDown = false;
65
- while (Date.now() - started < observeDownTimeoutMs) {
66
- if (!(await pingOnce(HEALTH_URL))) {
67
- observedDown = true;
68
- break;
69
- }
70
- await sleep(300);
71
- }
72
- if (!observedDown) {
73
- log('nest did not restart within 15s (maybe not in --watch mode); continue.');
74
- return;
75
- }
76
-
77
- // Phase 2: 等 nest 重新起来
78
- const totalTimeoutMs = 60_000;
79
- while (Date.now() - started < totalTimeoutMs) {
80
- if (await pingOnce(HEALTH_URL)) {
81
- log('nest ready.');
82
- return;
83
- }
84
- await sleep(500);
85
- }
86
- log(`nest not ready within ${totalTimeoutMs}ms, continue anyway.`);
66
+ function envInt(name) {
67
+ const v = process.env[name];
68
+ if (v === undefined || v === '') return undefined;
69
+ const n = Number(v);
70
+ return Number.isFinite(n) && n > 0 ? n : undefined;
87
71
  }
88
72
 
89
73
  function touchViteSentinel() {
@@ -97,8 +81,20 @@ function touchViteSentinel() {
97
81
  }
98
82
 
99
83
  (async () => {
100
- if (shouldWaitNest) await waitForNestReady();
101
- if (shouldTouchWeb) touchViteSentinel();
84
+ await waitForNestReady({
85
+ ping: () => pingOnce(HEALTH_URL),
86
+ sleep,
87
+ now: () => Date.now(),
88
+ log,
89
+ opts: {
90
+ pollMs: envInt('POSTGEN_POLL_MS'),
91
+ initialStableMs: envInt('POSTGEN_INITIAL_STABLE_MS'),
92
+ maxStableMs: envInt('POSTGEN_MAX_STABLE_MS'),
93
+ observeDownTimeoutMs: envInt('POSTGEN_OBSERVE_DOWN_TIMEOUT_MS'),
94
+ totalTimeoutMs: envInt('POSTGEN_TOTAL_TIMEOUT_MS'),
95
+ },
96
+ });
97
+ touchViteSentinel();
102
98
  })().catch((e) => {
103
99
  log(`unexpected error: ${e && e.stack ? e.stack : e}`);
104
100
  // 非致命,不 throw 避免影响 CLI 退出码
@@ -0,0 +1,148 @@
1
+ /* eslint-disable */
2
+ /**
3
+ * waitForNestReady —— 判断 nest --watch 是否"稳定就绪"的纯函数。
4
+ *
5
+ * 背景:postgadminGenerate hook 需要在 nest 重启完成后再 touch web sentinel,
6
+ * 触发 vite reload。旧实现只等"第一次 down → 第一次 up",遇到 gadmin g prisma
7
+ * 引发 tsc-watch 连续多次 rebuild 时会误判就绪:vite 触发浏览器 iframe 复活时
8
+ * nest 已被第 N+1 次 rebuild kill,vite proxy 拿到 ECONNRESET 返回 500 空 body,
9
+ * 页面白屏。
10
+ *
11
+ * 此模块用"自适应稳定窗口"取代"一次 down/up 就返回":
12
+ * - 观察到进入 up 后不立即返回,要求连续 stableRequiredMs 内保持 up。
13
+ * - 一旦观察到新的 up→down 转换(说明又 rebuild 了),根据实际 rebuild 周期
14
+ * 动态扩展 stableRequiredMs,保证下一个静默期至少覆盖下一轮 rebuild。
15
+ * - stableRequiredMs 有上限(maxStableMs),总等待时间有硬超时(totalTimeoutMs),
16
+ * 避免异常场景下 hook 无限阻塞。
17
+ *
18
+ * 依赖注入 ping/sleep/now/log 是为了单测可控。生产使用见 notify-dev-servers.js。
19
+ */
20
+
21
+ 'use strict';
22
+
23
+ const DEFAULTS = {
24
+ pollMs: 250,
25
+ initialStableMs: 3000,
26
+ maxStableMs: 15000,
27
+ observeDownTimeoutMs: 15000,
28
+ totalTimeoutMs: 60000,
29
+ };
30
+
31
+ /**
32
+ * @param {object} deps
33
+ * @param {() => Promise<boolean>} deps.ping 探活函数,true = up, false = down。
34
+ * @param {(ms: number) => Promise<void>} deps.sleep 睡眠函数(生产用 setTimeout,测试可 mock 时钟)。
35
+ * @param {() => number} deps.now 毫秒时间戳(生产用 Date.now,测试可 mock)。
36
+ * @param {(msg: string) => void} deps.log 日志函数。
37
+ * @param {Partial<typeof DEFAULTS>} [deps.opts]
38
+ * @returns {Promise<{skipped:boolean, reason?:string, timeout?:boolean, restarts?:number, stableRequiredMs?:number, waited?:number}>}
39
+ */
40
+ async function waitForNestReady({ ping, sleep, now, log, opts = {} }) {
41
+ const {
42
+ pollMs,
43
+ initialStableMs,
44
+ maxStableMs,
45
+ observeDownTimeoutMs,
46
+ totalTimeoutMs,
47
+ } = { ...DEFAULTS, ...opts };
48
+
49
+ // Phase 0:nest 一开始就没跑(本地未启动 / 生产环境),直接放行。
50
+ const initiallyAlive = await ping();
51
+ if (!initiallyAlive) {
52
+ log('nest not running, skip waiting.');
53
+ return { skipped: true, reason: 'not-running' };
54
+ }
55
+
56
+ const started = now();
57
+ log(
58
+ `waiting for nest restart (poll=${pollMs}ms, stable=${initialStableMs}ms, ` +
59
+ `maxStable=${maxStableMs}ms, totalTimeout=${totalTimeoutMs}ms) ...`,
60
+ );
61
+
62
+ // Phase 1:等第一次 down。若长时间不 down,视作"未在 --watch 模式"或"文件全 unchanged",跳过。
63
+ let observedDown = false;
64
+ while (now() - started < observeDownTimeoutMs) {
65
+ if (!(await ping())) {
66
+ observedDown = true;
67
+ break;
68
+ }
69
+ await sleep(pollMs);
70
+ }
71
+ if (!observedDown) {
72
+ log(
73
+ `nest did not restart within ${observeDownTimeoutMs}ms (maybe not in --watch mode); continue.`,
74
+ );
75
+ return { skipped: true, reason: 'no-restart' };
76
+ }
77
+
78
+ // Phase 2:状态机 + 自适应稳定窗口。
79
+ let state = 'down';
80
+ let lastDownAt = now();
81
+ let lastUpAt = null;
82
+ let stableRequiredMs = initialStableMs;
83
+ let restarts = 1;
84
+
85
+ while (true) {
86
+ if (now() - started >= totalTimeoutMs) {
87
+ log(
88
+ `nest not stable within ${totalTimeoutMs}ms ` +
89
+ `(restarts=${restarts}, stableRequired=${stableRequiredMs}ms); continue anyway.`,
90
+ );
91
+ return {
92
+ skipped: false,
93
+ timeout: true,
94
+ restarts,
95
+ stableRequiredMs,
96
+ waited: now() - started,
97
+ };
98
+ }
99
+
100
+ const alive = await ping();
101
+ const currentState = alive ? 'up' : 'down';
102
+
103
+ if (state === 'up' && currentState === 'down') {
104
+ // 新一轮 rebuild。用观察到的完整周期扩展下一次的静默要求。
105
+ const cycleLength = now() - lastDownAt;
106
+ const proposed = Math.ceil(cycleLength * 1.2);
107
+ const newStable = Math.min(
108
+ maxStableMs,
109
+ Math.max(stableRequiredMs, proposed),
110
+ );
111
+ if (newStable !== stableRequiredMs) {
112
+ log(
113
+ `observed rebuild cycle ${cycleLength}ms; ` +
114
+ `expanding stable window ${stableRequiredMs}→${newStable}ms.`,
115
+ );
116
+ stableRequiredMs = newStable;
117
+ }
118
+ lastDownAt = now();
119
+ restarts += 1;
120
+ } else if (state === 'down' && currentState === 'up') {
121
+ lastUpAt = now();
122
+ }
123
+
124
+ state = currentState;
125
+
126
+ if (
127
+ state === 'up' &&
128
+ lastUpAt !== null &&
129
+ now() - lastUpAt >= stableRequiredMs
130
+ ) {
131
+ const waited = now() - started;
132
+ log(
133
+ `nest ready (restarts=${restarts}, stable=${stableRequiredMs}ms, waited=${waited}ms).`,
134
+ );
135
+ return {
136
+ skipped: false,
137
+ timeout: false,
138
+ restarts,
139
+ stableRequiredMs,
140
+ waited,
141
+ };
142
+ }
143
+
144
+ await sleep(pollMs);
145
+ }
146
+ }
147
+
148
+ module.exports = { waitForNestReady, DEFAULTS };
@@ -0,0 +1,196 @@
1
+ /* eslint-disable */
2
+ /**
3
+ * waitForNestReady 单测。
4
+ * 运行方式:
5
+ * cd server && node --test scripts/wait-for-nest-ready.test.js
6
+ *
7
+ * 用 mock 时钟 + mock ping 序列离线覆盖以下场景:
8
+ * 1. 一次重启(当前 gadmin g prisma 观察到的常态)
9
+ * 2. 两次重启(调研中记录的双 rebuild 现象)
10
+ * 3. Phase 1 超时跳过(未在 --watch 模式或全 unchanged)
11
+ * 4. 稳定窗口按 rebuild 周期扩展(大 schema 情况)
12
+ * 5. 无限重启触发 totalTimeoutMs 硬超时(异常场景不卡死)
13
+ * 6. 初始 nest 未跑,直接跳过
14
+ */
15
+ 'use strict';
16
+
17
+ const { test } = require('node:test');
18
+ const assert = require('node:assert/strict');
19
+ const { waitForNestReady } = require('./wait-for-nest-ready');
20
+
21
+ /**
22
+ * 构造 mock 环境:ping 按脚本序列返回,时钟按 sleep 请求推进。
23
+ * 每次 ping 或 sleep 都会推进"一步",让状态机可控。
24
+ *
25
+ * @param {Array<'up'|'down'>} pingScript 每次 ping 依次返回的状态。用尽后重复最后一项。
26
+ */
27
+ function makeEnv(pingScript) {
28
+ let clock = 0;
29
+ let idx = 0;
30
+ const logs = [];
31
+ return {
32
+ ping: async () => {
33
+ const state = idx < pingScript.length ? pingScript[idx] : pingScript[pingScript.length - 1];
34
+ idx += 1;
35
+ return state === 'up';
36
+ },
37
+ sleep: async (ms) => {
38
+ clock += ms;
39
+ },
40
+ now: () => clock,
41
+ log: (msg) => logs.push(msg),
42
+ logs,
43
+ tick: (ms) => {
44
+ clock += ms;
45
+ },
46
+ };
47
+ }
48
+
49
+ test('nest 一开始就没跑:直接跳过', async () => {
50
+ const env = makeEnv(['down']);
51
+ const res = await waitForNestReady({
52
+ ping: env.ping,
53
+ sleep: env.sleep,
54
+ now: env.now,
55
+ log: env.log,
56
+ });
57
+ assert.deepEqual(res, { skipped: true, reason: 'not-running' });
58
+ });
59
+
60
+ test('Phase 1 超时(一直 up):跳过', async () => {
61
+ // 100 次 up,覆盖 15s / 250ms = 60 次以上的 poll。
62
+ const env = makeEnv(new Array(100).fill('up'));
63
+ const res = await waitForNestReady({
64
+ ping: env.ping,
65
+ sleep: env.sleep,
66
+ now: env.now,
67
+ log: env.log,
68
+ opts: { observeDownTimeoutMs: 1000, pollMs: 250 },
69
+ });
70
+ assert.equal(res.skipped, true);
71
+ assert.equal(res.reason, 'no-restart');
72
+ });
73
+
74
+ test('一次重启:down 后 up,稳定 3s 后返回', async () => {
75
+ // 序列: initial up, down (Phase 1 观察), 之后一直 up。
76
+ const env = makeEnv(['up', 'down', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up']);
77
+ const res = await waitForNestReady({
78
+ ping: env.ping,
79
+ sleep: env.sleep,
80
+ now: env.now,
81
+ log: env.log,
82
+ opts: { pollMs: 250, initialStableMs: 3000, totalTimeoutMs: 30000 },
83
+ });
84
+ assert.equal(res.skipped, false);
85
+ assert.equal(res.timeout, false);
86
+ assert.equal(res.restarts, 1);
87
+ assert.equal(res.stableRequiredMs, 3000);
88
+ // 稳定窗口至少满足初始值。
89
+ assert.ok(res.waited >= 3000, `waited ${res.waited} < 3000`);
90
+ });
91
+
92
+ test('两次重启(gadmin g prisma 常态):等到第二次稳定后才返回', async () => {
93
+ // 序列: initial up → down (rebuild #1) → up (nest #B) → down (rebuild #2) → up (nest #C)*N。
94
+ // 用 pollMs=250, initialStableMs=3000。
95
+ // Phase 1 拿到 down 后进入 Phase 2 state='down'。
96
+ // 第 1 次 poll 拿到 up:state up, lastUpAt=t。
97
+ // 之后马上 down:会重启 restart 计数并扩展窗口。
98
+ // 后续持续 up 直到 stable 满足。
99
+ const env = makeEnv([
100
+ 'up', // Phase 0 initial
101
+ 'down', // Phase 1 observe down
102
+ 'up', // Phase 2 first poll: state=up
103
+ 'down', // second poll: state=down (rebuild #2 检测到)
104
+ 'up', // recovery
105
+ ...new Array(50).fill('up'),
106
+ ]);
107
+ const res = await waitForNestReady({
108
+ ping: env.ping,
109
+ sleep: env.sleep,
110
+ now: env.now,
111
+ log: env.log,
112
+ opts: { pollMs: 250, initialStableMs: 3000, totalTimeoutMs: 30000 },
113
+ });
114
+ assert.equal(res.skipped, false);
115
+ assert.equal(res.timeout, false);
116
+ // 至少 2 次 restart(Phase 1 观察 + Phase 2 又 down 一次)。
117
+ assert.ok(res.restarts >= 2, `restarts=${res.restarts} < 2`);
118
+ });
119
+
120
+ test('大 rebuild 周期:稳定窗口按周期扩展', async () => {
121
+ // 让 up 持续到刚好 = initialStableMs 时再 down(触发 stable 检查前一步)。
122
+ // pollMs=250, initialStableMs=1000: up 持续 4 次 poll 后 down,
123
+ // 此时 cycleLength=1000ms,proposed=ceil(1200)=1200 → 触发扩展。
124
+ const script = [
125
+ 'up', // Phase 0
126
+ 'down', // Phase 1 observe down
127
+ 'up', // Phase 2 iter1: t=0 → 250
128
+ 'up', // iter2: 250 → 500
129
+ 'up', // iter3: 500 → 750
130
+ 'up', // iter4: 750 → 1000 (Check 750 < 1000, 不 return)
131
+ 'down', // iter5: 1000, cycleLength=1000, 扩展到 1200
132
+ ...new Array(30).fill('up'), // 恢复 + 稳定 1200ms
133
+ ];
134
+ const env = makeEnv(script);
135
+ const res = await waitForNestReady({
136
+ ping: env.ping,
137
+ sleep: env.sleep,
138
+ now: env.now,
139
+ log: env.log,
140
+ opts: { pollMs: 250, initialStableMs: 1000, maxStableMs: 15000, totalTimeoutMs: 60000 },
141
+ });
142
+ assert.equal(res.skipped, false);
143
+ assert.equal(res.timeout, false);
144
+ assert.equal(res.stableRequiredMs, 1200);
145
+ });
146
+
147
+ test('稳定窗口不超过 maxStableMs 上限', async () => {
148
+ // pollMs=250, initialStableMs=500, maxStableMs=800:
149
+ // up 持续 8 次 poll (2000ms) 后 down,cycleLength=2000ms, proposed=2400ms,
150
+ // 应被 maxStableMs=800 封顶为 800。
151
+ const script = [
152
+ 'up', // Phase 0
153
+ 'down', // Phase 1
154
+ ...new Array(8).fill('up'), // 稳定 500ms 检查会在 iter3 触发(250+250+250=750>=500),需要 initialStableMs 更大
155
+ ];
156
+ // 重新调整:让 initialStableMs 大过第一段 up,才能保证不提前 return。
157
+ const env = makeEnv([
158
+ 'up',
159
+ 'down',
160
+ ...new Array(8).fill('up'), // 8 * 250 = 2000ms
161
+ 'down', // cycleLength ≈ 2000ms
162
+ ...new Array(30).fill('up'),
163
+ ]);
164
+ const res = await waitForNestReady({
165
+ ping: env.ping,
166
+ sleep: env.sleep,
167
+ now: env.now,
168
+ log: env.log,
169
+ opts: {
170
+ pollMs: 250,
171
+ initialStableMs: 3000, // 大过第一段 up 的 2000ms,避免提前 return
172
+ maxStableMs: 1500, // 故意小于 proposed=2400
173
+ totalTimeoutMs: 60000,
174
+ },
175
+ });
176
+ assert.equal(res.skipped, false);
177
+ assert.equal(res.stableRequiredMs, 1500);
178
+ });
179
+
180
+ test('无限重启:命中 totalTimeoutMs 硬超时', async () => {
181
+ // 交替 up/down 无止境,验证 totalTimeoutMs 触发返回而不是死循环。
182
+ const script = ['up', 'down'];
183
+ // 让 makeEnv 初始 ping 返回 up;Phase 1 拿 down;Phase 2 交替。
184
+ const alternating = ['up', 'down'];
185
+ for (let i = 0; i < 5000; i++) alternating.push(i % 2 === 0 ? 'up' : 'down');
186
+ const env = makeEnv(alternating);
187
+ const res = await waitForNestReady({
188
+ ping: env.ping,
189
+ sleep: env.sleep,
190
+ now: env.now,
191
+ log: env.log,
192
+ opts: { pollMs: 250, initialStableMs: 3000, totalTimeoutMs: 5000 },
193
+ });
194
+ assert.equal(res.timeout, true);
195
+ assert.ok(res.waited >= 5000, `waited=${res.waited} < 5000`);
196
+ });
@@ -18,13 +18,13 @@ import {
18
18
  SwaggerDocumentOptions,
19
19
  SwaggerModule,
20
20
  } from '@nestjs/swagger';
21
- import cookieParser from 'cookie-parser';
22
21
  import 'dotenv/config';
23
22
  import { PrismaClientExceptionFilter, PrismaService } from 'nestjs-prisma';
24
23
  import { AppModule } from './app.module';
25
24
  import AuthGuard from './lib/auth.guard';
26
25
  import RolesGuard from './lib/roles.guard';
27
26
  import { UserService } from './modules/user/user.service';
27
+ import cookieParser = require('cookie-parser');
28
28
 
29
29
  // https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields#working-with-bigint
30
30
  const stringify = JSON.stringify;
@@ -0,0 +1,20 @@
1
+ // pm2 dev 场景专用配置:直接跑 vite CLI,不套 npm 壳。
2
+ // 由 branch.service.ts 通过 pm2 CLI 传入 PM2_NAME / VITE_PORT / VITE_BRANCH_OWNER 等环境变量。
3
+ // vite 自己会读 .env / .env.local,无需 dotenv-cli 层。
4
+ module.exports = {
5
+ apps: [
6
+ {
7
+ name: process.env.PM2_NAME,
8
+ script: './node_modules/vite/bin/vite.js',
9
+ args: process.env.VITE_PORT ? `--port ${process.env.VITE_PORT}` : '',
10
+ interpreter: 'node',
11
+ cwd: __dirname,
12
+ autorestart: false,
13
+ // NODE_OPTIONS 保留原 package.json 里的 4GB 上限设置。
14
+ node_args: '--max-old-space-size=4096',
15
+ env: {
16
+ ...(process.env.VITE_BRANCH_OWNER ? { VITE_BRANCH_OWNER: process.env.VITE_BRANCH_OWNER } : {}),
17
+ },
18
+ },
19
+ ],
20
+ };
@@ -5,8 +5,15 @@ import tsconfigPaths from 'vite-tsconfig-paths';
5
5
  import { devShellPlugin } from './src/plugins/devShellPlugin';
6
6
 
7
7
  // gadmin g prisma 生成完毕后触发:CLI 会 touch 本文件,
8
- // 插件监听变更后调用 server.restart() —— 清空 module graph 释放内存,
9
- // 并通过 HMR ws 广播 full-reload,浏览器自动刷新。
8
+ // 插件监听变更后 invalidate module graph 并广播 full-reload。
9
+ //
10
+ // 历史:曾用 server.restart(),每次会重新 esbuild bundle 一次 vite.config.ts
11
+ // 及其依赖,旧 config module 被 v8 保留,实测每次泄漏 15~20MB rss,
12
+ // 几十次调用后进程内存从 60MB 涨到 600MB+。
13
+ //
14
+ // 改用 moduleGraph.invalidateAll() + hot full-reload:
15
+ // - invalidateAll() 只清 transform 结果,不重载 config、不重跑 esbuild
16
+ // - full-reload 让浏览器重新请求,vite 按最新文件重新 transform
10
17
  function gadminRestartPlugin(): Plugin {
11
18
  const sentinel = path.resolve(__dirname, '.vite-restart');
12
19
  return {
@@ -16,10 +23,11 @@ function gadminRestartPlugin(): Plugin {
16
23
  server.watcher.on('change', (file) => {
17
24
  if (path.resolve(file) === sentinel) {
18
25
  server.config.logger.info(
19
- '[gadmin] generated code changed, restarting vite...',
26
+ '[gadmin] generated code changed, invalidating & full-reload...',
20
27
  { timestamp: true },
21
28
  );
22
- server.restart();
29
+ server.moduleGraph.invalidateAll();
30
+ server.hot.send({ type: 'full-reload', path: '*' });
23
31
  }
24
32
  });
25
33
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gadmin2n/schematics",
3
- "version": "0.0.130",
3
+ "version": "0.0.132",
4
4
  "description": "Gadmin - modern, fast, powerful node.js web framework (@schematics)",
5
5
  "main": "dist/index.js",
6
6
  "files": [