@lark-apaas/fullstack-cli 1.1.61-alpha.20260819082510 → 1.1.61-alpha.20260819103003
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/dist/index.js
CHANGED
|
@@ -2608,31 +2608,6 @@ function defaultProfile(opts) {
|
|
|
2608
2608
|
activateGitHooks: true
|
|
2609
2609
|
};
|
|
2610
2610
|
}
|
|
2611
|
-
function nestFullstackProfile(opts) {
|
|
2612
|
-
const base = defaultProfile(opts);
|
|
2613
|
-
return {
|
|
2614
|
-
...base,
|
|
2615
|
-
sync: [
|
|
2616
|
-
...base.sync,
|
|
2617
|
-
// dev 专用 tsconfig:只打开 noEmitOnError,其余继承 tsconfig.node.json
|
|
2618
|
-
{
|
|
2619
|
-
from: "templates/tsconfig.dev.json",
|
|
2620
|
-
to: "tsconfig.dev.json",
|
|
2621
|
-
type: "file",
|
|
2622
|
-
overwrite: true
|
|
2623
|
-
},
|
|
2624
|
-
// 让 dev:server 用上它。ifStartsWith 精确限定为平台模板生成的形态,
|
|
2625
|
-
// 用户真正改写过的 dev:server 保持原样(改写后仅退化为 message 文案不够精确,
|
|
2626
|
-
// ready/failed 判定不受影响)。
|
|
2627
|
-
{
|
|
2628
|
-
type: "patch-script",
|
|
2629
|
-
name: "dev:server",
|
|
2630
|
-
to: "NODE_ENV=development nest start --watch -p tsconfig.dev.json",
|
|
2631
|
-
ifStartsWith: "NODE_ENV=development nest start --watch"
|
|
2632
|
-
}
|
|
2633
|
-
]
|
|
2634
|
-
};
|
|
2635
|
-
}
|
|
2636
2611
|
function emptyProfile() {
|
|
2637
2612
|
return {
|
|
2638
2613
|
sync: [],
|
|
@@ -2649,7 +2624,7 @@ function viteReactProfile() {
|
|
|
2649
2624
|
}
|
|
2650
2625
|
var SYNC_PROFILES = {
|
|
2651
2626
|
"design-stack": defaultProfile,
|
|
2652
|
-
"nestjs-react-fullstack":
|
|
2627
|
+
"nestjs-react-fullstack": defaultProfile,
|
|
2653
2628
|
"vite-react": viteReactProfile,
|
|
2654
2629
|
html: emptyProfile,
|
|
2655
2630
|
// design-html 与 html 同语义:空 sync + 不激活 git hooks(彻底 no-op)
|
package/package.json
CHANGED
package/templates/scripts/dev.js
CHANGED
|
@@ -252,44 +252,23 @@ process.on('SIGTERM', cleanup);
|
|
|
252
252
|
process.on('SIGINT', cleanup);
|
|
253
253
|
process.on('SIGHUP', cleanup);
|
|
254
254
|
|
|
255
|
-
//
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
// ── Dev session state (consumed by /dev/health) ───────────────────────────────
|
|
265
|
-
// 记录本次 dev 会话首次拉起 server 的时刻。/dev/health 只用它做「超时兜底」:
|
|
266
|
-
// 超过上限仍未探测到存活的 Nest 进程,才允许判定为终态失败。
|
|
267
|
-
// 写失败一律静默——health 是旁路能力,绝不能反向拖垮 dev 主流程。
|
|
268
|
-
const devStatusPath = path.join(LOG_DIR, 'dev-status.json');
|
|
269
|
-
|
|
270
|
-
function writeDevStatus() {
|
|
271
|
-
const tmp = `${devStatusPath}.${process.pid}.tmp`;
|
|
272
|
-
try {
|
|
273
|
-
fs.writeFileSync(
|
|
274
|
-
tmp,
|
|
275
|
-
JSON.stringify({
|
|
276
|
-
schema: 1,
|
|
277
|
-
firstStartedAt: Date.now(),
|
|
278
|
-
supervisorPid: process.pid,
|
|
279
|
-
})
|
|
280
|
-
);
|
|
281
|
-
fs.renameSync(tmp, devStatusPath); // 原子替换,避免读到半个文件
|
|
282
|
-
} catch {
|
|
283
|
-
try { fs.unlinkSync(tmp); } catch {}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
255
|
+
// 定向保留 dist/ 里能复用的部分, 替代之前无脑 rm -rf dist/。
|
|
256
|
+
// 收益: 保留 dist/server 让 nest --watch 从增量编译起点起 (省 8-13s 冷启动),
|
|
257
|
+
// routes JSON 也保留 (build 会覆盖, 无 stale 风险)。dist/client/index.html 按
|
|
258
|
+
// preset 打的 <meta name="miaoda-html-mode"> marker 判定:
|
|
259
|
+
// marker=dev → 保留 dist/client/ 整个 (dev cache hit)
|
|
260
|
+
// marker=prod → agent 跑过 build:client 污染, 定向清 index.html + assets/
|
|
261
|
+
// 具体逻辑抽到 lib/preserve-dev-cache.cjs 便于单测。
|
|
262
|
+
// 降级 escape hatch: FORCE_CLEAN_DIST=true 走原全清路径 (marker 逻辑出 bug 时用)。
|
|
263
|
+
const { preserveDevCache } = require('./lib/preserve-dev-cache.cjs');
|
|
286
264
|
|
|
287
265
|
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
288
266
|
async function main() {
|
|
289
267
|
logEvent('INFO', 'main', '========== Dev session started ==========');
|
|
290
268
|
|
|
291
|
-
|
|
292
|
-
|
|
269
|
+
preserveDevCache(PROJECT_ROOT, {
|
|
270
|
+
log: (level, msg) => logEvent(level, 'main', msg),
|
|
271
|
+
});
|
|
293
272
|
|
|
294
273
|
// Initialize action plugins
|
|
295
274
|
writeOutput('\n🔌 Initializing action plugins...\n');
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
7
|
+
const { preserveDevCache, detectHtmlMode } = require('../preserve-dev-cache.cjs');
|
|
8
|
+
|
|
9
|
+
interface LogEntry {
|
|
10
|
+
level: string;
|
|
11
|
+
msg: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function makeLogger(entries: LogEntry[]) {
|
|
15
|
+
return (level: string, msg: string) => {
|
|
16
|
+
entries.push({ level, msg });
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const DEV_HTML =
|
|
21
|
+
'<html><head><meta name="miaoda-html-mode" content="dev"><title>x</title></head><body></body></html>';
|
|
22
|
+
const PROD_HTML =
|
|
23
|
+
'<html><head><meta name="miaoda-html-mode" content="prod"><title>x</title></head><body></body></html>';
|
|
24
|
+
const UNMARKED_HTML = '<html><head><title>x</title></head><body></body></html>';
|
|
25
|
+
|
|
26
|
+
describe('detectHtmlMode', () => {
|
|
27
|
+
it('reads dev marker', () => {
|
|
28
|
+
expect(detectHtmlMode(DEV_HTML)).toBe('dev');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('reads prod marker', () => {
|
|
32
|
+
expect(detectHtmlMode(PROD_HTML)).toBe('prod');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('returns unknown when no marker', () => {
|
|
36
|
+
expect(detectHtmlMode(UNMARKED_HTML)).toBe('unknown');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('preserveDevCache', () => {
|
|
41
|
+
let tmpRoot: string;
|
|
42
|
+
let logs: LogEntry[];
|
|
43
|
+
|
|
44
|
+
function setupDist(
|
|
45
|
+
files: Record<string, string | null> = {}
|
|
46
|
+
): { distPath: string; clientDir: string; serverDir: string } {
|
|
47
|
+
const distPath = path.join(tmpRoot, 'dist');
|
|
48
|
+
const clientDir = path.join(distPath, 'client');
|
|
49
|
+
const serverDir = path.join(distPath, 'server');
|
|
50
|
+
fs.mkdirSync(clientDir, { recursive: true });
|
|
51
|
+
fs.mkdirSync(serverDir, { recursive: true });
|
|
52
|
+
// 默认放个 nest 编译产物 + routes JSON, 用来验证保留策略
|
|
53
|
+
fs.writeFileSync(path.join(serverDir, 'main.js'), '/* nest bundle */');
|
|
54
|
+
fs.writeFileSync(
|
|
55
|
+
path.join(distPath, 'api-routes.json'),
|
|
56
|
+
JSON.stringify([])
|
|
57
|
+
);
|
|
58
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
59
|
+
if (content === null) continue;
|
|
60
|
+
const abs = path.join(distPath, rel);
|
|
61
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
62
|
+
fs.writeFileSync(abs, content);
|
|
63
|
+
}
|
|
64
|
+
return { distPath, clientDir, serverDir };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
beforeEach(() => {
|
|
68
|
+
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'preserve-dev-cache-'));
|
|
69
|
+
logs = [];
|
|
70
|
+
delete process.env.FORCE_CLEAN_DIST;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
afterEach(() => {
|
|
74
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
75
|
+
delete process.env.FORCE_CLEAN_DIST;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('no-op when dist/ does not exist', () => {
|
|
79
|
+
const result = preserveDevCache(tmpRoot, { log: makeLogger(logs) });
|
|
80
|
+
expect(result.action).toBe('no_dist');
|
|
81
|
+
expect(fs.existsSync(path.join(tmpRoot, 'dist'))).toBe(false);
|
|
82
|
+
expect(logs[0].msg).toMatch(/no dist/);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('preserves entire dist/client when index.html has dev marker', () => {
|
|
86
|
+
const { clientDir, serverDir } = setupDist({
|
|
87
|
+
'client/index.html': DEV_HTML,
|
|
88
|
+
'client/assets/foo.js': '/* dev asset */',
|
|
89
|
+
});
|
|
90
|
+
const result = preserveDevCache(tmpRoot, { log: makeLogger(logs) });
|
|
91
|
+
expect(result.action).toBe('preserved');
|
|
92
|
+
expect(result.mode).toBe('dev');
|
|
93
|
+
expect(fs.existsSync(path.join(clientDir, 'index.html'))).toBe(true);
|
|
94
|
+
expect(fs.existsSync(path.join(clientDir, 'assets', 'foo.js'))).toBe(true);
|
|
95
|
+
expect(fs.existsSync(path.join(serverDir, 'main.js'))).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('cleans index.html + assets/ but preserves server/ when marker=prod', () => {
|
|
99
|
+
const { distPath, clientDir, serverDir } = setupDist({
|
|
100
|
+
'client/index.html': PROD_HTML,
|
|
101
|
+
'client/assets/index-abc.js': '/* build product */',
|
|
102
|
+
'client/assets/index-def.css': '/* build product */',
|
|
103
|
+
});
|
|
104
|
+
const result = preserveDevCache(tmpRoot, { log: makeLogger(logs) });
|
|
105
|
+
expect(result.action).toBe('cleaned_polluted');
|
|
106
|
+
expect(result.mode).toBe('prod');
|
|
107
|
+
expect(fs.existsSync(path.join(clientDir, 'index.html'))).toBe(false);
|
|
108
|
+
expect(fs.existsSync(path.join(clientDir, 'assets'))).toBe(false);
|
|
109
|
+
// dist/server + routes JSON 保留
|
|
110
|
+
expect(fs.existsSync(path.join(serverDir, 'main.js'))).toBe(true);
|
|
111
|
+
expect(fs.existsSync(path.join(distPath, 'api-routes.json'))).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('cleans same way when marker is missing (treats as pollution)', () => {
|
|
115
|
+
const { clientDir, serverDir } = setupDist({
|
|
116
|
+
'client/index.html': UNMARKED_HTML,
|
|
117
|
+
'client/assets/foo.js': '/* whatever */',
|
|
118
|
+
});
|
|
119
|
+
const result = preserveDevCache(tmpRoot, { log: makeLogger(logs) });
|
|
120
|
+
expect(result.action).toBe('cleaned_polluted');
|
|
121
|
+
expect(result.mode).toBe('unknown');
|
|
122
|
+
expect(fs.existsSync(path.join(clientDir, 'index.html'))).toBe(false);
|
|
123
|
+
expect(fs.existsSync(path.join(clientDir, 'assets'))).toBe(false);
|
|
124
|
+
expect(fs.existsSync(path.join(serverDir, 'main.js'))).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('no-op when dist/client/index.html is missing (nest --watch has partial dist)', () => {
|
|
128
|
+
const { distPath, serverDir } = setupDist();
|
|
129
|
+
const result = preserveDevCache(tmpRoot, { log: makeLogger(logs) });
|
|
130
|
+
expect(result.action).toBe('no_index');
|
|
131
|
+
expect(fs.existsSync(distPath)).toBe(true);
|
|
132
|
+
expect(fs.existsSync(path.join(serverDir, 'main.js'))).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('FORCE_CLEAN_DIST=true wipes entire dist regardless of marker', () => {
|
|
136
|
+
const { distPath } = setupDist({
|
|
137
|
+
'client/index.html': DEV_HTML,
|
|
138
|
+
'client/assets/foo.js': 'x',
|
|
139
|
+
});
|
|
140
|
+
const result = preserveDevCache(tmpRoot, {
|
|
141
|
+
log: makeLogger(logs),
|
|
142
|
+
forceClean: true,
|
|
143
|
+
});
|
|
144
|
+
expect(result.action).toBe('force_cleaned');
|
|
145
|
+
expect(fs.existsSync(distPath)).toBe(false);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('FORCE_CLEAN_DIST env var enables force clean path', () => {
|
|
149
|
+
process.env.FORCE_CLEAN_DIST = 'true';
|
|
150
|
+
const { distPath } = setupDist({
|
|
151
|
+
'client/index.html': DEV_HTML,
|
|
152
|
+
});
|
|
153
|
+
const result = preserveDevCache(tmpRoot, { log: makeLogger(logs) });
|
|
154
|
+
expect(result.action).toBe('force_cleaned');
|
|
155
|
+
expect(fs.existsSync(distPath)).toBe(false);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('emits INFO log with mode and action', () => {
|
|
159
|
+
setupDist({
|
|
160
|
+
'client/index.html': PROD_HTML,
|
|
161
|
+
'client/assets/foo.js': 'x',
|
|
162
|
+
});
|
|
163
|
+
preserveDevCache(tmpRoot, { log: makeLogger(logs) });
|
|
164
|
+
const msgs = logs.map((l) => l.msg).join('\n');
|
|
165
|
+
expect(msgs).toContain('prod');
|
|
166
|
+
expect(msgs).toContain('cleaned');
|
|
167
|
+
});
|
|
168
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// dev.js 启动时替代无脑 rm -rf dist/ 的 lib: 保留 dist/server (nest --watch 增量编译起点)
|
|
4
|
+
// + 按 HTML mode marker 决定 dist/client 是保留还是清污染。
|
|
5
|
+
// 跟 preset 侧 html-output plugin 的 injectHtmlModeMarker / detectHtmlMode 配套:
|
|
6
|
+
// preset 写盘时打 marker (dev 版 vs prod 污染版), 这里读 marker 定去留。
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 从 HTML 里回读 <meta name="miaoda-html-mode" content="dev|prod"> marker。
|
|
13
|
+
* 与 packages/tools/fullstack-vite-preset/src/vite-plugins/html-output-plugin.ts
|
|
14
|
+
* 的 detectHtmlMode 同源, 为了让 dev.js 模板脚本零依赖跑起来这里重复一份。
|
|
15
|
+
*/
|
|
16
|
+
function detectHtmlMode(htmlContent) {
|
|
17
|
+
const match = htmlContent.match(
|
|
18
|
+
/<meta\s+name=["']miaoda-html-mode["']\s+content=["'](dev|prod)["'][^>]*>/i
|
|
19
|
+
);
|
|
20
|
+
return (match && match[1]) || 'unknown';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 定向清理 dist/ 的可复用副本, 替代 dev.js 之前无脑 `rm -rf dist/` 的 cleanStaleDist。
|
|
25
|
+
*
|
|
26
|
+
* 决策规则:
|
|
27
|
+
* - FORCE_CLEAN_DIST=true → 走 escape hatch 全清 (preserveDevCache 逻辑 / 保留的
|
|
28
|
+
* dist/server 出 stale 时兜底)
|
|
29
|
+
* - dist/ 不存在 → 无动作
|
|
30
|
+
* - dist/client/index.html 存在且 marker=dev → 整个 dist/client/ 保留 (dev cache hit)
|
|
31
|
+
* - 存在但 marker=prod 或缺失 → 只清 dist/client/index.html + dist/client/assets/
|
|
32
|
+
* (build 污染部分), dist/server/ 与 routes JSON 保留
|
|
33
|
+
* - 不存在 → 无动作, htmlOutputPlugin 稍后会写入
|
|
34
|
+
*
|
|
35
|
+
* @param {string} projectRoot 项目根目录
|
|
36
|
+
* @param {object} [options]
|
|
37
|
+
* @param {(level: string, msg: string) => void} [options.log] 日志回调, 默认 console.log
|
|
38
|
+
* @param {boolean} [options.forceClean] 覆盖 FORCE_CLEAN_DIST env var 判断 (给测试用)
|
|
39
|
+
* @returns {{ action: 'no_dist' | 'force_cleaned' | 'no_index' | 'preserved' | 'cleaned_polluted' | 'read_failed_cleaned' | 'cleanup_failed', mode: string }}
|
|
40
|
+
*/
|
|
41
|
+
function preserveDevCache(projectRoot, options) {
|
|
42
|
+
const log = (options && options.log) || defaultLogger;
|
|
43
|
+
const forceClean =
|
|
44
|
+
options && typeof options.forceClean === 'boolean'
|
|
45
|
+
? options.forceClean
|
|
46
|
+
: process.env.FORCE_CLEAN_DIST === 'true';
|
|
47
|
+
|
|
48
|
+
const distPath = path.join(projectRoot, 'dist');
|
|
49
|
+
if (!fs.existsSync(distPath)) {
|
|
50
|
+
log('INFO', 'preserveDevCache: no dist/, will be created by vite/nest');
|
|
51
|
+
return { action: 'no_dist', mode: 'unknown' };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (forceClean) {
|
|
55
|
+
fs.rmSync(distPath, { recursive: true, force: true });
|
|
56
|
+
log(
|
|
57
|
+
'INFO',
|
|
58
|
+
'preserveDevCache: FORCE_CLEAN_DIST=true, cleaned entire dist/'
|
|
59
|
+
);
|
|
60
|
+
return { action: 'force_cleaned', mode: 'unknown' };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const clientDir = path.join(distPath, 'client');
|
|
64
|
+
const indexHtml = path.join(clientDir, 'index.html');
|
|
65
|
+
|
|
66
|
+
if (!fs.existsSync(indexHtml)) {
|
|
67
|
+
log(
|
|
68
|
+
'INFO',
|
|
69
|
+
'preserveDevCache: no dist/client/index.html, other artifacts preserved'
|
|
70
|
+
);
|
|
71
|
+
return { action: 'no_index', mode: 'unknown' };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let mode = 'unknown';
|
|
75
|
+
let readFailed = false;
|
|
76
|
+
try {
|
|
77
|
+
const content = fs.readFileSync(indexHtml, 'utf-8');
|
|
78
|
+
mode = detectHtmlMode(content);
|
|
79
|
+
} catch (e) {
|
|
80
|
+
log(
|
|
81
|
+
'WARN',
|
|
82
|
+
`preserveDevCache: read dist/client/index.html failed: ${e.message}, treating as polluted`
|
|
83
|
+
);
|
|
84
|
+
readFailed = true;
|
|
85
|
+
mode = 'unknown';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (mode === 'dev') {
|
|
89
|
+
log('INFO', 'preserveDevCache: dist/client preserved (dev cache hit)');
|
|
90
|
+
return { action: 'preserved', mode: 'dev' };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// marker=prod 或 unknown → build:client 污染 (或人为放的产物), 定向清 index.html + assets/
|
|
94
|
+
// dist/server/, dist/*.json 保留
|
|
95
|
+
try {
|
|
96
|
+
fs.rmSync(indexHtml, { force: true });
|
|
97
|
+
const assetsDir = path.join(clientDir, 'assets');
|
|
98
|
+
if (fs.existsSync(assetsDir)) {
|
|
99
|
+
fs.rmSync(assetsDir, { recursive: true, force: true });
|
|
100
|
+
}
|
|
101
|
+
log(
|
|
102
|
+
'INFO',
|
|
103
|
+
`preserveDevCache: dist/client/index.html was ${mode}, cleaned index.html + assets/ (dist/server preserved)`
|
|
104
|
+
);
|
|
105
|
+
return {
|
|
106
|
+
action: readFailed ? 'read_failed_cleaned' : 'cleaned_polluted',
|
|
107
|
+
mode,
|
|
108
|
+
};
|
|
109
|
+
} catch (e) {
|
|
110
|
+
log(
|
|
111
|
+
'WARN',
|
|
112
|
+
`preserveDevCache: cleanup failed: ${e.message}, continuing anyway (index.html / assets may still be polluted)`
|
|
113
|
+
);
|
|
114
|
+
return { action: 'cleanup_failed', mode };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function defaultLogger(level, msg) {
|
|
119
|
+
// eslint-disable-next-line no-console
|
|
120
|
+
console.log(`[${level}] [main] ${msg}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = { detectHtmlMode, preserveDevCache };
|