@simplysm/sd-cli 14.0.44 → 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 +10 -5
- package/dist/deps/server-externals/server-production-files.d.ts.map +1 -1
- package/dist/deps/server-externals/server-production-files.js +27 -29
- package/dist/deps/server-externals/server-production-files.js.map +1 -1
- package/dist/esbuild/esbuild-angular-compiler-plugin.d.ts +3 -8
- package/dist/esbuild/esbuild-angular-compiler-plugin.d.ts.map +1 -1
- package/dist/esbuild/esbuild-angular-compiler-plugin.js +57 -83
- package/dist/esbuild/esbuild-angular-compiler-plugin.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 +52 -0
- package/dist/esbuild/esbuild-worker-plugin.d.ts.map +1 -0
- package/dist/esbuild/esbuild-worker-plugin.js +319 -0
- package/dist/esbuild/esbuild-worker-plugin.js.map +1 -0
- 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/dist/workers/server-build.worker.d.ts.map +1 -1
- package/dist/workers/server-build.worker.js +6 -5
- package/dist/workers/server-build.worker.js.map +1 -1
- package/dist/workers/server-esbuild-context.d.ts.map +1 -1
- package/dist/workers/server-esbuild-context.js +3 -1
- package/dist/workers/server-esbuild-context.js.map +1 -1
- package/dist/workers/server-watch-manager.js +1 -1
- package/dist/workers/server-watch-manager.js.map +1 -1
- package/package.json +7 -4
- package/src/deps/server-externals/server-production-files.ts +31 -31
- package/src/esbuild/esbuild-angular-compiler-plugin.ts +82 -123
- package/src/esbuild/esbuild-config.ts +2 -7
- package/src/esbuild/esbuild-worker-plugin.ts +419 -0
- package/src/utils/output-path-rewriter.ts +65 -17
- package/src/workers/server-build.worker.ts +6 -5
- package/src/workers/server-esbuild-context.ts +3 -1
- package/src/workers/server-watch-manager.ts +1 -1
- package/tests/deps/server-externals/mise-toml-parse-intent.verify.md +16 -0
- package/tests/esbuild/esbuild-angular-compiler-plugin-worker.verify.md +56 -28
- package/tests/esbuild/esbuild-worker-plugin-node.verify.md +11 -0
- package/tests/esbuild/esbuild-worker-plugin.acc.spec.ts +318 -0
- package/tests/esbuild/esbuild-worker-plugin.spec.ts +606 -0
- package/tests/esbuild/esbuild-worker-plugin.verify.md +7 -0
- package/tests/esbuild/fixtures/worker-plugin/node-worker.js +2 -0
- package/tests/esbuild/fixtures/worker-plugin/shared-worker.js +6 -0
- package/tests/esbuild/fixtures/worker-plugin/worker-error.js +1 -0
- package/tests/esbuild/fixtures/worker-plugin/worker.js +3 -0
- package/tests/esbuild/fixtures/worker-plugin/worker2.js +3 -0
- package/tests/workers/server-build-worker-plugin.verify.md +9 -0
- package/tests/workers/server-build-worker.spec.ts +26 -12
- package/tests/workers/server-esbuild-context.spec.ts +13 -5
- package/tests/workers/server-watch-manager.acc.spec.ts +2 -2
- package/tests/workers/server-watch-manager.spec.ts +2 -2
- package/dist/angular/web-worker-transformer.d.ts +0 -9
- package/dist/angular/web-worker-transformer.d.ts.map +0 -1
- package/dist/angular/web-worker-transformer.js +0 -73
- package/dist/angular/web-worker-transformer.js.map +0 -1
- package/src/angular/web-worker-transformer.ts +0 -117
- package/tests/angular/web-worker-transformer.spec.ts +0 -154
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import type esbuild from "esbuild";
|
|
4
|
+
import * as acorn from "acorn";
|
|
5
|
+
import * as walk from "acorn-walk";
|
|
6
|
+
|
|
7
|
+
//#region AST 기반 Worker 패턴 탐지
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* AST에서 탐지된 Worker 패턴 하나를 나타낸다.
|
|
11
|
+
*/
|
|
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
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* acorn AST를 사용하여 소스 코드에서 Worker/SharedWorker 및 import.meta.resolve 패턴을 탐지한다.
|
|
41
|
+
*
|
|
42
|
+
* 정규식과 달리 주석, 문자열 리터럴 내부의 패턴을 오탐하지 않는다.
|
|
43
|
+
* 파싱 실패 시 빈 배열을 반환한다.
|
|
44
|
+
*/
|
|
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
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Worker 번들 빌드 결과를 포함하는 transform 결과.
|
|
145
|
+
*/
|
|
146
|
+
export interface TransformWorkerResult {
|
|
147
|
+
contents: string;
|
|
148
|
+
errors: esbuild.PartialMessage[];
|
|
149
|
+
warnings: esbuild.PartialMessage[];
|
|
150
|
+
/** write: false일 때 Worker 번들의 outputFiles (onEnd에서 병합용) */
|
|
151
|
+
workerOutputFiles?: esbuild.OutputFile[];
|
|
152
|
+
/** Worker 번들의 metafile (onEnd에서 병합용) */
|
|
153
|
+
workerMetafile?: esbuild.Metafile;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Worker 파일을 esbuild.buildSync()로 별도 ESM 번들로 빌드한다.
|
|
158
|
+
* esbuild-angular-compiler-plugin.ts의 bundleWebWorker를 기반으로 작성.
|
|
159
|
+
*/
|
|
160
|
+
function bundleWorker(
|
|
161
|
+
build: esbuild.PluginBuild,
|
|
162
|
+
workerFile: string,
|
|
163
|
+
platform: esbuild.Platform,
|
|
164
|
+
): esbuild.BuildResult {
|
|
165
|
+
const sourcemap =
|
|
166
|
+
build.initialOptions.sourcemap != null &&
|
|
167
|
+
build.initialOptions.sourcemap !== false;
|
|
168
|
+
const write = build.initialOptions.write !== false;
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
return build.esbuild.buildSync({
|
|
172
|
+
...build.initialOptions,
|
|
173
|
+
platform,
|
|
174
|
+
write,
|
|
175
|
+
bundle: true,
|
|
176
|
+
metafile: true,
|
|
177
|
+
format: "esm",
|
|
178
|
+
entryNames: "worker-[hash]",
|
|
179
|
+
entryPoints: [workerFile],
|
|
180
|
+
sourcemap,
|
|
181
|
+
// 메인 빌드에서 상속하지 않는 옵션
|
|
182
|
+
external: undefined,
|
|
183
|
+
supported: undefined,
|
|
184
|
+
plugins: undefined,
|
|
185
|
+
outbase: undefined,
|
|
186
|
+
inject: undefined,
|
|
187
|
+
});
|
|
188
|
+
} catch (error) {
|
|
189
|
+
if (
|
|
190
|
+
error != null &&
|
|
191
|
+
typeof error === "object" &&
|
|
192
|
+
"errors" in error &&
|
|
193
|
+
"warnings" in error
|
|
194
|
+
) {
|
|
195
|
+
return error as esbuild.BuildResult;
|
|
196
|
+
}
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 파일 내용에서 Worker/SharedWorker 패턴을 감지하여 Worker 파일을 번들링하고
|
|
203
|
+
* URL 경로를 번들된 파일 경로로 치환한다.
|
|
204
|
+
*
|
|
205
|
+
* Angular 플러그인 등 외부에서 직접 호출할 수 있도록 export한다 (D2).
|
|
206
|
+
*
|
|
207
|
+
* @returns 변환 결과. 패턴이 없으면 undefined.
|
|
208
|
+
*/
|
|
209
|
+
export function transformWorkerPatterns(
|
|
210
|
+
content: string,
|
|
211
|
+
filePath: string,
|
|
212
|
+
build: esbuild.PluginBuild,
|
|
213
|
+
): TransformWorkerResult | undefined {
|
|
214
|
+
// 빠른 사전 필터 — AST 파싱 전에 키워드 존재 여부로 걸러냄 (원본 TS content 기준)
|
|
215
|
+
if (!content.includes("Worker") && !content.includes("import.meta.resolve")) {
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const errors: esbuild.PartialMessage[] = [];
|
|
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
|
+
|
|
254
|
+
const allOutputFiles: esbuild.OutputFile[] = [];
|
|
255
|
+
let mergedMetafile: esbuild.Metafile | undefined;
|
|
256
|
+
|
|
257
|
+
const write = build.initialOptions.write !== false;
|
|
258
|
+
const outdir = build.initialOptions.outdir ?? "";
|
|
259
|
+
const containingDir = path.dirname(filePath);
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Worker 번들을 빌드하고, 결과에서 출력 파일 경로를 찾아 반환한다.
|
|
263
|
+
* 에러/경고/outputFiles/metafile을 외부 변수에 수집한다.
|
|
264
|
+
*/
|
|
265
|
+
function processWorkerBundle(
|
|
266
|
+
fullWorkerPath: string,
|
|
267
|
+
platform: esbuild.Platform,
|
|
268
|
+
): string | undefined {
|
|
269
|
+
const workerResult = bundleWorker(build, fullWorkerPath, platform);
|
|
270
|
+
|
|
271
|
+
warnings.push(...workerResult.warnings);
|
|
272
|
+
|
|
273
|
+
if (workerResult.errors.length > 0) {
|
|
274
|
+
errors.push(...workerResult.errors);
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (!write && workerResult.outputFiles != null) {
|
|
279
|
+
allOutputFiles.push(...workerResult.outputFiles);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (workerResult.metafile != null) {
|
|
283
|
+
if (mergedMetafile == null) {
|
|
284
|
+
mergedMetafile = { inputs: {}, outputs: {} };
|
|
285
|
+
}
|
|
286
|
+
Object.assign(mergedMetafile.inputs, workerResult.metafile.inputs);
|
|
287
|
+
Object.assign(mergedMetafile.outputs, workerResult.metafile.outputs);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const workerCodeFile =
|
|
291
|
+
workerResult.outputFiles?.find((file) =>
|
|
292
|
+
/^worker-[a-z0-9]+\.[cm]?js$/i.test(path.basename(file.path)),
|
|
293
|
+
) ??
|
|
294
|
+
(workerResult.metafile != null
|
|
295
|
+
? (() => {
|
|
296
|
+
const outputKey = Object.keys(workerResult.metafile.outputs).find(
|
|
297
|
+
(key) => /worker-[a-z0-9]+\.[cm]?js$/i.test(path.basename(key)),
|
|
298
|
+
);
|
|
299
|
+
return outputKey != null ? { path: path.resolve(outputKey) } : undefined;
|
|
300
|
+
})()
|
|
301
|
+
: undefined);
|
|
302
|
+
|
|
303
|
+
if (workerCodeFile == null) {
|
|
304
|
+
errors.push({
|
|
305
|
+
text: `Worker 번들 출력 파일을 찾을 수 없습니다: ${fullWorkerPath}`,
|
|
306
|
+
location: null,
|
|
307
|
+
});
|
|
308
|
+
return undefined;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// outdir 루트 기준 상대 경로. bundleWorker에서 outbase: undefined를 설정하여
|
|
312
|
+
// Worker 파일이 항상 outdir 루트에 출력됨을 보장한다.
|
|
313
|
+
return path.relative(outdir, workerCodeFile.path).replaceAll("\\", "/");
|
|
314
|
+
}
|
|
315
|
+
|
|
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
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
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;
|
|
354
|
+
}
|
|
355
|
+
chunks.push(effectiveContent.slice(cursor));
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
contents: chunks.join(""),
|
|
359
|
+
errors,
|
|
360
|
+
warnings,
|
|
361
|
+
workerOutputFiles: allOutputFiles.length > 0 ? allOutputFiles : undefined,
|
|
362
|
+
workerMetafile: mergedMetafile,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* esbuild Worker 번들링 플러그인을 생성한다.
|
|
368
|
+
*
|
|
369
|
+
* onLoad에서 .js/.ts 파일의 Worker/SharedWorker 패턴을 감지하여
|
|
370
|
+
* Worker 파일을 별도 ESM 번들로 빌드하고, URL 경로를 번들된 파일로 치환한다.
|
|
371
|
+
*/
|
|
372
|
+
export function createWorkerBundlePlugin(): esbuild.Plugin {
|
|
373
|
+
return {
|
|
374
|
+
name: "sd-worker-bundle",
|
|
375
|
+
setup(build) {
|
|
376
|
+
const pendingWorkerResults: Array<{
|
|
377
|
+
outputFiles?: esbuild.OutputFile[];
|
|
378
|
+
metafile?: esbuild.Metafile;
|
|
379
|
+
}> = [];
|
|
380
|
+
|
|
381
|
+
build.onLoad({ filter: /\.[cm]?[jt]sx?$/ }, (args) => {
|
|
382
|
+
const content = fs.readFileSync(args.path, "utf-8");
|
|
383
|
+
const result = transformWorkerPatterns(content, args.path, build);
|
|
384
|
+
if (result == null) return undefined;
|
|
385
|
+
|
|
386
|
+
// write: false일 때 Worker outputFiles를 모아뒀다가 onEnd에서 병합
|
|
387
|
+
if (result.workerOutputFiles != null || result.workerMetafile != null) {
|
|
388
|
+
pendingWorkerResults.push({
|
|
389
|
+
outputFiles: result.workerOutputFiles,
|
|
390
|
+
metafile: result.workerMetafile,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
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");
|
|
397
|
+
return {
|
|
398
|
+
contents: result.contents,
|
|
399
|
+
loader: isJsx ? ("tsx" as const) : ("js" as const),
|
|
400
|
+
errors: result.errors.length > 0 ? result.errors : undefined,
|
|
401
|
+
warnings: result.warnings.length > 0 ? result.warnings : undefined,
|
|
402
|
+
};
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
build.onEnd((result) => {
|
|
406
|
+
for (const wr of pendingWorkerResults) {
|
|
407
|
+
if (wr.outputFiles != null && wr.outputFiles.length > 0) {
|
|
408
|
+
result.outputFiles?.push(...wr.outputFiles);
|
|
409
|
+
}
|
|
410
|
+
if (result.metafile != null && wr.metafile != null) {
|
|
411
|
+
Object.assign(result.metafile.inputs, wr.metafile.inputs);
|
|
412
|
+
Object.assign(result.metafile.outputs, wr.metafile.outputs);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
pendingWorkerResults.length = 0;
|
|
416
|
+
});
|
|
417
|
+
},
|
|
418
|
+
};
|
|
419
|
+
}
|
|
@@ -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
|
/**
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
import { collectAllExternals, generateProductionFiles } from "../deps/server-externals/server-production-files";
|
|
19
19
|
import { SdTsCompiler } from "../ts-compiler/SdTsCompiler";
|
|
20
20
|
import { createTscPlugin } from "../esbuild/esbuild-tsc-plugin";
|
|
21
|
+
import { createWorkerBundlePlugin } from "../esbuild/esbuild-worker-plugin";
|
|
21
22
|
import { setupWorkerLifecycle } from "./shared-worker-lifecycle";
|
|
22
23
|
import { buildWatchPaths } from "./build-watch-paths";
|
|
23
24
|
import { copyPublicFiles, watchPublicFiles } from "../utils/copy-public";
|
|
@@ -140,7 +141,7 @@ async function build(info: ServerBuildInfo): Promise<ServerBuildResult> {
|
|
|
140
141
|
const entryPoints = getPackageSourceFiles(info.pkgDir, parsedConfig);
|
|
141
142
|
|
|
142
143
|
// 외부 모듈 수집
|
|
143
|
-
const
|
|
144
|
+
const { bundleExternals, prodDependencies } = collectAllExternals(info.pkgDir, info.externals);
|
|
144
145
|
|
|
145
146
|
let jsResult: { success: boolean; errors?: string[]; warnings?: string[] };
|
|
146
147
|
let tscErrors: string[];
|
|
@@ -162,10 +163,10 @@ async function build(info: ServerBuildInfo): Promise<ServerBuildResult> {
|
|
|
162
163
|
pkgDir: info.pkgDir,
|
|
163
164
|
entryPoints,
|
|
164
165
|
env: info.env,
|
|
165
|
-
external,
|
|
166
|
+
external: bundleExternals,
|
|
166
167
|
});
|
|
167
168
|
|
|
168
|
-
jsResult = await esbuild.build({ ...esbuildOptions, plugins: [tscPlugin.plugin] })
|
|
169
|
+
jsResult = await esbuild.build({ ...esbuildOptions, plugins: [createWorkerBundlePlugin(), tscPlugin.plugin] })
|
|
169
170
|
.then(async (result) => {
|
|
170
171
|
if (result.outputFiles) {
|
|
171
172
|
await writeChangedOutputFiles(result.outputFiles);
|
|
@@ -212,7 +213,7 @@ async function build(info: ServerBuildInfo): Promise<ServerBuildResult> {
|
|
|
212
213
|
|
|
213
214
|
await copyPublicFiles(info.pkgDir, false);
|
|
214
215
|
|
|
215
|
-
generateProductionFiles(info,
|
|
216
|
+
generateProductionFiles(info, prodDependencies);
|
|
216
217
|
}
|
|
217
218
|
|
|
218
219
|
const allErrors = [...(jsResult.errors ?? []), ...tscErrors];
|
|
@@ -310,7 +311,7 @@ async function startWatch(info: ServerWatchInfo): Promise<void> {
|
|
|
310
311
|
const entryPoints = getPackageSourceFiles(info.pkgDir, parsedConfig);
|
|
311
312
|
|
|
312
313
|
// 외부 모듈 수집 (watch 모드용 — watch manager가 자체 캐시를 유지)
|
|
313
|
-
const cachedExternal = collectAllExternals(info.pkgDir, info.externals);
|
|
314
|
+
const { bundleExternals: cachedExternal } = collectAllExternals(info.pkgDir, info.externals);
|
|
314
315
|
|
|
315
316
|
// esbuild 컨텍스트 생성 (JS 출력 필요 시, tsc 플러그인 포함)
|
|
316
317
|
if (info.output.js) {
|
|
@@ -10,6 +10,7 @@ import { createTscPlugin, type TscPluginResult } from "../esbuild/esbuild-tsc-pl
|
|
|
10
10
|
import type { TypecheckEnv } from "../utils/tsconfig";
|
|
11
11
|
import type { SerializedDiagnostic } from "../typecheck/typecheck-serialization";
|
|
12
12
|
import type { LintWithProgramResult } from "../lint/lint-with-program";
|
|
13
|
+
import { createWorkerBundlePlugin } from "../esbuild/esbuild-worker-plugin";
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* esbuild watch context 생성 옵션
|
|
@@ -62,9 +63,10 @@ export async function createContext(options: EsbuildContextOptions): Promise<voi
|
|
|
62
63
|
dev: true,
|
|
63
64
|
});
|
|
64
65
|
|
|
66
|
+
const workerPlugin = createWorkerBundlePlugin();
|
|
65
67
|
context = await esbuild.context({
|
|
66
68
|
...baseOptions,
|
|
67
|
-
plugins: tscPlugin != null ? [tscPlugin.plugin] : [],
|
|
69
|
+
plugins: tscPlugin != null ? [workerPlugin, tscPlugin.plugin] : [workerPlugin],
|
|
68
70
|
metafile: true,
|
|
69
71
|
write: false,
|
|
70
72
|
});
|
|
@@ -59,7 +59,7 @@ export async function startServerWatchLoop(config: ServerWatchLoopConfig): Promi
|
|
|
59
59
|
c.path.endsWith("package.json"),
|
|
60
60
|
);
|
|
61
61
|
if (hasPackageJsonChange) {
|
|
62
|
-
cachedExternal = collectAllExternals(info.pkgDir, info.externals);
|
|
62
|
+
cachedExternal = collectAllExternals(info.pkgDir, info.externals).bundleExternals;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
if (info.output.js) {
|
|
@@ -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줄 추가)과 무관
|