@simplysm/sd-cli 14.0.36 → 14.0.38
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/angular/vite-angular-plugin.d.ts +2 -5
- package/dist/angular/vite-angular-plugin.d.ts.map +1 -1
- package/dist/angular/vite-angular-plugin.js +19 -72
- package/dist/angular/vite-angular-plugin.js.map +1 -1
- package/dist/commands/publish/storage-publisher.js +1 -0
- package/dist/commands/publish/storage-publisher.js.map +1 -1
- package/dist/dev-server/hmr-service.d.ts +2 -0
- package/dist/dev-server/hmr-service.d.ts.map +1 -1
- package/dist/dev-server/hmr-service.js +24 -10
- package/dist/dev-server/hmr-service.js.map +1 -1
- package/dist/electron/electron.js +4 -4
- package/dist/electron/electron.js.map +1 -1
- package/dist/engines/BaseEngine.d.ts.map +1 -1
- package/dist/engines/BaseEngine.js +10 -1
- package/dist/engines/BaseEngine.js.map +1 -1
- package/dist/engines/EsbuildClientEngine.d.ts.map +1 -1
- package/dist/engines/EsbuildClientEngine.js +12 -1
- package/dist/engines/EsbuildClientEngine.js.map +1 -1
- package/dist/engines/engine-factory.d.ts +0 -4
- package/dist/engines/engine-factory.d.ts.map +1 -1
- package/dist/engines/engine-factory.js.map +1 -1
- package/dist/esbuild/esbuild-client-config.js +5 -5
- package/dist/esbuild/esbuild-client-config.js.map +1 -1
- package/dist/orchestrators/DevOrchestrator.d.ts.map +1 -1
- package/dist/orchestrators/DevOrchestrator.js +0 -5
- package/dist/orchestrators/DevOrchestrator.js.map +1 -1
- package/dist/orchestrators/TypecheckOrchestrator.js +2 -2
- package/dist/orchestrators/TypecheckOrchestrator.js.map +1 -1
- package/dist/sd-cli.js +2 -1
- package/dist/sd-cli.js.map +1 -1
- package/dist/utils/output-utils.d.ts.map +1 -1
- package/dist/utils/output-utils.js +3 -2
- package/dist/utils/output-utils.js.map +1 -1
- package/dist/workers/client.worker.d.ts.map +1 -1
- package/dist/workers/client.worker.js +7 -1
- package/dist/workers/client.worker.js.map +1 -1
- package/package.json +4 -4
- package/src/angular/vite-angular-plugin.ts +19 -82
- package/src/commands/publish/storage-publisher.ts +1 -0
- package/src/dev-server/hmr-service.ts +28 -13
- package/src/electron/electron.ts +5 -5
- package/src/engines/BaseEngine.ts +10 -1
- package/src/engines/EsbuildClientEngine.ts +13 -1
- package/src/engines/engine-factory.ts +0 -1
- package/src/esbuild/esbuild-client-config.ts +6 -6
- package/src/orchestrators/DevOrchestrator.ts +0 -6
- package/src/orchestrators/TypecheckOrchestrator.ts +2 -2
- package/src/sd-cli.ts +2 -1
- package/src/utils/output-utils.ts +3 -2
- package/src/workers/client.worker.ts +8 -1
- package/tests/angular/_vite-angular-plugin-test-setup.ts +3 -30
- package/tests/angular/vite-angular-plugin-legacy-watch.spec.ts +14 -31
- package/tests/angular/vite-angular-plugin-vitest.spec.ts +4 -34
- package/tests/angular/vite-angular-plugin.spec.ts +24 -60
- package/tests/commands/typecheck.spec.ts +1 -1
- package/tests/engines/base-engine.spec.ts +25 -0
- package/tests/engines/engine-adapter-isolation.spec.ts +3 -3
- package/tests/engines/esbuild-client-engine.acc.spec.ts +29 -0
- package/tests/engines/esbuild-client-engine.spec.ts +26 -0
- package/tests/orchestrators/build-orchestrator.spec.ts +1 -1
- package/tests/orchestrators/dev-orchestrator.spec.ts +1 -1
- package/tests/orchestrators/typecheck-orchestrator.spec.ts +1 -1
- package/tests/orchestrators/watch-orchestrator.spec.ts +1 -1
- package/tests/utils/esbuild-client-config.acc.spec.ts +4 -1
- package/tests/utils/hmr-service-dispatcher.acc.spec.ts +70 -0
- package/tests/workers/client-worker-initial-build-error.verify.md +8 -0
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type http from "node:http";
|
|
2
2
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import crypto from "crypto";
|
|
3
6
|
import type esbuild from "esbuild";
|
|
4
7
|
import { WebSocketServer, type WebSocket } from "ws";
|
|
5
8
|
|
|
@@ -10,6 +13,8 @@ export interface HmrServiceOptions {
|
|
|
10
13
|
basePath: string;
|
|
11
14
|
/** templateUpdates Map (createCompilerPlugin과 공유) */
|
|
12
15
|
templateUpdates: Map<string, string>;
|
|
16
|
+
/** 빌드 출력 디렉토리 경로. 설정 시 파일 내용 hash로 변경 감지 (bytes 대신) */
|
|
17
|
+
outDir?: string;
|
|
13
18
|
}
|
|
14
19
|
|
|
15
20
|
export interface HmrService {
|
|
@@ -24,7 +29,7 @@ export interface HmrService {
|
|
|
24
29
|
}
|
|
25
30
|
|
|
26
31
|
export function createHmrService(options: HmrServiceOptions): HmrService {
|
|
27
|
-
const { httpServer, basePath, templateUpdates } = options;
|
|
32
|
+
const { httpServer, basePath, templateUpdates, outDir } = options;
|
|
28
33
|
const clients = new Set<WebSocket>();
|
|
29
34
|
|
|
30
35
|
const wss = new WebSocketServer({ server: httpServer });
|
|
@@ -36,7 +41,7 @@ export function createHmrService(options: HmrServiceOptions): HmrService {
|
|
|
36
41
|
});
|
|
37
42
|
});
|
|
38
43
|
|
|
39
|
-
let prevOutputs: Map<string,
|
|
44
|
+
let prevOutputs: Map<string, string> | undefined;
|
|
40
45
|
let debounceTimer: ReturnType<typeof setTimeout> | undefined;
|
|
41
46
|
let pendingMetafile: esbuild.Metafile | undefined;
|
|
42
47
|
|
|
@@ -49,12 +54,22 @@ export function createHmrService(options: HmrServiceOptions): HmrService {
|
|
|
49
54
|
}, 100);
|
|
50
55
|
}
|
|
51
56
|
|
|
52
|
-
function collectOutputs(metafile: esbuild.Metafile): Map<string,
|
|
53
|
-
const outputs = new Map<string,
|
|
57
|
+
function collectOutputs(metafile: esbuild.Metafile): Map<string, string> {
|
|
58
|
+
const outputs = new Map<string, string>();
|
|
54
59
|
for (const [outputPath, output] of Object.entries(metafile.outputs)) {
|
|
55
60
|
const normalizedPath = outputPath.replace(/\\/g, "/");
|
|
56
61
|
if (normalizedPath.endsWith(".js") || normalizedPath.endsWith(".css")) {
|
|
57
|
-
|
|
62
|
+
let fingerprint = String(output.bytes);
|
|
63
|
+
if (outDir != null) {
|
|
64
|
+
try {
|
|
65
|
+
const filePath = path.resolve(outDir, normalizedPath);
|
|
66
|
+
const content = fs.readFileSync(filePath);
|
|
67
|
+
fingerprint = crypto.createHash("md5").update(content).digest("hex");
|
|
68
|
+
} catch {
|
|
69
|
+
// 파일 읽기 실패 시 bytes 폴백
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
outputs.set(normalizedPath, fingerprint);
|
|
58
73
|
}
|
|
59
74
|
}
|
|
60
75
|
return outputs;
|
|
@@ -91,12 +106,12 @@ export function createHmrService(options: HmrServiceOptions): HmrService {
|
|
|
91
106
|
let cssChanged = false;
|
|
92
107
|
const changedCssFiles: string[] = [];
|
|
93
108
|
|
|
94
|
-
for (const [
|
|
95
|
-
const
|
|
96
|
-
if (
|
|
97
|
-
if (
|
|
109
|
+
for (const [outputPath, fingerprint] of currentOutputs) {
|
|
110
|
+
const prevFingerprint = prevOutputs.get(outputPath);
|
|
111
|
+
if (prevFingerprint !== fingerprint) {
|
|
112
|
+
if (outputPath.endsWith(".css")) {
|
|
98
113
|
cssChanged = true;
|
|
99
|
-
changedCssFiles.push(
|
|
114
|
+
changedCssFiles.push(outputPath.split("/").pop() ?? outputPath);
|
|
100
115
|
} else {
|
|
101
116
|
jsChanged = true;
|
|
102
117
|
}
|
|
@@ -104,9 +119,9 @@ export function createHmrService(options: HmrServiceOptions): HmrService {
|
|
|
104
119
|
}
|
|
105
120
|
|
|
106
121
|
// 삭제된 파일 체크
|
|
107
|
-
for (const [
|
|
108
|
-
if (!currentOutputs.has(
|
|
109
|
-
if (
|
|
122
|
+
for (const [outputPath] of prevOutputs) {
|
|
123
|
+
if (!currentOutputs.has(outputPath)) {
|
|
124
|
+
if (outputPath.endsWith(".css")) {
|
|
110
125
|
cssChanged = true;
|
|
111
126
|
} else {
|
|
112
127
|
jsChanged = true;
|
package/src/electron/electron.ts
CHANGED
|
@@ -97,7 +97,7 @@ export class Electron {
|
|
|
97
97
|
|
|
98
98
|
let currentElectron: cpx.SpawnProcess | null = null;
|
|
99
99
|
let isRestarting = false;
|
|
100
|
-
let resolveTermination: (() => void) | null = null;
|
|
100
|
+
let resolveTermination: (() => void | Promise<void>) | null = null;
|
|
101
101
|
|
|
102
102
|
const spawnElectron = () => {
|
|
103
103
|
Electron._logger.debug("Electron 프로세스 시작");
|
|
@@ -112,7 +112,7 @@ export class Electron {
|
|
|
112
112
|
currentElectron = null;
|
|
113
113
|
if (!isRestarting && resolveTermination != null) {
|
|
114
114
|
Electron._logger.info("Electron이 종료되었습니다.");
|
|
115
|
-
resolveTermination();
|
|
115
|
+
void resolveTermination();
|
|
116
116
|
}
|
|
117
117
|
});
|
|
118
118
|
};
|
|
@@ -160,13 +160,13 @@ export class Electron {
|
|
|
160
160
|
await new Promise<void>((resolve) => {
|
|
161
161
|
let disposed = false;
|
|
162
162
|
|
|
163
|
-
const cleanup = () => {
|
|
163
|
+
const cleanup = async () => {
|
|
164
164
|
if (disposed) return;
|
|
165
165
|
disposed = true;
|
|
166
166
|
Electron._logger.debug("cleanup 시작");
|
|
167
167
|
process.removeListener("SIGINT", signalHandler);
|
|
168
168
|
process.removeListener("SIGTERM", signalHandler);
|
|
169
|
-
|
|
169
|
+
await ctx.dispose();
|
|
170
170
|
resolve();
|
|
171
171
|
};
|
|
172
172
|
|
|
@@ -175,7 +175,7 @@ export class Electron {
|
|
|
175
175
|
const signalHandler = () => {
|
|
176
176
|
Electron._logger.debug("시그널 수신, Electron 종료 중");
|
|
177
177
|
if (currentElectron != null) currentElectron.kill();
|
|
178
|
-
cleanup();
|
|
178
|
+
void cleanup();
|
|
179
179
|
};
|
|
180
180
|
|
|
181
181
|
process.once("SIGINT", signalHandler);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Worker, type WorkerProxy } from "@simplysm/core-node";
|
|
2
|
+
import { err as errNs } from "@simplysm/core-common";
|
|
2
3
|
import { consola } from "consola";
|
|
3
4
|
import type { BuildResult, ResultCollector } from "../runtime/ResultCollector";
|
|
4
5
|
import { stopEngineWorker } from "../runtime/engine-stop";
|
|
@@ -182,7 +183,15 @@ export abstract class BaseEngine<
|
|
|
182
183
|
}
|
|
183
184
|
});
|
|
184
185
|
|
|
185
|
-
this._callStartWatch(output).catch(() => {
|
|
186
|
+
this._callStartWatch(output).catch((err: unknown) => {
|
|
187
|
+
logger.error(`[${this._pkg.name}] startWatch 실패:`, errNs.message(err));
|
|
188
|
+
this._resultCollector?.add({
|
|
189
|
+
name: this._pkg.name,
|
|
190
|
+
target: this._getTarget(),
|
|
191
|
+
type: "build",
|
|
192
|
+
status: "error",
|
|
193
|
+
message: errNs.message(err),
|
|
194
|
+
});
|
|
186
195
|
resolveInitialBuild();
|
|
187
196
|
});
|
|
188
197
|
|
|
@@ -121,7 +121,7 @@ export class EsbuildClientEngine implements BuildEngine {
|
|
|
121
121
|
? this._pkg.config.server
|
|
122
122
|
: undefined;
|
|
123
123
|
|
|
124
|
-
await this._worker!.startWatch({
|
|
124
|
+
const result = await this._worker!.startWatch({
|
|
125
125
|
name: this._pkg.name,
|
|
126
126
|
cwd: this._cwd,
|
|
127
127
|
pkgDir: this._pkg.dir,
|
|
@@ -131,6 +131,18 @@ export class EsbuildClientEngine implements BuildEngine {
|
|
|
131
131
|
pwa: this._pkg.config.pwa,
|
|
132
132
|
browserSupport: this._pkg.config.browserSupport,
|
|
133
133
|
});
|
|
134
|
+
|
|
135
|
+
if (!result.success) {
|
|
136
|
+
const errorDetail = result.errors?.join("; ") ?? "unknown error";
|
|
137
|
+
logger.error(`[${this._pkg.name}] 초기 빌드 실패: ${errorDetail}`);
|
|
138
|
+
this._resultCollector?.add({
|
|
139
|
+
name: this._pkg.name,
|
|
140
|
+
target: "client",
|
|
141
|
+
type: "build",
|
|
142
|
+
status: "error",
|
|
143
|
+
message: errorDetail,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
134
146
|
}
|
|
135
147
|
|
|
136
148
|
/**
|
|
@@ -24,7 +24,6 @@ export function createBuildEngine(
|
|
|
24
24
|
options: {
|
|
25
25
|
cwd: string;
|
|
26
26
|
replaceDeps?: Record<string, string>;
|
|
27
|
-
resolvedReplaceDeps?: Array<{ packageName: string; sourcePath: string }>;
|
|
28
27
|
resultCollector?: ResultCollector;
|
|
29
28
|
rebuildManager?: RebuildManager;
|
|
30
29
|
/** 클라이언트 빌드 출력 경로 (EsbuildClientEngine에만 적용) */
|
|
@@ -65,16 +65,16 @@ export async function createClientEsbuildContext(
|
|
|
65
65
|
const sourceFileCache = new SourceFileCache(cachePath);
|
|
66
66
|
|
|
67
67
|
// CompilerPluginOptions
|
|
68
|
-
const pluginOptions: CompilerPluginOptions
|
|
68
|
+
const pluginOptions: CompilerPluginOptions = {
|
|
69
69
|
tsconfig: options.tsconfig ?? path.join(options.pkgDir, "tsconfig.json"),
|
|
70
70
|
sourcemap: isDev,
|
|
71
71
|
advancedOptimizations: !isDev,
|
|
72
72
|
thirdPartySourcemaps: isDev,
|
|
73
|
-
incremental:
|
|
73
|
+
incremental: isDev,
|
|
74
74
|
sourceFileCache,
|
|
75
75
|
loadResultCache: sourceFileCache.loadResultCache,
|
|
76
76
|
templateUpdates: options.templateUpdates,
|
|
77
|
-
|
|
77
|
+
includeTestMetadata: isDev,
|
|
78
78
|
};
|
|
79
79
|
|
|
80
80
|
// BundleStylesheetOptions
|
|
@@ -106,7 +106,7 @@ export async function createClientEsbuildContext(
|
|
|
106
106
|
// SCSS side-effect import 처리 플러그인
|
|
107
107
|
const scssPlugin = createScssPlugin({
|
|
108
108
|
loadPaths: [
|
|
109
|
-
path.join(options.pkgDir, "
|
|
109
|
+
path.join(options.pkgDir, "node_modules"),
|
|
110
110
|
path.join(options.cwd, "node_modules"),
|
|
111
111
|
],
|
|
112
112
|
});
|
|
@@ -156,7 +156,7 @@ export async function createClientEsbuildContext(
|
|
|
156
156
|
],
|
|
157
157
|
target: esbuildTarget,
|
|
158
158
|
entryNames: isDev ? "[name]" : "[name]-[hash]",
|
|
159
|
-
chunkNames:
|
|
159
|
+
chunkNames: "[name]-[hash]",
|
|
160
160
|
assetNames: isDev ? "[name]" : "[name]-[hash]",
|
|
161
161
|
bundle: true,
|
|
162
162
|
splitting: options.legacyModule !== true,
|
|
@@ -166,7 +166,7 @@ export async function createClientEsbuildContext(
|
|
|
166
166
|
metafile: true,
|
|
167
167
|
write: true,
|
|
168
168
|
sourcemap: isDev ? "linked" : false,
|
|
169
|
-
logLevel: "silent",
|
|
169
|
+
logLevel: isDev ? "warning" : "silent",
|
|
170
170
|
tsconfig: options.tsconfig ?? path.join(options.pkgDir, "tsconfig.json"),
|
|
171
171
|
define,
|
|
172
172
|
banner: hmrBanner != null ? { js: hmrBanner } : undefined,
|
|
@@ -97,11 +97,6 @@ export class DevOrchestrator extends BaseOrchestrator implements OrchestratorLif
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
// 클라이언트 패키지용 BuildEngine 생성
|
|
100
|
-
const resolvedReplaceDeps = this._replaceDepWatcher?.entries.map((e) => ({
|
|
101
|
-
packageName: e.targetName,
|
|
102
|
-
sourcePath: e.resolvedSourcePath,
|
|
103
|
-
}));
|
|
104
|
-
|
|
105
100
|
for (const { name, dir, config } of this._clientPackages) {
|
|
106
101
|
const engineConfig = { ...config, env: { ...this._baseEnv, ...config.env } };
|
|
107
102
|
const engine = createBuildEngine(
|
|
@@ -109,7 +104,6 @@ export class DevOrchestrator extends BaseOrchestrator implements OrchestratorLif
|
|
|
109
104
|
{
|
|
110
105
|
cwd: this._cwd,
|
|
111
106
|
replaceDeps: this._replaceDeps,
|
|
112
|
-
resolvedReplaceDeps,
|
|
113
107
|
resultCollector: this._resultCollector,
|
|
114
108
|
rebuildManager: this._rebuildManager,
|
|
115
109
|
},
|
|
@@ -262,8 +262,8 @@ export class TypecheckOrchestrator implements OrchestratorLifecycle<TypecheckRes
|
|
|
262
262
|
deserializeDiagnostic(d, fileCache),
|
|
263
263
|
);
|
|
264
264
|
allDiagnostics.push(...buildDiags);
|
|
265
|
-
totalErrorCount += buildDiags.filter((d) => d.category ===
|
|
266
|
-
totalWarningCount += buildDiags.filter((d) => d.category ===
|
|
265
|
+
totalErrorCount += buildDiags.filter((d) => d.category === ts.DiagnosticCategory.Error).length;
|
|
266
|
+
totalWarningCount += buildDiags.filter((d) => d.category === ts.DiagnosticCategory.Warning).length;
|
|
267
267
|
if (!engineResult.build.success && buildDiags.length === 0) {
|
|
268
268
|
for (const errMsg of engineResult.build.errors) {
|
|
269
269
|
allDiagnostics.push({
|
package/src/sd-cli.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* .js 실행 (프로덕션): replaceDeps 실행 후 새 프로세스에서 sd-cli-entry 실행
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { cpx } from "@simplysm/core-node";
|
|
10
|
+
import { cpx, setupConsola } from "@simplysm/core-node";
|
|
11
11
|
import { consola } from "consola";
|
|
12
12
|
import os from "os";
|
|
13
13
|
import path from "path";
|
|
@@ -26,6 +26,7 @@ if (isDev) {
|
|
|
26
26
|
await createCliParser(process.argv.slice(2)).parse();
|
|
27
27
|
} else {
|
|
28
28
|
// Production mode (.js): two-stage execution
|
|
29
|
+
setupConsola({ cli: true });
|
|
29
30
|
|
|
30
31
|
// Phase 1: replaceDeps (인라인 — 설치된 버전으로 복사)
|
|
31
32
|
try {
|
|
@@ -50,8 +50,9 @@ export function printServers(
|
|
|
50
50
|
if (server.target === "server") {
|
|
51
51
|
// 서버에 연결된 클라이언트가 있으면 클라이언트 URL만 출력
|
|
52
52
|
const clients = serverClientsMap?.get(server.name) ?? [];
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
const activeClients = clients.filter((c) => results.get(`${c}:build`)?.status !== "error");
|
|
54
|
+
if (activeClients.length > 0) {
|
|
55
|
+
for (const clientName of activeClients) {
|
|
55
56
|
consola.info(`[server] http://localhost:${server.port}/${clientName}/`);
|
|
56
57
|
}
|
|
57
58
|
} else {
|
|
@@ -282,7 +282,13 @@ async function startWatch(info: ClientBuildInfo): Promise<ClientBuildResult> {
|
|
|
282
282
|
// 초기 빌드 완료 시 resolve
|
|
283
283
|
if (isInitialBuild) {
|
|
284
284
|
isInitialBuild = false;
|
|
285
|
-
initialBuildResolve?.({
|
|
285
|
+
initialBuildResolve?.({
|
|
286
|
+
success,
|
|
287
|
+
errors:
|
|
288
|
+
result.errors.length > 0
|
|
289
|
+
? result.errors.map((e) => e.text)
|
|
290
|
+
: undefined,
|
|
291
|
+
});
|
|
286
292
|
}
|
|
287
293
|
} catch (err) {
|
|
288
294
|
const message = errNs.message(err);
|
|
@@ -309,6 +315,7 @@ async function startWatch(info: ClientBuildInfo): Promise<ClientBuildResult> {
|
|
|
309
315
|
httpServer: httpDevServer.httpServer,
|
|
310
316
|
basePath,
|
|
311
317
|
templateUpdates,
|
|
318
|
+
outDir: outdir,
|
|
312
319
|
});
|
|
313
320
|
|
|
314
321
|
// 7. esbuild watch 시작 + 초기 빌드 대기
|
|
@@ -1,37 +1,10 @@
|
|
|
1
|
-
import { vi } from "vitest";
|
|
2
1
|
import path from "path";
|
|
3
|
-
import type { SdConfig, SdClientPackageConfig } from "../../src/sd-config.types";
|
|
4
2
|
|
|
5
3
|
export const FIXTURE_DIR = path.resolve(import.meta.dirname, "fixtures");
|
|
6
4
|
export const PKG_DIR = path.resolve(FIXTURE_DIR, "packages/basic-app");
|
|
7
5
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
return {
|
|
12
|
-
packages: {
|
|
13
|
-
"basic-app": { target: "client", server: 3000, ...overrides },
|
|
14
|
-
},
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** Vite lifecycle 시뮬레이션: config → configResolved → [configureServer] → buildStart */
|
|
19
|
-
export async function initPlugin(
|
|
20
|
-
plugin: any,
|
|
21
|
-
options?: { mode?: string; command?: string; sourcemap?: boolean; withServer?: boolean },
|
|
22
|
-
): Promise<void> {
|
|
23
|
-
const mode = options?.mode ?? "development";
|
|
24
|
-
const command = options?.command ?? "serve";
|
|
25
|
-
const sourcemap = options?.sourcemap ?? false;
|
|
26
|
-
await plugin.config?.({}, { mode, command });
|
|
27
|
-
plugin.configResolved?.({ build: { sourcemap } });
|
|
28
|
-
if (options?.withServer === true) {
|
|
29
|
-
plugin.configureServer?.({
|
|
30
|
-
middlewares: { use: vi.fn() },
|
|
31
|
-
httpServer: { on: vi.fn() },
|
|
32
|
-
config: { base: "/" },
|
|
33
|
-
hot: { send: vi.fn() },
|
|
34
|
-
});
|
|
35
|
-
}
|
|
6
|
+
/** Vite lifecycle 시뮬레이션: config → buildStart */
|
|
7
|
+
export async function initPlugin(plugin: any): Promise<void> {
|
|
8
|
+
await plugin.config?.({}, { mode: "development", command: "serve" });
|
|
36
9
|
await plugin.buildStart?.call({});
|
|
37
10
|
}
|
|
@@ -1,35 +1,22 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
2
|
import path from "path";
|
|
3
|
-
import type { SdConfig } from "../../src/sd-config.types";
|
|
4
3
|
import {
|
|
5
4
|
FIXTURE_DIR,
|
|
6
5
|
PKG_DIR,
|
|
7
|
-
createTestSdConfig,
|
|
8
6
|
initPlugin,
|
|
9
7
|
} from "./_vite-angular-plugin-test-setup";
|
|
10
8
|
|
|
11
|
-
const mockLoadSdConfig = vi.fn<(...args: unknown[]) => Promise<SdConfig>>();
|
|
12
|
-
|
|
13
|
-
vi.mock("../../src/utils/sd-config", () => ({
|
|
14
|
-
loadSdConfig: (...args: unknown[]) => mockLoadSdConfig(...args),
|
|
15
|
-
}));
|
|
16
|
-
|
|
17
9
|
const { sdAngularPlugin } = await import("../../src/angular/vite-angular-plugin.js");
|
|
18
10
|
|
|
19
|
-
describe("sdAngularPlugin
|
|
11
|
+
describe("sdAngularPlugin watch rebuild", () => {
|
|
20
12
|
beforeEach(() => {
|
|
21
13
|
vi.clearAllMocks();
|
|
22
14
|
vi.spyOn(process, "cwd").mockReturnValue(FIXTURE_DIR);
|
|
23
|
-
mockLoadSdConfig.mockResolvedValue(
|
|
24
|
-
createTestSdConfig({ browserSupport: { legacyModule: true } }),
|
|
25
|
-
);
|
|
26
15
|
});
|
|
27
16
|
|
|
28
17
|
// Acceptance: 재빌드 시 변경 파일의 캐시가 무효화되고 증분 컴파일된다
|
|
29
18
|
it("invalidates cache and produces updated output on watch rebuild", async () => {
|
|
30
|
-
const plugin = sdAngularPlugin({
|
|
31
|
-
pkg: "basic-app",
|
|
32
|
-
});
|
|
19
|
+
const plugin = sdAngularPlugin({ pkg: "basic-app" });
|
|
33
20
|
|
|
34
21
|
// 초기 빌드
|
|
35
22
|
await initPlugin(plugin);
|
|
@@ -39,31 +26,29 @@ describe("sdAngularPlugin legacy watch rebuild", () => {
|
|
|
39
26
|
.replace(/\\/g, "/");
|
|
40
27
|
|
|
41
28
|
// 초기 transform 결과 캡처
|
|
42
|
-
const initialResult =
|
|
29
|
+
const initialResult = (plugin as any).transform?.call({}, "", appComponentPath);
|
|
43
30
|
expect(initialResult).toBeDefined();
|
|
44
31
|
expect(initialResult.code.length).toBeGreaterThan(0);
|
|
45
32
|
|
|
46
33
|
// watchChange 호출 (파일 변경 알림)
|
|
47
34
|
expect((plugin as any).watchChange).toBeDefined();
|
|
48
|
-
|
|
35
|
+
(plugin as any).watchChange?.call({}, appComponentPath, { event: "update" });
|
|
49
36
|
|
|
50
37
|
// 재빌드 (buildStart 재호출)
|
|
51
38
|
await (plugin as any).buildStart?.call({});
|
|
52
39
|
|
|
53
40
|
// 재빌드 후 transform — 컴파일러가 재실행되어 결과를 반환해야 한다
|
|
54
|
-
const rebuiltResult =
|
|
41
|
+
const rebuiltResult = (plugin as any).transform?.call({}, "", appComponentPath);
|
|
55
42
|
expect(rebuiltResult).toBeDefined();
|
|
56
43
|
expect(rebuiltResult.code).toBeDefined();
|
|
57
44
|
expect(rebuiltResult.code.length).toBeGreaterThan(0);
|
|
58
45
|
|
|
59
|
-
|
|
46
|
+
(plugin as any).buildEnd?.call({});
|
|
60
47
|
});
|
|
61
48
|
|
|
62
|
-
// Acceptance: 첫 buildStart는
|
|
49
|
+
// Acceptance: 첫 buildStart는 전체 컴파일한다
|
|
63
50
|
it("performs full compilation on first buildStart", async () => {
|
|
64
|
-
const plugin = sdAngularPlugin({
|
|
65
|
-
pkg: "basic-app",
|
|
66
|
-
});
|
|
51
|
+
const plugin = sdAngularPlugin({ pkg: "basic-app" });
|
|
67
52
|
|
|
68
53
|
// watchChange 없이 첫 buildStart
|
|
69
54
|
await initPlugin(plugin);
|
|
@@ -72,33 +57,31 @@ describe("sdAngularPlugin legacy watch rebuild", () => {
|
|
|
72
57
|
.join(PKG_DIR, "src/app.component.ts")
|
|
73
58
|
.replace(/\\/g, "/");
|
|
74
59
|
|
|
75
|
-
const result =
|
|
60
|
+
const result = (plugin as any).transform?.call({}, "", appComponentPath);
|
|
76
61
|
expect(result).toBeDefined();
|
|
77
62
|
expect(result.code.length).toBeGreaterThan(0);
|
|
78
63
|
|
|
79
|
-
|
|
64
|
+
(plugin as any).buildEnd?.call({});
|
|
80
65
|
});
|
|
81
66
|
|
|
82
67
|
// Acceptance: watchChange 없이 buildStart 재호출 시에도 정상 동작
|
|
83
68
|
it("handles buildStart re-invocation without watchChange gracefully", async () => {
|
|
84
|
-
const plugin = sdAngularPlugin({
|
|
85
|
-
pkg: "basic-app",
|
|
86
|
-
});
|
|
69
|
+
const plugin = sdAngularPlugin({ pkg: "basic-app" });
|
|
87
70
|
|
|
88
71
|
// 초기 빌드
|
|
89
72
|
await initPlugin(plugin);
|
|
90
73
|
|
|
91
|
-
// watchChange 없이 재빌드
|
|
74
|
+
// watchChange 없이 재빌드
|
|
92
75
|
await (plugin as any).buildStart?.call({});
|
|
93
76
|
|
|
94
77
|
const appComponentPath = path
|
|
95
78
|
.join(PKG_DIR, "src/app.component.ts")
|
|
96
79
|
.replace(/\\/g, "/");
|
|
97
80
|
|
|
98
|
-
const result =
|
|
81
|
+
const result = (plugin as any).transform?.call({}, "", appComponentPath);
|
|
99
82
|
expect(result).toBeDefined();
|
|
100
83
|
expect(result.code.length).toBeGreaterThan(0);
|
|
101
84
|
|
|
102
|
-
|
|
85
|
+
(plugin as any).buildEnd?.call({});
|
|
103
86
|
});
|
|
104
87
|
});
|
|
@@ -1,19 +1,11 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeAll } from "vitest";
|
|
2
2
|
import type { Plugin } from "vite";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
|
-
import type { SdConfig } from "../../src/sd-config.types";
|
|
5
4
|
import {
|
|
6
5
|
FIXTURE_DIR,
|
|
7
6
|
PKG_DIR,
|
|
8
|
-
createTestSdConfig,
|
|
9
7
|
} from "./_vite-angular-plugin-test-setup";
|
|
10
8
|
|
|
11
|
-
const mockLoadSdConfig = vi.fn<(...args: unknown[]) => Promise<SdConfig>>();
|
|
12
|
-
|
|
13
|
-
vi.mock("../../src/utils/sd-config", () => ({
|
|
14
|
-
loadSdConfig: (...args: unknown[]) => mockLoadSdConfig(...args),
|
|
15
|
-
}));
|
|
16
|
-
|
|
17
9
|
const { sdAngularPlugin } = await import("../../src/angular/vite-angular-plugin");
|
|
18
10
|
|
|
19
11
|
describe("sdAngularPlugin Vitest 지원", () => {
|
|
@@ -21,18 +13,16 @@ describe("sdAngularPlugin Vitest 지원", () => {
|
|
|
21
13
|
|
|
22
14
|
beforeAll(async () => {
|
|
23
15
|
vi.spyOn(process, "cwd").mockReturnValue(FIXTURE_DIR);
|
|
24
|
-
mockLoadSdConfig.mockResolvedValue(createTestSdConfig());
|
|
25
16
|
|
|
26
17
|
plugin = sdAngularPlugin({ pkg: "basic-app" });
|
|
27
18
|
await (plugin as any).config?.({}, { mode: "development", command: "serve" });
|
|
28
|
-
(plugin as any).configResolved?.({ build: { sourcemap: false } });
|
|
29
19
|
await (plugin as any).buildStart?.call({});
|
|
30
20
|
}, 60_000);
|
|
31
21
|
|
|
32
22
|
// Scenario: @Component 소스 컴파일 및 ɵcmp 런타임 코드 서빙
|
|
33
|
-
it("compiles @Component source and serves ɵcmp runtime code",
|
|
23
|
+
it("compiles @Component source and serves ɵcmp runtime code", () => {
|
|
34
24
|
const filePath = resolve(PKG_DIR, "src/app.component.ts");
|
|
35
|
-
const result =
|
|
25
|
+
const result = (plugin as any).transform?.call({}, "", filePath);
|
|
36
26
|
|
|
37
27
|
expect(result).toBeDefined();
|
|
38
28
|
expect(result!.code).toContain("ɵcmp");
|
|
@@ -40,32 +30,12 @@ describe("sdAngularPlugin Vitest 지원", () => {
|
|
|
40
30
|
});
|
|
41
31
|
|
|
42
32
|
// Scenario: node_modules .ts 파일은 AOT 대상이 아니므로 undefined 반환
|
|
43
|
-
it("returns undefined for node_modules .ts path",
|
|
44
|
-
const result =
|
|
33
|
+
it("returns undefined for node_modules .ts path", () => {
|
|
34
|
+
const result = (plugin as any).transform?.call(
|
|
45
35
|
{},
|
|
46
36
|
"",
|
|
47
37
|
resolve(FIXTURE_DIR, "node_modules/@angular/core/index.ts"),
|
|
48
38
|
);
|
|
49
39
|
expect(result).toBeUndefined();
|
|
50
40
|
});
|
|
51
|
-
|
|
52
|
-
// Scenario: browser target 패키지에서 플러그인 초기화
|
|
53
|
-
it("initializes successfully with non-client (browser target) package", async () => {
|
|
54
|
-
mockLoadSdConfig.mockResolvedValue(
|
|
55
|
-
createTestSdConfig({ target: "browser" } as any),
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
const browserPlugin = sdAngularPlugin({ pkg: "basic-app" });
|
|
59
|
-
|
|
60
|
-
await (browserPlugin as any).config?.({}, { mode: "development", command: "serve" });
|
|
61
|
-
(browserPlugin as any).configResolved?.({ build: { sourcemap: false } });
|
|
62
|
-
await (browserPlugin as any).buildStart?.call({});
|
|
63
|
-
|
|
64
|
-
const appComponentPath = resolve(PKG_DIR, "src/app.component.ts");
|
|
65
|
-
const result = await (browserPlugin as any).transform?.call({}, "", appComponentPath);
|
|
66
|
-
expect(result).toBeDefined();
|
|
67
|
-
expect(result.code.length).toBeGreaterThan(0);
|
|
68
|
-
|
|
69
|
-
await (browserPlugin as any).buildEnd?.call({});
|
|
70
|
-
});
|
|
71
41
|
});
|