@simplysm/sd-cli 14.0.45 → 14.0.46
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/deps/server-externals/server-production-files.d.ts.map +1 -1
- package/dist/deps/server-externals/server-production-files.js +5 -3
- package/dist/deps/server-externals/server-production-files.js.map +1 -1
- package/dist/esbuild/esbuild-config.d.ts.map +1 -1
- package/dist/esbuild/esbuild-config.js +2 -5
- package/dist/esbuild/esbuild-config.js.map +1 -1
- package/dist/esbuild/esbuild-worker-plugin.d.ts +22 -0
- package/dist/esbuild/esbuild-worker-plugin.d.ts.map +1 -1
- package/dist/esbuild/esbuild-worker-plugin.js +161 -39
- package/dist/esbuild/esbuild-worker-plugin.js.map +1 -1
- package/dist/utils/output-path-rewriter.d.ts +5 -2
- package/dist/utils/output-path-rewriter.d.ts.map +1 -1
- package/dist/utils/output-path-rewriter.js +60 -12
- package/dist/utils/output-path-rewriter.js.map +1 -1
- package/package.json +6 -4
- package/src/deps/server-externals/server-production-files.ts +5 -3
- package/src/esbuild/esbuild-config.ts +2 -7
- package/src/esbuild/esbuild-worker-plugin.ts +208 -55
- package/src/utils/output-path-rewriter.ts +65 -17
- package/tests/deps/server-externals/mise-toml-parse-intent.verify.md +16 -0
- package/tests/esbuild/esbuild-worker-plugin.spec.ts +310 -1
|
@@ -2,6 +2,7 @@ import type { ServerBuildInfo } from "../../workers/server-build.worker";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
import fs from "fs";
|
|
4
4
|
import YAML from "yaml";
|
|
5
|
+
import TOML from "smol-toml";
|
|
5
6
|
import { cpx } from "@simplysm/core-node";
|
|
6
7
|
import { consola } from "consola";
|
|
7
8
|
import { collectAllDependencyExternals } from "../../esbuild/esbuild-config";
|
|
@@ -110,9 +111,10 @@ export function generateProductionFiles(
|
|
|
110
111
|
let nodeVersion = "20";
|
|
111
112
|
if (fs.existsSync(rootMiseTomlPath)) {
|
|
112
113
|
const miseContent = fs.readFileSync(rootMiseTomlPath, "utf-8");
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
// mise.toml은 저장소에서 관리되는 설정 파일이므로, 파싱 실패 시 폴백하지 않고 예외를 전파하여 설정 오류를 즉시 드러낸다.
|
|
115
|
+
const miseConfig = TOML.parse(miseContent) as { tools?: { node?: string } };
|
|
116
|
+
if (miseConfig.tools?.node != null) {
|
|
117
|
+
nodeVersion = miseConfig.tools.node;
|
|
116
118
|
}
|
|
117
119
|
}
|
|
118
120
|
fs.writeFileSync(path.join(distDir, "mise.toml"), `[tools]\nnode = "${nodeVersion}"\n`);
|
|
@@ -4,6 +4,7 @@ import fs from "fs/promises";
|
|
|
4
4
|
import { createRequire } from "module";
|
|
5
5
|
import type esbuild from "esbuild";
|
|
6
6
|
import { consola } from "consola";
|
|
7
|
+
import { addJsExtensionToImports } from "../utils/output-path-rewriter";
|
|
7
8
|
|
|
8
9
|
const logger = consola.withTag("sd:cli:esbuild-config");
|
|
9
10
|
|
|
@@ -19,13 +20,7 @@ export async function writeChangedOutputFiles(outputFiles: esbuild.OutputFile[])
|
|
|
19
20
|
await Promise.all(
|
|
20
21
|
outputFiles.map(async (file) => {
|
|
21
22
|
const finalText = file.path.endsWith(".js")
|
|
22
|
-
? file.text
|
|
23
|
-
/((?:from|import)\s*["'])(\.\.?\/[^"']*?)(["'])/g,
|
|
24
|
-
(_match, prefix: string, importPath: string, suffix: string) => {
|
|
25
|
-
if (/\.(js|mjs|cjs|json|css|wasm|node)$/i.test(importPath)) return _match;
|
|
26
|
-
return `${prefix}${importPath}.js${suffix}`;
|
|
27
|
-
},
|
|
28
|
-
)
|
|
23
|
+
? addJsExtensionToImports(file.text)
|
|
29
24
|
: file.text;
|
|
30
25
|
|
|
31
26
|
try {
|
|
@@ -1,27 +1,144 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import type esbuild from "esbuild";
|
|
4
|
+
import * as acorn from "acorn";
|
|
5
|
+
import * as walk from "acorn-walk";
|
|
6
|
+
|
|
7
|
+
//#region AST 기반 Worker 패턴 탐지
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* 캡처 그룹:
|
|
9
|
-
* - Group 1: `Worker` 또는 `SharedWorker`
|
|
10
|
-
* - Group 2: URL 경로 (예: `"./worker.ts"`)
|
|
11
|
-
* - Group 3: 옵션 객체 (예: `{ type: "module" }`) — 없으면 undefined
|
|
10
|
+
* AST에서 탐지된 Worker 패턴 하나를 나타낸다.
|
|
12
11
|
*/
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
export interface WorkerMatch {
|
|
13
|
+
type: "browser" | "node";
|
|
14
|
+
/** 전체 표현식의 start/end (치환 범위) */
|
|
15
|
+
start: number;
|
|
16
|
+
end: number;
|
|
17
|
+
/** Worker/SharedWorker 이름 (browser만) */
|
|
18
|
+
workerType?: string;
|
|
19
|
+
/** URL 경로 문자열 값 */
|
|
20
|
+
urlPath: string;
|
|
21
|
+
/** 옵션 객체의 원본 소스 텍스트 (browser만, 없으면 undefined) */
|
|
22
|
+
existingOpts?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* MemberExpression이 import.meta.url인지 확인한다.
|
|
27
|
+
*/
|
|
28
|
+
function isImportMetaUrl(node: any): boolean {
|
|
29
|
+
return (
|
|
30
|
+
node.type === "MemberExpression" &&
|
|
31
|
+
node.object.type === "MetaProperty" &&
|
|
32
|
+
node.object.meta.name === "import" &&
|
|
33
|
+
node.object.property.name === "meta" &&
|
|
34
|
+
node.property.type === "Identifier" &&
|
|
35
|
+
node.property.name === "url"
|
|
36
|
+
);
|
|
37
|
+
}
|
|
15
38
|
|
|
16
39
|
/**
|
|
17
|
-
*
|
|
18
|
-
* 상대 경로(./ 또는 ../)만 감지 — 절대 모듈 경로("some-package")는 무시.
|
|
40
|
+
* acorn AST를 사용하여 소스 코드에서 Worker/SharedWorker 및 import.meta.resolve 패턴을 탐지한다.
|
|
19
41
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
42
|
+
* 정규식과 달리 주석, 문자열 리터럴 내부의 패턴을 오탐하지 않는다.
|
|
43
|
+
* 파싱 실패 시 빈 배열을 반환한다.
|
|
22
44
|
*/
|
|
23
|
-
|
|
24
|
-
|
|
45
|
+
export function findWorkerPatterns(content: string): WorkerMatch[] {
|
|
46
|
+
let ast: acorn.Node;
|
|
47
|
+
try {
|
|
48
|
+
ast = acorn.parse(content, {
|
|
49
|
+
ecmaVersion: "latest",
|
|
50
|
+
sourceType: "module",
|
|
51
|
+
});
|
|
52
|
+
} catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const matches: WorkerMatch[] = [];
|
|
57
|
+
|
|
58
|
+
walk.simple(ast, {
|
|
59
|
+
NewExpression(node: any) {
|
|
60
|
+
// new Worker(new URL("path", import.meta.url), opts?)
|
|
61
|
+
// new SharedWorker(new URL("path", import.meta.url), opts?)
|
|
62
|
+
if (
|
|
63
|
+
node.callee.type !== "Identifier" ||
|
|
64
|
+
(node.callee.name !== "Worker" && node.callee.name !== "SharedWorker")
|
|
65
|
+
) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const args = node.arguments;
|
|
70
|
+
if (args.length < 1) return;
|
|
71
|
+
|
|
72
|
+
const urlArg = args[0];
|
|
73
|
+
if (
|
|
74
|
+
urlArg.type !== "NewExpression" ||
|
|
75
|
+
urlArg.callee.type !== "Identifier" ||
|
|
76
|
+
urlArg.callee.name !== "URL"
|
|
77
|
+
) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const urlArgs = urlArg.arguments;
|
|
82
|
+
if (urlArgs.length < 2) return;
|
|
83
|
+
|
|
84
|
+
// 첫 번째 인자: 문자열 리터럴 (경로)
|
|
85
|
+
if (urlArgs[0].type !== "Literal" || typeof urlArgs[0].value !== "string") return;
|
|
86
|
+
|
|
87
|
+
// 두 번째 인자: import.meta.url
|
|
88
|
+
if (!isImportMetaUrl(urlArgs[1])) return;
|
|
89
|
+
|
|
90
|
+
const match: WorkerMatch = {
|
|
91
|
+
type: "browser",
|
|
92
|
+
start: node.start,
|
|
93
|
+
end: node.end,
|
|
94
|
+
workerType: node.callee.name,
|
|
95
|
+
urlPath: urlArgs[0].value,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// 옵션 객체 (두 번째 인자)
|
|
99
|
+
if (args.length >= 2) {
|
|
100
|
+
match.existingOpts = content.slice(args[1].start, args[1].end);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
matches.push(match);
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
CallExpression(node: any) {
|
|
107
|
+
// import.meta.resolve("./relative-path")
|
|
108
|
+
const callee = node.callee;
|
|
109
|
+
if (
|
|
110
|
+
callee.type !== "MemberExpression" ||
|
|
111
|
+
callee.object.type !== "MetaProperty" ||
|
|
112
|
+
callee.object.meta.name !== "import" ||
|
|
113
|
+
callee.object.property.name !== "meta" ||
|
|
114
|
+
callee.property.type !== "Identifier" ||
|
|
115
|
+
callee.property.name !== "resolve"
|
|
116
|
+
) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const args = node.arguments;
|
|
121
|
+
if (args.length < 1) return;
|
|
122
|
+
|
|
123
|
+
if (args[0].type !== "Literal" || typeof args[0].value !== "string") return;
|
|
124
|
+
|
|
125
|
+
const urlPath = args[0].value as string;
|
|
126
|
+
// 상대 경로만 처리
|
|
127
|
+
if (!urlPath.startsWith("./") && !urlPath.startsWith("../")) return;
|
|
128
|
+
|
|
129
|
+
matches.push({
|
|
130
|
+
type: "node",
|
|
131
|
+
start: node.start,
|
|
132
|
+
end: node.end,
|
|
133
|
+
urlPath,
|
|
134
|
+
});
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return matches.sort((a, b) => a.start - b.start);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
//#endregion
|
|
25
142
|
|
|
26
143
|
/**
|
|
27
144
|
* Worker 번들 빌드 결과를 포함하는 transform 결과.
|
|
@@ -94,20 +211,46 @@ export function transformWorkerPatterns(
|
|
|
94
211
|
filePath: string,
|
|
95
212
|
build: esbuild.PluginBuild,
|
|
96
213
|
): TransformWorkerResult | undefined {
|
|
97
|
-
// 빠른 사전 필터
|
|
98
|
-
|
|
99
|
-
if (hasBrowserWorker) WORKER_PATTERN.lastIndex = 0;
|
|
100
|
-
|
|
101
|
-
const hasNodeWorker =
|
|
102
|
-
content.includes("import.meta.resolve") && NODE_WORKER_PATTERN.test(content);
|
|
103
|
-
if (hasNodeWorker) NODE_WORKER_PATTERN.lastIndex = 0;
|
|
104
|
-
|
|
105
|
-
if (!hasBrowserWorker && !hasNodeWorker) {
|
|
214
|
+
// 빠른 사전 필터 — AST 파싱 전에 키워드 존재 여부로 걸러냄 (원본 TS content 기준)
|
|
215
|
+
if (!content.includes("Worker") && !content.includes("import.meta.resolve")) {
|
|
106
216
|
return undefined;
|
|
107
217
|
}
|
|
108
218
|
|
|
109
219
|
const errors: esbuild.PartialMessage[] = [];
|
|
110
220
|
const warnings: esbuild.PartialMessage[] = [];
|
|
221
|
+
|
|
222
|
+
// TS(.ts/.cts/.mts)는 JS로 변환한 후 AST 파싱. acorn은 TS 구문을 처리하지 못하므로
|
|
223
|
+
// import type, 타입 어노테이션 등이 있으면 파싱 실패로 Worker 패턴이 조용히 누락된다.
|
|
224
|
+
let effectiveContent = content;
|
|
225
|
+
if (/\.[cm]?ts$/.test(filePath)) {
|
|
226
|
+
try {
|
|
227
|
+
const transformed = build.esbuild.transformSync(content, {
|
|
228
|
+
loader: "ts",
|
|
229
|
+
sourcemap: false,
|
|
230
|
+
});
|
|
231
|
+
effectiveContent = transformed.code;
|
|
232
|
+
warnings.push(...transformed.warnings);
|
|
233
|
+
} catch (e) {
|
|
234
|
+
const failure = e as {
|
|
235
|
+
errors?: esbuild.PartialMessage[];
|
|
236
|
+
warnings?: esbuild.PartialMessage[];
|
|
237
|
+
};
|
|
238
|
+
return {
|
|
239
|
+
contents: content,
|
|
240
|
+
errors: failure.errors ?? [
|
|
241
|
+
{ text: `TS transform failed: ${String(e)}`, location: null },
|
|
242
|
+
],
|
|
243
|
+
warnings: failure.warnings ?? [],
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// AST 기반 패턴 탐지 (변환된 JS 기준)
|
|
249
|
+
const matches = findWorkerPatterns(effectiveContent);
|
|
250
|
+
if (matches.length === 0) {
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
|
|
111
254
|
const allOutputFiles: esbuild.OutputFile[] = [];
|
|
112
255
|
let mergedMetafile: esbuild.Metafile | undefined;
|
|
113
256
|
|
|
@@ -170,42 +313,49 @@ export function transformWorkerPatterns(
|
|
|
170
313
|
return path.relative(outdir, workerCodeFile.path).replaceAll("\\", "/");
|
|
171
314
|
}
|
|
172
315
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
316
|
+
// 정방향 chunks 패턴으로 치환 (esbuild-postcss-plugin.ts 동일 패턴)
|
|
317
|
+
const replacements: Array<{ start: number; end: number; text: string }> = [];
|
|
318
|
+
|
|
319
|
+
for (const match of matches) {
|
|
320
|
+
if (match.type === "browser") {
|
|
321
|
+
const fullWorkerPath = path.resolve(containingDir, match.urlPath);
|
|
322
|
+
const workerCodePath = processWorkerBundle(fullWorkerPath, "browser");
|
|
323
|
+
if (workerCodePath == null) continue;
|
|
324
|
+
|
|
325
|
+
const optsStr = match.existingOpts ?? '{ type: "module" }';
|
|
326
|
+
replacements.push({
|
|
327
|
+
start: match.start,
|
|
328
|
+
end: match.end,
|
|
329
|
+
text: `new ${match.workerType}(new URL("${workerCodePath}", import.meta.url), ${optsStr})`,
|
|
330
|
+
});
|
|
331
|
+
} else {
|
|
332
|
+
const fullWorkerPath = path.resolve(containingDir, match.urlPath);
|
|
333
|
+
const workerCodePath = processWorkerBundle(
|
|
334
|
+
fullWorkerPath,
|
|
335
|
+
build.initialOptions.platform ?? "browser",
|
|
336
|
+
);
|
|
337
|
+
if (workerCodePath == null) continue;
|
|
338
|
+
|
|
339
|
+
replacements.push({
|
|
340
|
+
start: match.start,
|
|
341
|
+
end: match.end,
|
|
342
|
+
text: `new URL("${workerCodePath}", import.meta.url).href`,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
188
345
|
}
|
|
189
346
|
|
|
190
|
-
//
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
fullWorkerPath,
|
|
198
|
-
build.initialOptions.platform ?? "browser",
|
|
199
|
-
);
|
|
200
|
-
if (workerCodePath == null) return match;
|
|
201
|
-
|
|
202
|
-
return `new URL("${workerCodePath}", import.meta.url).href`;
|
|
203
|
-
},
|
|
204
|
-
);
|
|
347
|
+
// chunks 조립 (변환된 JS 기준 — 매치의 start/end는 effectiveContent 오프셋)
|
|
348
|
+
const chunks: string[] = [];
|
|
349
|
+
let cursor = 0;
|
|
350
|
+
for (const rep of replacements) {
|
|
351
|
+
chunks.push(effectiveContent.slice(cursor, rep.start));
|
|
352
|
+
chunks.push(rep.text);
|
|
353
|
+
cursor = rep.end;
|
|
205
354
|
}
|
|
355
|
+
chunks.push(effectiveContent.slice(cursor));
|
|
206
356
|
|
|
207
357
|
return {
|
|
208
|
-
contents:
|
|
358
|
+
contents: chunks.join(""),
|
|
209
359
|
errors,
|
|
210
360
|
warnings,
|
|
211
361
|
workerOutputFiles: allOutputFiles.length > 0 ? allOutputFiles : undefined,
|
|
@@ -241,9 +391,12 @@ export function createWorkerBundlePlugin(): esbuild.Plugin {
|
|
|
241
391
|
});
|
|
242
392
|
}
|
|
243
393
|
|
|
394
|
+
// TS(.ts/.cts/.mts)는 transformWorkerPatterns 내부에서 JS로 변환되어 반환되므로
|
|
395
|
+
// loader는 "js". .tsx/.jsx는 변환하지 않으므로 esbuild가 JSX를 처리하도록 "tsx".
|
|
396
|
+
const isJsx = /\.[cm]?tsx$/.test(args.path) || args.path.endsWith(".jsx");
|
|
244
397
|
return {
|
|
245
398
|
contents: result.contents,
|
|
246
|
-
loader:
|
|
399
|
+
loader: isJsx ? ("tsx" as const) : ("js" as const),
|
|
247
400
|
errors: result.errors.length > 0 ? result.errors : undefined,
|
|
248
401
|
warnings: result.warnings.length > 0 ? result.warnings : undefined,
|
|
249
402
|
};
|
|
@@ -1,39 +1,87 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import { pathx } from "@simplysm/core-node";
|
|
3
|
+
import { init, parse } from "es-module-lexer";
|
|
4
|
+
|
|
5
|
+
await init;
|
|
6
|
+
|
|
7
|
+
const KNOWN_JS_EXTENSIONS = /\.(js|mjs|cjs|json|css|scss|wasm|node)$/i;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* es-module-lexer의 import에서 specifier 내용의 시작/끝 위치를 반환한다.
|
|
11
|
+
*
|
|
12
|
+
* static import (d === -1): s..e가 따옴표 없는 specifier 내용
|
|
13
|
+
* dynamic import (d >= 0): s..e가 따옴표를 포함한 문자열 리터럴
|
|
14
|
+
*/
|
|
15
|
+
function getSpecifierRange(imp: { s: number; e: number; d: number }): [number, number] {
|
|
16
|
+
if (imp.d >= 0) {
|
|
17
|
+
return [imp.s + 1, imp.e - 1];
|
|
18
|
+
}
|
|
19
|
+
return [imp.s, imp.e];
|
|
20
|
+
}
|
|
3
21
|
|
|
4
22
|
/**
|
|
5
23
|
* ESM 출력에서 확장자가 없는 상대 import/export 경로에 .js 확장자를 추가한다.
|
|
6
24
|
*
|
|
7
|
-
*
|
|
25
|
+
* es-module-lexer를 사용하여 import/export specifier 위치를 정확히 파악한다.
|
|
26
|
+
* 주석, 문자열 리터럴 내부의 패턴은 무시된다.
|
|
27
|
+
*
|
|
28
|
+
* 매칭: from "./foo", import("./bar"), from "../baz", export { x } from "./qux"
|
|
8
29
|
* 스킵: bare 지정자("lodash"), 이미 알려진 확장자로 끝나는 경로 (.js, .json, .css 등)
|
|
9
30
|
*/
|
|
10
31
|
export function addJsExtensionToImports(text: string): string {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
32
|
+
const [imports] = parse(text);
|
|
33
|
+
if (imports.length === 0) return text;
|
|
34
|
+
|
|
35
|
+
// 역순으로 치환하여 위치 밀림 방지
|
|
36
|
+
const sorted = [...imports].sort((a, b) => b.s - a.s);
|
|
37
|
+
let result = text;
|
|
38
|
+
|
|
39
|
+
for (const imp of sorted) {
|
|
40
|
+
const specifier = imp.n;
|
|
41
|
+
if (specifier == null) continue;
|
|
42
|
+
if (!specifier.startsWith("./") && !specifier.startsWith("../")) continue;
|
|
43
|
+
if (KNOWN_JS_EXTENSIONS.test(specifier)) continue;
|
|
44
|
+
|
|
45
|
+
const [start, end] = getSpecifierRange(imp);
|
|
46
|
+
result = result.slice(0, start) + specifier + ".js" + result.slice(end);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return result;
|
|
18
50
|
}
|
|
19
51
|
|
|
20
52
|
/**
|
|
21
53
|
* emit된 JS 텍스트에서 상대 .scss import를 .css로 변환한다.
|
|
22
54
|
* 변환된 텍스트와 원본 .scss import 경로 목록을 반환한다.
|
|
23
55
|
*
|
|
24
|
-
*
|
|
56
|
+
* es-module-lexer를 사용하여 import specifier 위치를 정확히 파악한다.
|
|
25
57
|
* 상대 import(./ 또는 ../ 시작)만 처리한다.
|
|
26
58
|
*/
|
|
27
59
|
export function rewriteScssImports(text: string): { text: string; scssImports: string[] } {
|
|
60
|
+
const [imports] = parse(text);
|
|
61
|
+
if (imports.length === 0) return { text, scssImports: [] };
|
|
62
|
+
|
|
28
63
|
const scssImports: string[] = [];
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
64
|
+
|
|
65
|
+
// 역순으로 치환하여 위치 밀림 방지
|
|
66
|
+
const sorted = [...imports].sort((a, b) => b.s - a.s);
|
|
67
|
+
let result = text;
|
|
68
|
+
|
|
69
|
+
for (const imp of sorted) {
|
|
70
|
+
const specifier = imp.n;
|
|
71
|
+
if (specifier == null) continue;
|
|
72
|
+
if (!specifier.startsWith("./") && !specifier.startsWith("../")) continue;
|
|
73
|
+
if (!specifier.endsWith(".scss")) continue;
|
|
74
|
+
|
|
75
|
+
scssImports.push(specifier);
|
|
76
|
+
const newSpec = specifier.slice(0, -5) + ".css";
|
|
77
|
+
const [start, end] = getSpecifierRange(imp);
|
|
78
|
+
result = result.slice(0, start) + newSpec + result.slice(end);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 역순으로 수집되었으므로 원래 순서로 복원
|
|
82
|
+
scssImports.reverse();
|
|
83
|
+
|
|
84
|
+
return { text: result, scssImports };
|
|
37
85
|
}
|
|
38
86
|
|
|
39
87
|
/**
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# mise.toml 파싱 실패 시 명시적 크래시 의도 — LLM 검증
|
|
2
|
+
|
|
3
|
+
대상: `packages/sd-cli/src/deps/server-externals/server-production-files.ts` `generateProductionFiles()` 내 `TOML.parse(miseContent)` 호출부
|
|
4
|
+
|
|
5
|
+
근거: `.tasks/260417132441_review-regex-to-parser/1.3-toml-parse-intent-comment.md`
|
|
6
|
+
|
|
7
|
+
## 검증 항목
|
|
8
|
+
|
|
9
|
+
- [x] **WHY 주석 존재**: `server-production-files.ts:114`에 `// mise.toml은 저장소에서 관리되는 설정 파일이므로, 파싱 실패 시 폴백하지 않고 예외를 전파하여 설정 오류를 즉시 드러낸다.` 주석이 `TOML.parse(miseContent)` 호출(`:115`) 직전에 존재함을 확인
|
|
10
|
+
- [x] **명시적 크래시 의도 서술**: 주석이 "저장소에서 관리되는 설정 파일", "폴백하지 않고", "예외를 전파", "설정 오류를 즉시 드러낸다" 표현을 모두 포함하여 wbs에 명시된 WHY를 서술
|
|
11
|
+
- [x] **WHAT 서술 지양**: "TOML을 파싱한다" 같은 코드 재서술이 아니라, 폴백을 의도적으로 하지 않는 이유(설정 오류 즉시 노출)를 설명
|
|
12
|
+
- [x] **try-catch 미추가**: `:109-121` 블록 전체에 try 키워드 없음을 확인 — 파싱 예외는 상위로 전파됨
|
|
13
|
+
- [x] **주석 스타일**: `//` 한 줄 주석 (JSDoc 아님)
|
|
14
|
+
- [x] **주석 언어**: 한국어
|
|
15
|
+
- [x] **기타 코드 미변경**: `git diff server-production-files.ts` 결과 라인 114의 주석 추가 1줄 외 변경 없음 (아래 명령 실행 결과 참조)
|
|
16
|
+
- [x] **기존 테스트 통과**: `pnpm exec vitest run --project node packages/sd-cli/tests/workers/server-build-worker.spec.ts` 25/25 통과, 특히 `generates dist/mise.toml when packageManager=mise` 테스트(TOML.parse 경로 실행)가 통과하여 파싱 로직 회귀 없음을 확인. 참고: 전체 sd-cli 테스트(`packages/sd-cli/tests/`)에서 1건 실패(`esbuild-worker-plugin.acc.spec.ts:143`)가 있으나 이는 Feature 1.1 영역이며 본 Feature 1.3 변경(주석 1줄 추가)과 무관
|