@lark-apaas/fullstack-cli 1.1.59-alpha.20260818125424 → 1.1.59-beta.0

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
@@ -7698,7 +7698,7 @@ function sanitizeStructuredLog(value) {
7698
7698
  delete sanitized.pid;
7699
7699
  return sanitized;
7700
7700
  }
7701
- var TRANSIENT_CONNECTION_ERROR_PATTERN = /ECONNREFUSED|ECONNRESET|ETIMEDOUT|ENETUNREACH|proxy error|\[Proxy\] (?:Error:\s*$|Error during|Connection error|Non-connection error|Headers already sent|Service (?:recovered|did not recover))/i;
7701
+ var TRANSIENT_CONNECTION_ERROR_PATTERN = /ECONNREFUSED|ECONNRESET|ETIMEDOUT|ENETUNREACH|socket hang up|proxy error|\[Proxy\] (?:Error:\s*$|Error during|Connection error|Non-connection error|Headers already sent|Service (?:recovered|did not recover))/i;
7702
7702
  function hasErrorInStdLines(lines) {
7703
7703
  const filtered = lines.filter((line) => !TRANSIENT_CONNECTION_ERROR_PATTERN.test(line));
7704
7704
  const combined = filtered.join("\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/fullstack-cli",
3
- "version": "1.1.59-alpha.20260818125424",
3
+ "version": "1.1.59-beta.0",
4
4
  "description": "CLI tool for fullstack template management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -252,24 +252,20 @@ process.on('SIGTERM', cleanup);
252
252
  process.on('SIGINT', cleanup);
253
253
  process.on('SIGHUP', cleanup);
254
254
 
255
- // Track A / P0 #5 保守版实现: 不再无脑 rm -rf dist/, 改成定向保留可复用产物 + 只清
256
- // 「build:client 污染的部分」。收益: nest --watch 增量编译起点 (8-13s) + routes JSON。
257
- //
258
- // 逻辑抽到 lib/preserve-dev-cache.js 便于单测; 见该文件头部注释。
259
- // fullstack-vite-preset/html-output-plugin.ts injectHtmlModeMarker 配套:
260
- // preset 写盘时打 marker, 这里按 marker 决定要不要保留 dist/client。
261
- //
262
- // Escape hatch: FORCE_CLEAN_DIST=true 走 lib 里的全清路径,
263
- // preserveDevCache 逻辑有 bug 或 nest --watch 因保留的 dist/server 出 stale 时降级用。
264
- const { preserveDevCache } = require('./lib/preserve-dev-cache.cjs');
255
+ // Stale dist makes nest --watch skip missing files; watcher won't self-heal.
256
+ function cleanStaleDist() {
257
+ const distPath = path.join(PROJECT_ROOT, 'dist');
258
+ if (fs.existsSync(distPath)) {
259
+ fs.rmSync(distPath, { recursive: true, force: true });
260
+ logEvent('INFO', 'main', 'Cleaned dist/ to force full rebuild');
261
+ }
262
+ }
265
263
 
266
264
  // ── Main ──────────────────────────────────────────────────────────────────────
267
265
  async function main() {
268
266
  logEvent('INFO', 'main', '========== Dev session started ==========');
269
267
 
270
- preserveDevCache(PROJECT_ROOT, {
271
- log: (level, msg) => logEvent(level, 'main', msg),
272
- });
268
+ cleanStaleDist();
273
269
 
274
270
  // Initialize action plugins
275
271
  writeOutput('\n🔌 Initializing action plugins...\n');
@@ -76,13 +76,34 @@ function isStylelintTarget(filePath) {
76
76
  return filePath.endsWith('.css');
77
77
  }
78
78
 
79
+ const STYLELINT_CONFIG_FILES = [
80
+ '.stylelintrc',
81
+ '.stylelintrc.js',
82
+ '.stylelintrc.cjs',
83
+ '.stylelintrc.json',
84
+ 'stylelint.config.js',
85
+ ];
86
+
87
+ /**
88
+ * 老模板(fullstack-nestjs-template 1.x 时代)的应用没有 stylelint 配置,跑 stylelint 会以
89
+ * "ConfigurationError: No configuration provided" 挂掉、把 pre-commit 卡死。没配置就跳过。
90
+ */
91
+ function hasStylelintConfig() {
92
+ return STYLELINT_CONFIG_FILES.some(file => fs.existsSync(path.join(cwd, file)));
93
+ }
94
+
79
95
  async function runDefaultLint() {
80
96
  const taskSpecs = [
81
97
  [getBinName('npm'), ['run', 'eslint']],
82
98
  [getBinName('npm'), ['run', 'type:check']],
83
- [getBinName('npm'), ['run', 'stylelint']],
84
99
  ];
85
100
 
101
+ if (hasStylelintConfig()) {
102
+ taskSpecs.push([getBinName('npm'), ['run', 'stylelint']]);
103
+ } else {
104
+ console.warn('[lint] Skip stylelint: no stylelint config found');
105
+ }
106
+
86
107
  process.exit(await runTasksSerially(taskSpecs));
87
108
  }
88
109
 
@@ -120,7 +141,9 @@ async function runSelectiveLint(inputFiles) {
120
141
  taskSpecs.push([getBinName('npx'), ['eslint', '--quiet', ...eslintFiles]]);
121
142
  }
122
143
 
123
- if (stylelintFiles.length > 0) {
144
+ if (stylelintFiles.length > 0 && !hasStylelintConfig()) {
145
+ console.warn('[lint] Skip stylelint: no stylelint config found');
146
+ } else if (stylelintFiles.length > 0) {
124
147
  taskSpecs.push([getBinName('npx'), ['stylelint', '--quiet', ...stylelintFiles]]);
125
148
  }
126
149
 
@@ -1,168 +0,0 @@
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
- });
@@ -1,122 +0,0 @@
1
- 'use strict';
2
-
3
- // Track A / P0 #5 保守版实现所在。跟 fullstack-vite-preset/html-output-plugin.ts
4
- // 的 detectHtmlMode / injectHtmlModeMarker 是配套的一对: preset 写盘时打 marker,
5
- // dev.js 启动时按 marker 决定要不要保留 dist/client。
6
-
7
- const fs = require('fs');
8
- const path = require('path');
9
-
10
- /**
11
- * 从 HTML 里回读 <meta name="miaoda-html-mode" content="dev|prod"> marker。
12
- * 与 packages/tools/fullstack-vite-preset/src/vite-plugins/html-output-plugin.ts
13
- * 的 detectHtmlMode 同源, 为了让 dev.js 模板脚本零依赖跑起来这里重复一份。
14
- */
15
- function detectHtmlMode(htmlContent) {
16
- const match = htmlContent.match(
17
- /<meta\s+name=["']miaoda-html-mode["']\s+content=["'](dev|prod)["'][^>]*>/i
18
- );
19
- return (match && match[1]) || 'unknown';
20
- }
21
-
22
- /**
23
- * 定向清理 dist/ 的可复用副本, 替代 dev.js 之前无脑 `rm -rf dist/` 的 cleanStaleDist。
24
- *
25
- * 决策规则:
26
- * - FORCE_CLEAN_DIST=true → 走 escape hatch 全清 (preserveDevCache 逻辑 / 保留的
27
- * dist/server 出 stale 时兜底)
28
- * - dist/ 不存在 → 无动作
29
- * - dist/client/index.html 存在且 marker=dev → 整个 dist/client/ 保留 (dev cache hit)
30
- * - 存在但 marker=prod 或缺失 → 只清 dist/client/index.html + dist/client/assets/
31
- * (build 污染部分), dist/server/ 与 routes JSON 保留
32
- * - 不存在 → 无动作, htmlOutputPlugin 稍后会写入
33
- *
34
- * @param {string} projectRoot 项目根目录
35
- * @param {object} [options]
36
- * @param {(level: string, msg: string) => void} [options.log] 日志回调, 默认 console.log
37
- * @param {boolean} [options.forceClean] 覆盖 FORCE_CLEAN_DIST env var 判断 (给测试用)
38
- * @returns {{ action: 'no_dist' | 'force_cleaned' | 'no_index' | 'preserved' | 'cleaned_polluted' | 'read_failed_cleaned' | 'cleanup_failed', mode: string }}
39
- */
40
- function preserveDevCache(projectRoot, options) {
41
- const log = (options && options.log) || defaultLogger;
42
- const forceClean =
43
- options && typeof options.forceClean === 'boolean'
44
- ? options.forceClean
45
- : process.env.FORCE_CLEAN_DIST === 'true';
46
-
47
- const distPath = path.join(projectRoot, 'dist');
48
- if (!fs.existsSync(distPath)) {
49
- log('INFO', 'preserveDevCache: no dist/, will be created by vite/nest');
50
- return { action: 'no_dist', mode: 'unknown' };
51
- }
52
-
53
- if (forceClean) {
54
- fs.rmSync(distPath, { recursive: true, force: true });
55
- log(
56
- 'INFO',
57
- 'preserveDevCache: FORCE_CLEAN_DIST=true, cleaned entire dist/'
58
- );
59
- return { action: 'force_cleaned', mode: 'unknown' };
60
- }
61
-
62
- const clientDir = path.join(distPath, 'client');
63
- const indexHtml = path.join(clientDir, 'index.html');
64
-
65
- if (!fs.existsSync(indexHtml)) {
66
- log(
67
- 'INFO',
68
- 'preserveDevCache: no dist/client/index.html, other artifacts preserved'
69
- );
70
- return { action: 'no_index', mode: 'unknown' };
71
- }
72
-
73
- let mode = 'unknown';
74
- let readFailed = false;
75
- try {
76
- const content = fs.readFileSync(indexHtml, 'utf-8');
77
- mode = detectHtmlMode(content);
78
- } catch (e) {
79
- log(
80
- 'WARN',
81
- `preserveDevCache: read dist/client/index.html failed: ${e.message}, treating as polluted`
82
- );
83
- readFailed = true;
84
- mode = 'unknown';
85
- }
86
-
87
- if (mode === 'dev') {
88
- log('INFO', 'preserveDevCache: dist/client preserved (dev cache hit)');
89
- return { action: 'preserved', mode: 'dev' };
90
- }
91
-
92
- // marker=prod 或 unknown → build:client 污染 (或人为放的产物), 定向清 index.html + assets/
93
- // dist/server/, dist/*.json 保留
94
- try {
95
- fs.rmSync(indexHtml, { force: true });
96
- const assetsDir = path.join(clientDir, 'assets');
97
- if (fs.existsSync(assetsDir)) {
98
- fs.rmSync(assetsDir, { recursive: true, force: true });
99
- }
100
- log(
101
- 'INFO',
102
- `preserveDevCache: dist/client/index.html was ${mode}, cleaned index.html + assets/ (dist/server preserved)`
103
- );
104
- return {
105
- action: readFailed ? 'read_failed_cleaned' : 'cleaned_polluted',
106
- mode,
107
- };
108
- } catch (e) {
109
- log(
110
- 'WARN',
111
- `preserveDevCache: cleanup failed: ${e.message}, continuing anyway (index.html / assets may still be polluted)`
112
- );
113
- return { action: 'cleanup_failed', mode };
114
- }
115
- }
116
-
117
- function defaultLogger(level, msg) {
118
- // eslint-disable-next-line no-console
119
- console.log(`[${level}] [main] ${msg}`);
120
- }
121
-
122
- module.exports = { detectHtmlMode, preserveDevCache };