@lark-apaas/miaoda-cli 0.1.46 → 0.1.48

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.
@@ -0,0 +1,634 @@
1
+ "use strict";
2
+ /**
3
+ * migrate 定向适配的纯函数集合:输入文件文本,输出新文本或冲突原因,不读写磁盘。
4
+ *
5
+ * 这些函数只处理"本次转换必须改的键/片段",其余内容原样保留;遇到无法确定性处理的形态
6
+ * 返回 conflict,由调用方报冲突并保持原工程不变。不是通用三方合并器。
7
+ */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.RELAX_ESLINT_MARKER = void 0;
43
+ exports.parseJsoncObject = parseJsoncObject;
44
+ exports.rewriteSrcPath = rewriteSrcPath;
45
+ exports.adaptTsconfig = adaptTsconfig;
46
+ exports.adaptHtmlEntry = adaptHtmlEntry;
47
+ exports.adaptNestAppModule = adaptNestAppModule;
48
+ exports.ensureScripts = ensureScripts;
49
+ exports.normalizeConfigSource = normalizeConfigSource;
50
+ exports.matchesKnownDefault = matchesKnownDefault;
51
+ exports.rewriteLegacyLayoutGlob = rewriteLegacyLayoutGlob;
52
+ exports.carryEslintIgnores = carryEslintIgnores;
53
+ exports.relaxEslintRules = relaxEslintRules;
54
+ const jsonc = __importStar(require("jsonc-parser"));
55
+ const JSONC_FORMAT = {
56
+ formattingOptions: { insertSpaces: true, tabSize: 2, eol: '\n' },
57
+ };
58
+ function isPlainObject(value) {
59
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
60
+ }
61
+ /** 解析 JSON(C);不是对象或有语法错误时返回 undefined */
62
+ function parseJsoncObject(text) {
63
+ const errors = [];
64
+ const value = jsonc.parse(text, errors, { allowTrailingComma: true });
65
+ if (errors.length > 0 || !isPlainObject(value))
66
+ return undefined;
67
+ return value;
68
+ }
69
+ function setJsonc(text, jsonPath, value) {
70
+ return jsonc.applyEdits(text, jsonc.modify(text, jsonPath, value, JSONC_FORMAT));
71
+ }
72
+ function sameJson(a, b) {
73
+ return JSON.stringify(a) === JSON.stringify(b);
74
+ }
75
+ const SRC_PATH_RE = /^(\.\/)?src(\/|$)/;
76
+ /** `./src/*` → `./client/src/*`,`src` → `client/src`;其他原样 */
77
+ function rewriteSrcPath(value) {
78
+ return value.replace(SRC_PATH_RE, (_m, dot, sep) => {
79
+ return `${dot ?? ''}client/src${sep}`;
80
+ });
81
+ }
82
+ function isSrcIncludeEntry(value) {
83
+ return typeof value === 'string' && /^(\.\/)?src(\/.*)?$/.test(value);
84
+ }
85
+ function isViteOnlyInclude(value) {
86
+ return (Array.isArray(value) &&
87
+ value.length > 0 &&
88
+ value.every((e) => typeof e === 'string' && /^(\.\/)?vite\.config\.(ts|mts|js|mjs)$/.test(e)));
89
+ }
90
+ function getCompilerOptions(obj) {
91
+ const co = obj.compilerOptions;
92
+ return isPlainObject(co) ? co : {};
93
+ }
94
+ function getPaths(obj) {
95
+ const paths = getCompilerOptions(obj).paths;
96
+ return isPlainObject(paths) ? paths : {};
97
+ }
98
+ /** 把 paths 里每个值数组中 src 起头的条目改写到 client/src;返回改写后的文本与改动数 */
99
+ function rewritePathsValues(text, obj) {
100
+ let next = text;
101
+ let count = 0;
102
+ for (const [key, raw] of Object.entries(getPaths(obj))) {
103
+ if (!Array.isArray(raw))
104
+ continue;
105
+ const rewritten = raw.map((v) => typeof v === 'string' ? rewriteSrcPath(v) : v);
106
+ if (!sameJson(rewritten, raw)) {
107
+ next = setJsonc(next, ['compilerOptions', 'paths', key], rewritten);
108
+ count++;
109
+ }
110
+ }
111
+ return { text: next, count };
112
+ }
113
+ /** 模板里有、用户缺失的 compilerOptions 键补齐(paths 单独处理) */
114
+ function addMissingCompilerOptions(text, user, tpl) {
115
+ let next = text;
116
+ const added = [];
117
+ const userCo = getCompilerOptions(user);
118
+ for (const [key, value] of Object.entries(getCompilerOptions(tpl))) {
119
+ if (key === 'paths')
120
+ continue;
121
+ if (Object.prototype.hasOwnProperty.call(userCo, key))
122
+ continue;
123
+ next = setJsonc(next, ['compilerOptions', key], value);
124
+ added.push(key);
125
+ }
126
+ return { text: next, added };
127
+ }
128
+ function addMissingPaths(text, user, tpl) {
129
+ let next = text;
130
+ const added = [];
131
+ const userPaths = getPaths(user);
132
+ for (const [key, value] of Object.entries(getPaths(tpl))) {
133
+ if (Object.prototype.hasOwnProperty.call(userPaths, key))
134
+ continue;
135
+ next = setJsonc(next, ['compilerOptions', 'paths', key], value);
136
+ added.push(key);
137
+ }
138
+ return { text: next, added };
139
+ }
140
+ /**
141
+ * 顶层数组(exclude)里 src 起头的条目改写到 client/src。用户排除的目录随 src → client/src
142
+ * 搬迁,不改写就指向不存在的路径、排除失效(典型:旧模板排除 src/components/ui,迁移后
143
+ * shadcn 组件被纳入类型检查)。
144
+ */
145
+ function rewriteSrcArrayKey(text, obj, key) {
146
+ const raw = obj[key];
147
+ if (!Array.isArray(raw))
148
+ return text;
149
+ const rewritten = raw.map((v) => (typeof v === 'string' ? rewriteSrcPath(v) : v));
150
+ if (sameJson(rewritten, raw))
151
+ return text;
152
+ return setJsonc(text, [key], rewritten);
153
+ }
154
+ function unionArrayKey(text, user, tpl, key) {
155
+ const tplRaw = tpl[key];
156
+ if (!Array.isArray(tplRaw))
157
+ return text;
158
+ const tplArr = tplRaw;
159
+ const userArr = Array.isArray(user[key]) ? user[key] : [];
160
+ const missing = tplArr.filter((e) => !userArr.some((u) => sameJson(u, e)));
161
+ if (missing.length === 0)
162
+ return text;
163
+ return setJsonc(text, [key], [...userArr, ...missing]);
164
+ }
165
+ /**
166
+ * tsconfig 定向适配,语义见 AdaptTsconfigRule。
167
+ * userText 为 undefined 表示用户侧文件不存在 —— 'root' / 'node' 直接采用模板。
168
+ */
169
+ function adaptTsconfig(opts) {
170
+ const tpl = parseJsoncObject(opts.templateText);
171
+ if (tpl === undefined) {
172
+ return { ok: false, conflict: 'template tsconfig is not valid JSON' };
173
+ }
174
+ if (opts.userText === undefined) {
175
+ return { ok: true, text: opts.templateText, changed: true, detail: 'created from template' };
176
+ }
177
+ const user = parseJsoncObject(opts.userText);
178
+ if (user === undefined) {
179
+ return { ok: false, conflict: 'not a parsable JSON(C) object; cannot adapt automatically' };
180
+ }
181
+ if (opts.role === 'root') {
182
+ const { text: pathsText, count } = rewritePathsValues(opts.userText, user);
183
+ const text = rewriteSrcArrayKey(pathsText, user, 'exclude');
184
+ return {
185
+ ok: true,
186
+ text,
187
+ changed: text !== opts.userText,
188
+ detail: `${count.toString()} path(s) rewritten`,
189
+ };
190
+ }
191
+ if (opts.role === 'node') {
192
+ const tplExtends = tpl.extends;
193
+ if (user.extends === undefined && isViteOnlyInclude(user.include)) {
194
+ // 旧 stack 的 tsconfig.node.json 只服务 vite.config,与新 stack 同名文件职责不同
195
+ return {
196
+ ok: true,
197
+ text: opts.templateText,
198
+ changed: true,
199
+ replaced: true,
200
+ detail: 'replaced vite-only config',
201
+ };
202
+ }
203
+ if (tplExtends === undefined || !sameJson(user.extends, tplExtends)) {
204
+ return {
205
+ ok: false,
206
+ conflict: `extends ${JSON.stringify(user.extends ?? null)} is neither the vite-only legacy form nor ${JSON.stringify(tplExtends ?? null)}`,
207
+ };
208
+ }
209
+ let text = opts.userText;
210
+ const co = addMissingCompilerOptions(text, user, tpl);
211
+ text = co.text;
212
+ const paths = addMissingPaths(text, user, tpl);
213
+ text = paths.text;
214
+ text = unionArrayKey(text, parseJsoncObject(text) ?? user, tpl, 'include');
215
+ text = unionArrayKey(text, parseJsoncObject(text) ?? user, tpl, 'exclude');
216
+ return { ok: true, text, changed: text !== opts.userText };
217
+ }
218
+ // role === 'app'
219
+ let text = opts.userText;
220
+ const tplExtends = tpl.extends;
221
+ const userExtends = user.extends;
222
+ const legacy = opts.legacyExtends ?? [];
223
+ if (!sameJson(userExtends, tplExtends)) {
224
+ const replaceable = userExtends === undefined ||
225
+ (typeof userExtends === 'string' && legacy.includes(userExtends));
226
+ if (!replaceable || tplExtends === undefined) {
227
+ return {
228
+ ok: false,
229
+ conflict: `extends ${JSON.stringify(userExtends)} is not a recognized platform preset`,
230
+ };
231
+ }
232
+ text = setJsonc(text, ['extends'], tplExtends);
233
+ }
234
+ const tplInclude = Array.isArray(tpl.include) ? tpl.include : [];
235
+ const userInclude = Array.isArray(user.include) ? user.include : [];
236
+ const keptInclude = userInclude.filter((e) => !isSrcIncludeEntry(e) && !tplInclude.some((t) => sameJson(t, e)));
237
+ const nextInclude = [...tplInclude, ...keptInclude];
238
+ if (!sameJson(nextInclude, userInclude)) {
239
+ text = setJsonc(text, ['include'], nextInclude);
240
+ }
241
+ const rewritten = rewritePathsValues(text, user);
242
+ text = rewritten.text;
243
+ text = addMissingPaths(text, user, tpl).text;
244
+ text = addMissingCompilerOptions(text, user, tpl).text;
245
+ text = rewriteSrcArrayKey(text, user, 'exclude');
246
+ text = unionArrayKey(text, parseJsoncObject(text) ?? user, tpl, 'exclude');
247
+ return { ok: true, text, changed: text !== opts.userText };
248
+ }
249
+ // ---------------------------------------------------------------------------
250
+ // HTML 入口
251
+ // ---------------------------------------------------------------------------
252
+ const HEAD_RE = /<head(\s[^>]*)?>([\s\S]*?)<\/head>/i;
253
+ const BODY_RE = /<body(\s[^>]*)?>([\s\S]*?)<\/body>/i;
254
+ const HTML_OPEN_RE = /<html(\s[^>]*)?>/i;
255
+ const ELEMENT_TOKEN_RE = /<!--[\s\S]*?-->|<(title|script|style|noscript)\b[^>]*>[\s\S]*?<\/\1\s*>|<(?:meta|link|base)\b[^>]*\/?>/gi;
256
+ const BODY_TOKEN_RE = /<!--[\s\S]*?-->|<(script|style|noscript|div|template)\b[^>]*>[\s\S]*?<\/\1\s*>|<(?:link|meta)\b[^>]*\/?>/gi;
257
+ function tokenize(content, re) {
258
+ const tokens = content.match(re) ?? [];
259
+ let rest = content;
260
+ for (const t of tokens)
261
+ rest = rest.replace(t, '');
262
+ if (rest.trim() !== '')
263
+ return undefined;
264
+ return tokens;
265
+ }
266
+ function attr(tag, name) {
267
+ const m = new RegExp(`\\b${name}\\s*=\\s*(["'])([^"']*)\\1`, 'i').exec(tag);
268
+ return m?.[2];
269
+ }
270
+ /** 可选捕获组:未参与匹配时为 undefined(TS 类型标成 string),统一回落空串 */
271
+ function group(match, index) {
272
+ return match[index] ?? '';
273
+ }
274
+ function openTag(token) {
275
+ return /^<[^>]*>/.exec(token)?.[0] ?? token;
276
+ }
277
+ function isPlatformHeadToken(token, legacyEntry) {
278
+ if (token.startsWith('<!--'))
279
+ return true;
280
+ const tag = openTag(token).toLowerCase();
281
+ if (tag.startsWith('<meta') && attr(tag, 'charset') !== undefined)
282
+ return true;
283
+ if (tag.startsWith('<meta') && attr(tag, 'name') === 'viewport')
284
+ return true;
285
+ if (tag.startsWith('<link') && (attr(tag, 'rel') ?? '').includes('icon')) {
286
+ return attr(tag, 'href') === '/favicon.svg';
287
+ }
288
+ if (tag.startsWith('<script') && attr(tag, 'src') === legacyEntry)
289
+ return true;
290
+ return false;
291
+ }
292
+ function isPlatformBodyToken(token, legacyEntry) {
293
+ if (token.startsWith('<!--'))
294
+ return true;
295
+ const tag = openTag(token).toLowerCase();
296
+ if (tag.startsWith('<script') && attr(tag, 'src') === legacyEntry)
297
+ return true;
298
+ if (tag.startsWith('<div') && attr(tag, 'id') === 'root') {
299
+ return /^<div\b[^>]*>\s*<\/div\s*>$/i.test(token);
300
+ }
301
+ return false;
302
+ }
303
+ /** 用户自定义片段里引用旧布局 /src/ 的地址同步到 /client/src/ */
304
+ function rewriteSrcRefs(fragment) {
305
+ return fragment.replace(/\b(src|href)=(["'])\/src\//g, '$1=$2/client/src/');
306
+ }
307
+ /**
308
+ * 旧根 index.html → 新 client/index.html。语义见 AdaptHtmlEntryRule。
309
+ */
310
+ function adaptHtmlEntry(opts) {
311
+ const { userHtml, templateHtml, legacyEntry } = opts;
312
+ if (userHtml.includes('{{')) {
313
+ return {
314
+ ok: false,
315
+ conflict: 'contains "{{" which the HBS view engine would evaluate; adapt manually',
316
+ };
317
+ }
318
+ const head = HEAD_RE.exec(userHtml);
319
+ const body = BODY_RE.exec(userHtml);
320
+ if (head === null || body === null) {
321
+ return { ok: false, conflict: 'unrecognized HTML structure (missing <head> or <body>)' };
322
+ }
323
+ const tplHead = HEAD_RE.exec(templateHtml);
324
+ const tplBody = BODY_RE.exec(templateHtml);
325
+ if (tplHead === null || tplBody === null) {
326
+ return { ok: false, conflict: 'template HTML missing <head> or <body>' };
327
+ }
328
+ const headTokens = tokenize(head[2], ELEMENT_TOKEN_RE);
329
+ const bodyTokens = tokenize(body[2], BODY_TOKEN_RE);
330
+ if (headTokens === undefined) {
331
+ return { ok: false, conflict: 'unrecognized content in <head>; adapt manually' };
332
+ }
333
+ if (bodyTokens === undefined) {
334
+ return { ok: false, conflict: 'unrecognized content in <body>; adapt manually' };
335
+ }
336
+ let userTitle;
337
+ const extraHead = [];
338
+ for (const token of headTokens) {
339
+ if (/^<title\b/i.test(token)) {
340
+ userTitle = token;
341
+ continue;
342
+ }
343
+ if (isPlatformHeadToken(token, legacyEntry))
344
+ continue;
345
+ extraHead.push(rewriteSrcRefs(token));
346
+ }
347
+ const extraBody = bodyTokens
348
+ .filter((t) => !isPlatformBodyToken(t, legacyEntry))
349
+ .map((t) => rewriteSrcRefs(t));
350
+ let headInner = tplHead[2];
351
+ if (userTitle !== undefined) {
352
+ const title = userTitle;
353
+ headInner = /<title\b[^>]*>[\s\S]*?<\/title\s*>/i.test(headInner)
354
+ ? headInner.replace(/<title\b[^>]*>[\s\S]*?<\/title\s*>/i, () => title)
355
+ : `${headInner}\n ${title}`;
356
+ }
357
+ if (extraHead.length > 0) {
358
+ headInner = `${headInner.replace(/\s*$/, '')}\n ${extraHead.join('\n ')}\n`;
359
+ }
360
+ let bodyInner = tplBody[2];
361
+ if (extraBody.length > 0) {
362
+ bodyInner = `${bodyInner.replace(/\s*$/, '')}\n ${extraBody.join('\n ')}\n`;
363
+ }
364
+ // 替换一律用函数形式,避免用户内容里的 `$&` 等被当成替换模式
365
+ let text = templateHtml
366
+ .replace(tplHead[0], () => `<head${group(tplHead, 1)}>${headInner}</head>`)
367
+ .replace(tplBody[0], () => `<body${group(body, 1) || group(tplBody, 1)}>${bodyInner}</body>`);
368
+ const userHtmlOpen = HTML_OPEN_RE.exec(userHtml);
369
+ if (userHtmlOpen?.[1] !== undefined) {
370
+ text = text.replace(HTML_OPEN_RE, () => userHtmlOpen[0]);
371
+ }
372
+ return {
373
+ ok: true,
374
+ text,
375
+ changed: true,
376
+ detail: `kept ${(extraHead.length + extraBody.length + (userTitle ? 1 : 0)).toString()} custom element(s)`,
377
+ };
378
+ }
379
+ // ---------------------------------------------------------------------------
380
+ // Nest 根模块
381
+ // ---------------------------------------------------------------------------
382
+ /** 从 openIndex(指向 `[`)起做括号匹配,跳过字符串与注释;返回匹配的 `]` 下标 */
383
+ function findClosingBracket(text, openIndex) {
384
+ let depth = 0;
385
+ for (let i = openIndex; i < text.length; i++) {
386
+ const ch = text[i];
387
+ if (ch === '/' && text[i + 1] === '/') {
388
+ const nl = text.indexOf('\n', i);
389
+ if (nl < 0)
390
+ return -1;
391
+ i = nl;
392
+ continue;
393
+ }
394
+ if (ch === '/' && text[i + 1] === '*') {
395
+ const end = text.indexOf('*/', i + 2);
396
+ if (end < 0)
397
+ return -1;
398
+ i = end + 1;
399
+ continue;
400
+ }
401
+ if (ch === '"' || ch === "'" || ch === '`') {
402
+ let j = i + 1;
403
+ while (j < text.length && text[j] !== ch) {
404
+ if (text[j] === '\\')
405
+ j++;
406
+ j++;
407
+ }
408
+ i = j;
409
+ continue;
410
+ }
411
+ if (ch === '[' || ch === '(' || ch === '{')
412
+ depth++;
413
+ if (ch === ']' || ch === ')' || ch === '}') {
414
+ depth--;
415
+ if (depth === 0)
416
+ return ch === ']' ? i : -1;
417
+ }
418
+ }
419
+ return -1;
420
+ }
421
+ /** 把注释替换成等长空格(保持下标不变),用于定位最后一个有效字符 */
422
+ function blankComments(fragment) {
423
+ return fragment
424
+ .replace(/\/\*[\s\S]*?\*\//g, (m) => ' '.repeat(m.length))
425
+ .replace(/\/\/[^\n]*/g, (m) => ' '.repeat(m.length));
426
+ }
427
+ /** 返回 fragment 中最后一个非注释、非空白字符的下标;没有则 -1 */
428
+ function lastMeaningfulIndex(fragment) {
429
+ const blanked = blankComments(fragment);
430
+ for (let i = blanked.length - 1; i >= 0; i--) {
431
+ if (!/\s/.test(blanked[i]))
432
+ return i;
433
+ }
434
+ return -1;
435
+ }
436
+ function insertImport(text, statement) {
437
+ const importRe = /^import[\s\S]*?from\s+['"][^'"]+['"];?[ \t]*$/gm;
438
+ let lastEnd = -1;
439
+ for (const m of text.matchAll(importRe)) {
440
+ lastEnd = m.index + m[0].length;
441
+ }
442
+ if (lastEnd < 0)
443
+ return `${statement}\n${text}`;
444
+ return `${text.slice(0, lastEnd)}\n${statement}${text.slice(lastEnd)}`;
445
+ }
446
+ /**
447
+ * 保证 Nest 根模块注册 PlatformModule.forRoot()(首位)与 ViewModule(末位)。
448
+ * includeView=false 时不处理 ViewModule(项目里没有 view 模块可引用)。
449
+ */
450
+ function adaptNestAppModule(text, opts) {
451
+ const needPlatform = !/\bPlatformModule\b/.test(text);
452
+ const needView = opts.includeView && !/\bViewModule\b/.test(text);
453
+ if (!needPlatform && !needView)
454
+ return { ok: true, text, changed: false };
455
+ const moduleIdx = text.search(/@Module\s*\(\s*\{/);
456
+ if (moduleIdx < 0) {
457
+ return { ok: false, conflict: '@Module({...}) decorator not found; adapt manually' };
458
+ }
459
+ const importsMatch = /\bimports\s*:\s*\[/.exec(text.slice(moduleIdx));
460
+ if (importsMatch === null) {
461
+ return { ok: false, conflict: '@Module imports array not found; adapt manually' };
462
+ }
463
+ const openIdx = moduleIdx + importsMatch.index + importsMatch[0].length - 1;
464
+ const closeIdx = findClosingBracket(text, openIdx);
465
+ if (closeIdx < 0) {
466
+ return { ok: false, conflict: '@Module imports array is not parsable; adapt manually' };
467
+ }
468
+ let next = text;
469
+ const added = [];
470
+ // 先改尾部(ViewModule),再改头部(PlatformModule),避免下标漂移
471
+ if (needView) {
472
+ const inner = next.slice(openIdx + 1, closeIdx);
473
+ const lastIdx = lastMeaningfulIndex(inner);
474
+ let closeAt = closeIdx;
475
+ if (lastIdx >= 0 && inner[lastIdx] !== ',') {
476
+ // 末元素没有尾逗号:逗号紧跟在末元素后面(不能落进其后的行注释里)
477
+ const at = openIdx + 1 + lastIdx + 1;
478
+ next = `${next.slice(0, at)},${next.slice(at)}`;
479
+ closeAt++;
480
+ }
481
+ // 新元素以换行开头,插在末尾注释之后也不会被注释吞掉
482
+ const insertAt = openIdx + 1 + next.slice(openIdx + 1, closeAt).trimEnd().length;
483
+ next = `${next.slice(0, insertAt)}\n ViewModule,${next.slice(insertAt)}`;
484
+ added.push('ViewModule');
485
+ }
486
+ if (needPlatform) {
487
+ next = `${next.slice(0, openIdx + 1)}\n PlatformModule.forRoot(),${next.slice(openIdx + 1)}`;
488
+ added.push('PlatformModule');
489
+ }
490
+ if (needView) {
491
+ next = insertImport(next, "import { ViewModule } from './modules/view/view.module';");
492
+ }
493
+ if (needPlatform) {
494
+ next = insertImport(next, "import { PlatformModule } from '@lark-apaas/fullstack-nestjs-core';");
495
+ }
496
+ return { ok: true, text: next, changed: true, detail: `registered ${added.join(', ')}` };
497
+ }
498
+ /** 语义见 EnsureScriptsRule */
499
+ function ensureScripts(current, specs) {
500
+ const scripts = { ...current };
501
+ const changed = [];
502
+ const conflicts = [];
503
+ for (const spec of specs) {
504
+ const has = Object.prototype.hasOwnProperty.call(scripts, spec.name);
505
+ if (!has) {
506
+ scripts[spec.name] = spec.command;
507
+ changed.push(spec.name);
508
+ continue;
509
+ }
510
+ const value = scripts[spec.name];
511
+ if (value === spec.command)
512
+ continue;
513
+ const isLegacy = (spec.legacy ?? []).includes(value) ||
514
+ (spec.legacyPrefixes ?? []).some((p) => value.startsWith(p));
515
+ if (isLegacy) {
516
+ scripts[spec.name] = spec.command;
517
+ changed.push(spec.name);
518
+ continue;
519
+ }
520
+ conflicts.push({ name: spec.name, current: value, expected: spec.command });
521
+ }
522
+ return { scripts, changed, conflicts };
523
+ }
524
+ // ---------------------------------------------------------------------------
525
+ // 已知默认配置识别
526
+ // ---------------------------------------------------------------------------
527
+ // 字符串字面量优先匹配,其中的 `/*`、`//`(如 glob '**/x/**'、URL)不当注释
528
+ const SOURCE_TOKEN_RE = /("(?:\\.|[^"\\\n])*"|'(?:\\.|[^'\\\n])*'|`(?:\\.|[^`\\])*`)|\/\*[\s\S]*?\*\/|\/\/[^\n]*/g;
529
+ /** 去注释、空白与分号,引号统一为双引号 —— 用于和已知平台默认配置做结构比对 */
530
+ function normalizeConfigSource(text) {
531
+ return text
532
+ .replace(SOURCE_TOKEN_RE, (match, literal) => literal ?? '')
533
+ .replace(/'/g, '"')
534
+ .replace(/[\s;]/g, '');
535
+ }
536
+ function matchesKnownDefault(text, patterns) {
537
+ const normalized = normalizeConfigSource(text);
538
+ return patterns.some((p) => new RegExp(`^(?:${p})$`).test(normalized));
539
+ }
540
+ // ---------------------------------------------------------------------------
541
+ // ESLint 忽略项迁移
542
+ // ---------------------------------------------------------------------------
543
+ const GLOBAL_IGNORES_RE = /\bglobalIgnores\s*\(\s*\[([\s\S]*?)\]/;
544
+ const IGNORES_ARRAY_RE = /(\bignores\s*:\s*\[)([\s\S]*?)(\])/;
545
+ const STRING_LITERAL_RE = /(['"`])((?:(?!\1)[^\\\n]|\\.)*)\1/g;
546
+ function quote(value) {
547
+ return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
548
+ }
549
+ function stringLiterals(text) {
550
+ return [...text.matchAll(STRING_LITERAL_RE)].map((m) => m[2]);
551
+ }
552
+ /** 旧布局下的根相对路径改写到新布局:src → client/src,public → client/public;glob 前缀 ! 保留 */
553
+ function rewriteLegacyLayoutGlob(value) {
554
+ const negated = value.startsWith('!') ? '!' : '';
555
+ const body = negated ? value.slice(1) : value;
556
+ const rewritten = rewriteSrcPath(body).replace(/^(\.\/)?public(\/|$)/, (_m, dot, sep) => `${dot ?? ''}client/public${sep}`);
557
+ return negated + rewritten;
558
+ }
559
+ /**
560
+ * 把旧 flat config(eslint.config.mjs)里 `globalIgnores([...])` 的条目带到新配置第一个
561
+ * `ignores: [...]` 数组:路径按新布局改写,已有的不重复加。
562
+ *
563
+ * 背景:vite-react 模板用 globalIgnores 排除了 `**\/components/ui/**`,迁移删掉旧配置后新配置
564
+ * 不排除 shadcn 组件,组件里引用新版 react-hooks 规则的 eslint-disable 注释报"规则不存在"。
565
+ *
566
+ * 旧配置没有 globalIgnores → 无需迁移(changed=false);新配置找不到 ignores 数组 → 冲突。
567
+ */
568
+ function carryEslintIgnores(opts) {
569
+ const legacy = GLOBAL_IGNORES_RE.exec(opts.legacyText);
570
+ if (legacy === null) {
571
+ return { ok: true, text: opts.targetText, changed: false, detail: 'no legacy globalIgnores' };
572
+ }
573
+ const wanted = stringLiterals(legacy[1]).map(rewriteLegacyLayoutGlob);
574
+ const target = IGNORES_ARRAY_RE.exec(opts.targetText);
575
+ if (target === null) {
576
+ return { ok: false, conflict: 'no `ignores: [...]` array to carry legacy ignores into' };
577
+ }
578
+ const existing = new Set(stringLiterals(target[2]));
579
+ const missing = [...new Set(wanted)].filter((p) => !existing.has(p));
580
+ if (missing.length === 0) {
581
+ return { ok: true, text: opts.targetText, changed: false, detail: 'ignores already present' };
582
+ }
583
+ const body = target[2];
584
+ const itemIndent = /\n([ \t]+)\S/.exec(body)?.[1];
585
+ let nextBody;
586
+ if (itemIndent !== undefined) {
587
+ // 多行数组:沿用条目缩进逐行追加,保留原有收尾(换行 + 闭合缩进)
588
+ const closing = /\n[ \t]*$/.exec(body)?.[0] ?? '\n';
589
+ const items = body.slice(0, body.length - closing.length).replace(/,?\s*$/, ',');
590
+ nextBody = items + missing.map((p) => `\n${itemIndent}${quote(p)},`).join('') + closing;
591
+ }
592
+ else {
593
+ const items = body.trim().replace(/,$/, '');
594
+ const added = missing.map(quote).join(', ');
595
+ nextBody = items === '' ? added : `${items}, ${added}`;
596
+ }
597
+ const start = target.index + target[1].length;
598
+ const text = opts.targetText.slice(0, start) + nextBody + opts.targetText.slice(start + body.length);
599
+ return { ok: true, text, changed: true, detail: `carried ${missing.join(', ')}` };
600
+ }
601
+ /** relax-eslint-rules 追加段的标识注释 */
602
+ exports.RELAX_ESLINT_MARKER = 'miaoda-migrate: relax rules added by the vite-react → fullstack migration';
603
+ const CONFIG_CALL_END_RE = /\n\);?\s*$/;
604
+ /**
605
+ * 在 `module.exports = tseslint.config(...)` 收尾前追加一段覆盖:files 范围内 rules 降为 warn。
606
+ * 已有 marker → 不变;找不到 `tseslint.config(` 或收尾的 `)` → 冲突原因(调用方记 kept,不阻断)。
607
+ */
608
+ function relaxEslintRules(opts) {
609
+ if (opts.text.includes(exports.RELAX_ESLINT_MARKER)) {
610
+ return { ok: true, text: opts.text, changed: false, detail: 'already relaxed' };
611
+ }
612
+ const end = CONFIG_CALL_END_RE.exec(opts.text);
613
+ if (!/\btseslint\.config\(/.test(opts.text) || end === null) {
614
+ return { ok: false, conflict: 'not a `tseslint.config(...)` config; rules not relaxed' };
615
+ }
616
+ const before = opts.text.slice(0, end.index).replace(/,?\s*$/, ',');
617
+ const block = [
618
+ ` // ${exports.RELAX_ESLINT_MARKER}`,
619
+ ' // 新 preset 新增 / 加严的平台规范先降为 warn(lint 以 --quiet 运行不阻断提交),按提示逐步修复后可删除本段',
620
+ ' {',
621
+ ` files: [${opts.files.map(quote).join(', ')}],`,
622
+ ' rules: {',
623
+ ...opts.rules.map((r) => ` ${quote(r)}: 'warn',`),
624
+ ' },',
625
+ ' },',
626
+ ].join('\n');
627
+ const text = `${before}\n${block}${opts.text.slice(end.index)}`;
628
+ return {
629
+ ok: true,
630
+ text,
631
+ changed: true,
632
+ detail: `${opts.rules.length.toString()} rule(s) relaxed`,
633
+ };
634
+ }