@simplysm/sd-cli 14.0.96 → 14.0.97
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/commands/init/generators/client.d.ts.map +1 -1
- package/dist/commands/init/generators/client.js +3 -0
- package/dist/commands/init/generators/client.js.map +1 -1
- package/dist/commands/init/normalize.d.ts.map +1 -1
- package/dist/commands/init/normalize.js +1 -0
- package/dist/commands/init/normalize.js.map +1 -1
- package/dist/commands/init/prompts.d.ts.map +1 -1
- package/dist/commands/init/prompts.js +8 -1
- package/dist/commands/init/prompts.js.map +1 -1
- package/dist/commands/init/types.d.ts +3 -0
- package/dist/commands/init/types.d.ts.map +1 -1
- package/dist/engines/EsbuildClientEngine.d.ts.map +1 -1
- package/dist/engines/EsbuildClientEngine.js +1 -0
- package/dist/engines/EsbuildClientEngine.js.map +1 -1
- package/dist/esbuild/esbuild-client-config.d.ts.map +1 -1
- package/dist/esbuild/esbuild-client-config.js +2 -11
- package/dist/esbuild/esbuild-client-config.js.map +1 -1
- package/dist/esbuild/esbuild-postcss-plugin.d.ts +4 -0
- package/dist/esbuild/esbuild-postcss-plugin.d.ts.map +1 -1
- package/dist/esbuild/esbuild-postcss-plugin.js +15 -0
- package/dist/esbuild/esbuild-postcss-plugin.js.map +1 -1
- package/dist/esbuild/esbuild-ssr-config.d.ts +27 -0
- package/dist/esbuild/esbuild-ssr-config.d.ts.map +1 -0
- package/dist/esbuild/esbuild-ssr-config.js +113 -0
- package/dist/esbuild/esbuild-ssr-config.js.map +1 -0
- package/dist/sd-config.types.d.ts +7 -0
- package/dist/sd-config.types.d.ts.map +1 -1
- package/dist/ssg/prerender.d.ts +19 -0
- package/dist/ssg/prerender.d.ts.map +1 -0
- package/dist/ssg/prerender.js +43 -0
- package/dist/ssg/prerender.js.map +1 -0
- package/dist/workers/client.worker.d.ts +2 -0
- package/dist/workers/client.worker.d.ts.map +1 -1
- package/dist/workers/client.worker.js +21 -0
- package/dist/workers/client.worker.js.map +1 -1
- package/package.json +6 -6
- package/src/commands/init/generators/client.ts +8 -0
- package/src/commands/init/normalize.ts +1 -0
- package/src/commands/init/prompts.ts +9 -1
- package/src/commands/init/templates/client/package.json.hbs +2 -1
- package/src/commands/init/templates/client/src/main.server.ts.hbs +24 -0
- package/src/commands/init/templates/client/src/main.ts.hbs +13 -0
- package/src/commands/init/templates/workspace-root/sd.config.ts.hbs +3 -0
- package/src/commands/init/types.ts +3 -0
- package/src/engines/EsbuildClientEngine.ts +1 -0
- package/src/esbuild/esbuild-client-config.ts +2 -12
- package/src/esbuild/esbuild-postcss-plugin.ts +18 -0
- package/src/esbuild/esbuild-ssr-config.ts +149 -0
- package/src/sd-config.types.ts +7 -0
- package/src/ssg/prerender.ts +65 -0
- package/src/workers/client.worker.ts +26 -0
- package/tests/engines/base-engine.spec.ts +1 -26
- package/tests/init/__snapshots__/render.spec.ts.snap +38 -20
- package/tests/init/render.spec.ts +113 -33
- package/tests/utils/hmr-client-script.acc.spec.ts +0 -21
- package/tests/angular/vite-angular-plugin.spec.ts +0 -102
- package/tests/engines/engine-adapter-isolation.spec.ts +0 -79
- package/tests/runtime/signal-handler.spec.ts +0 -21
- package/tests/utils/angular-build.spec.ts +0 -109
- package/tests/utils/esbuild-client-config.acc.spec.ts +0 -438
- package/tests/utils/esbuild-client-config.spec.ts +0 -659
- package/tests/utils/hmr-client-script.spec.ts +0 -44
- package/tests/utils/tsconfig-angular.spec.ts +0 -9
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
|
|
5
|
-
// Acceptance: Scenario "EsbuildClientEngine이 어댑터 격리를 준수한다"
|
|
6
|
-
describe("EsbuildClientEngine adapter isolation", () => {
|
|
7
|
-
it("EsbuildClientEngine과 그 워커가 @angular/* 패키지를 직접 import하지 않음", () => {
|
|
8
|
-
|
|
9
|
-
const sdCliSrc = path.resolve(import.meta.dirname, "../../src");
|
|
10
|
-
|
|
11
|
-
const filesToCheck = [
|
|
12
|
-
path.join(sdCliSrc, "engines", "EsbuildClientEngine.ts"),
|
|
13
|
-
path.join(sdCliSrc, "workers", "client.worker.ts"),
|
|
14
|
-
];
|
|
15
|
-
|
|
16
|
-
const angularImportPattern = /from\s+["']@angular\/(build|compiler-cli)/;
|
|
17
|
-
|
|
18
|
-
for (const filePath of filesToCheck) {
|
|
19
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
20
|
-
const hasDirectImport = angularImportPattern.test(content);
|
|
21
|
-
expect(
|
|
22
|
-
hasDirectImport,
|
|
23
|
-
`${path.basename(filePath)} should not directly import @angular/*`,
|
|
24
|
-
).toBe(false);
|
|
25
|
-
}
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("vite-angular-plugin.ts does not import @angular/* directly", () => {
|
|
29
|
-
|
|
30
|
-
const pluginFile = path.resolve(
|
|
31
|
-
import.meta.dirname,
|
|
32
|
-
"../../src/angular/vite-angular-plugin.ts",
|
|
33
|
-
);
|
|
34
|
-
const content = fs.readFileSync(pluginFile, "utf-8");
|
|
35
|
-
|
|
36
|
-
const angularImportPattern = /from\s+["']@angular\/(build|compiler-cli)/;
|
|
37
|
-
expect(angularImportPattern.test(content)).toBe(false);
|
|
38
|
-
expect(content).not.toContain("createAngularCompilation");
|
|
39
|
-
expect(content).not.toMatch(/\bSourceFileCache\b(?<!AngularSourceFileCache)/);
|
|
40
|
-
expect(content).not.toContain("ComponentStylesheetBundler");
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
// Acceptance: Scenario "NgtscEngine이 어댑터 격리를 준수한다"
|
|
45
|
-
describe("NgtscEngine adapter isolation", () => {
|
|
46
|
-
it("NgtscEngine과 그 의존성이 @angular/* 패키지를 직접 import하지 않음", () => {
|
|
47
|
-
|
|
48
|
-
const sdCliSrc = path.resolve(import.meta.dirname, "../../src");
|
|
49
|
-
|
|
50
|
-
const filesToCheck = [
|
|
51
|
-
path.join(sdCliSrc, "engines", "NgtscEngine.ts"),
|
|
52
|
-
path.join(sdCliSrc, "angular", "ngtsc-build-core.ts"),
|
|
53
|
-
path.join(sdCliSrc, "utils", "output-path-rewriter.ts"),
|
|
54
|
-
];
|
|
55
|
-
|
|
56
|
-
const angularImportPattern = /from\s+["']@angular\/(build|compiler-cli)/;
|
|
57
|
-
|
|
58
|
-
for (const filePath of filesToCheck) {
|
|
59
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
60
|
-
const hasDirectImport = angularImportPattern.test(content);
|
|
61
|
-
expect(
|
|
62
|
-
hasDirectImport,
|
|
63
|
-
`${path.basename(filePath)} should not directly import @angular/*`,
|
|
64
|
-
).toBe(false);
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("all Angular API access goes through angular-compiler.ts adapter", () => {
|
|
69
|
-
|
|
70
|
-
// Angular API 접근은 ngtsc-build-core.ts → angular-compiler.ts 또는 SdTsCompiler → angular-compiler.ts로 이루어진다
|
|
71
|
-
const compilerFile = path.resolve(
|
|
72
|
-
import.meta.dirname,
|
|
73
|
-
"../../src/ts-compiler/SdTsCompiler.ts",
|
|
74
|
-
);
|
|
75
|
-
const content = fs.readFileSync(compilerFile, "utf-8");
|
|
76
|
-
|
|
77
|
-
expect(content).toContain("angular-compiler");
|
|
78
|
-
});
|
|
79
|
-
});
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { describe, it } from "vitest";
|
|
2
|
-
import { SignalHandler } from "../../src/runtime/SignalHandler";
|
|
3
|
-
|
|
4
|
-
describe("SignalHandler", () => {
|
|
5
|
-
it("resolves waitForTermination on requestTermination", async () => {
|
|
6
|
-
const handler = new SignalHandler();
|
|
7
|
-
|
|
8
|
-
handler.requestTermination();
|
|
9
|
-
|
|
10
|
-
await handler.waitForTermination();
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it("handles double requestTermination gracefully", async () => {
|
|
14
|
-
const handler = new SignalHandler();
|
|
15
|
-
|
|
16
|
-
handler.requestTermination();
|
|
17
|
-
handler.requestTermination();
|
|
18
|
-
|
|
19
|
-
await handler.waitForTermination();
|
|
20
|
-
});
|
|
21
|
-
});
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeAll } from "vitest";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import * as angularBuildMod from "../../src/angular/angular-build";
|
|
5
|
-
|
|
6
|
-
describe("Angular Build Adapter - Library Compilation API", () => {
|
|
7
|
-
// Rule: 어댑터는 Library 빌드에 필요한 Angular 컴파일러 API를 노출한다
|
|
8
|
-
|
|
9
|
-
it("exports NgtscProgram from adapter module", () => {
|
|
10
|
-
expect(typeof angularBuildMod.NgtscProgram).toBe("function");
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it("exports OptimizeFor enum from adapter module", () => {
|
|
14
|
-
expect(angularBuildMod.OptimizeFor.WholeProgram).toBeDefined();
|
|
15
|
-
expect(angularBuildMod.OptimizeFor.SingleFile).toBeDefined();
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it("NgtscProgram이 AngularLibraryHostExtensions로 확장된 ts.CompilerHost를 수용", () => {
|
|
19
|
-
expect(typeof angularBuildMod.NgtscProgram).toBe("function");
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
describe("Angular Build Adapter - Scope after Feature 3.1", () => {
|
|
24
|
-
it("adapter only exports Library-path APIs (NgtscProgram, OptimizeFor)", () => {
|
|
25
|
-
const exports = Object.keys(angularBuildMod);
|
|
26
|
-
|
|
27
|
-
// Library-path exports retained
|
|
28
|
-
expect(exports).toContain("NgtscProgram");
|
|
29
|
-
expect(exports).toContain("OptimizeFor");
|
|
30
|
-
|
|
31
|
-
// Client-path exports removed (moved to angular-facade.ts)
|
|
32
|
-
expect(exports).not.toContain("buildApplicationInternal");
|
|
33
|
-
expect(exports).not.toContain("serveWithVite");
|
|
34
|
-
expect(exports).not.toContain("normalizeDevServerOptions");
|
|
35
|
-
expect(exports).not.toContain("createAngularBuilderContext");
|
|
36
|
-
expect(exports).not.toContain("ResultKind");
|
|
37
|
-
expect(exports).not.toContain("IndexHtmlGenerator");
|
|
38
|
-
expect(exports).not.toContain("checkPort");
|
|
39
|
-
expect(exports).not.toContain("emitFilesToDisk");
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("adapter does not export browserslist/PostCSS APIs", () => {
|
|
43
|
-
const exports = Object.keys(angularBuildMod);
|
|
44
|
-
expect(exports).not.toContain("transformSupportedBrowsersToTargets");
|
|
45
|
-
expect(exports).not.toContain("getSupportedBrowsers");
|
|
46
|
-
expect(exports).not.toContain("loadPostcssConfiguration");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("adapter does not export ServiceWorker/i18n APIs", () => {
|
|
50
|
-
const exports = Object.keys(angularBuildMod);
|
|
51
|
-
expect(exports).not.toContain("augmentAppWithServiceWorker");
|
|
52
|
-
expect(exports).not.toContain("createI18nOptions");
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
describe("Angular Build Adapter - Isolation", () => {
|
|
57
|
-
const sdCliSrcDir = path.resolve(import.meta.dirname, "../../src");
|
|
58
|
-
const adapterFiles = ["angular-build.ts", "vite-angular-plugin.ts", "esbuild-angular-compiler-plugin.ts", "esbuild-client-config.ts", "esbuild-index-html.ts", "esbuild-pwa.ts"];
|
|
59
|
-
let srcFiles: string[];
|
|
60
|
-
|
|
61
|
-
beforeAll(() => {
|
|
62
|
-
const collectFiles = (dir: string): string[] => {
|
|
63
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
64
|
-
return entries.flatMap((e) =>
|
|
65
|
-
e.isDirectory()
|
|
66
|
-
? collectFiles(path.join(dir, e.name))
|
|
67
|
-
: e.name.endsWith(".ts")
|
|
68
|
-
? [path.join(dir, e.name)]
|
|
69
|
-
: [],
|
|
70
|
-
);
|
|
71
|
-
};
|
|
72
|
-
srcFiles = collectFiles(sdCliSrcDir).filter(
|
|
73
|
-
(f) => !adapterFiles.some((a) => f.endsWith(a)),
|
|
74
|
-
);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
// Rule: Library 경로 — angular-build.ts 통해서만 @angular/compiler-cli 접근
|
|
78
|
-
it("no src file outside adapters imports @angular/compiler-cli", () => {
|
|
79
|
-
for (const file of srcFiles) {
|
|
80
|
-
const content = fs.readFileSync(file, "utf-8");
|
|
81
|
-
expect(
|
|
82
|
-
content.includes("@angular/compiler-cli"),
|
|
83
|
-
`${path.relative(sdCliSrcDir, file)} imports @angular/compiler-cli directly`,
|
|
84
|
-
).toBe(false);
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
// Rule: Client 경로 — vite-angular-plugin.ts 통해서만 @angular/build/private 접근 (JavaScriptTransformer)
|
|
89
|
-
it("no src file outside adapters imports @angular/build/private", () => {
|
|
90
|
-
for (const file of srcFiles) {
|
|
91
|
-
const content = fs.readFileSync(file, "utf-8");
|
|
92
|
-
expect(
|
|
93
|
-
content.includes("@angular/build/private"),
|
|
94
|
-
`${path.relative(sdCliSrcDir, file)} imports @angular/build/private directly`,
|
|
95
|
-
).toBe(false);
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
// Rule: @angular/compiler-cli는 sd-cli의 직접 의존성이다
|
|
100
|
-
it("package.json includes @angular/compiler-cli in dependencies", () => {
|
|
101
|
-
const pkgJson = JSON.parse(
|
|
102
|
-
fs.readFileSync(
|
|
103
|
-
path.resolve(import.meta.dirname, "../../package.json"),
|
|
104
|
-
"utf-8",
|
|
105
|
-
),
|
|
106
|
-
);
|
|
107
|
-
expect(pkgJson.dependencies["@angular/compiler-cli"]).toBeDefined();
|
|
108
|
-
});
|
|
109
|
-
});
|
|
@@ -1,438 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
import path from "path";
|
|
3
|
-
|
|
4
|
-
// 외부 npm + Node 표준 모듈은 ESM namespace immutable이라 vi.mock 유지
|
|
5
|
-
const mockContext = {
|
|
6
|
-
rebuild: vi.fn(),
|
|
7
|
-
watch: vi.fn(),
|
|
8
|
-
dispose: vi.fn(),
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
vi.mock("esbuild", () => ({
|
|
12
|
-
default: {
|
|
13
|
-
context: vi.fn(() => Promise.resolve(mockContext)),
|
|
14
|
-
},
|
|
15
|
-
}));
|
|
16
|
-
|
|
17
|
-
vi.mock("browserslist-to-esbuild", () => ({
|
|
18
|
-
default: vi.fn(() => ["chrome61"]),
|
|
19
|
-
}));
|
|
20
|
-
|
|
21
|
-
vi.mock("module", async (importOriginal) => {
|
|
22
|
-
const actual = await importOriginal<typeof import("module")>();
|
|
23
|
-
return {
|
|
24
|
-
...actual,
|
|
25
|
-
createRequire: vi.fn(() => (name: string) => {
|
|
26
|
-
if (name === "nonexistent-plugin") {
|
|
27
|
-
throw new Error(`Cannot find module '${name}'`);
|
|
28
|
-
}
|
|
29
|
-
return (..._args: any[]) => ({ postcssPlugin: name });
|
|
30
|
-
}),
|
|
31
|
-
};
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
import * as angularPluginMod from "../../src/esbuild/esbuild-angular-compiler-plugin";
|
|
35
|
-
import * as transformStylesheetMod from "../../src/angular/client-transform-stylesheet";
|
|
36
|
-
|
|
37
|
-
const mockAngularPlugin = { name: "sd-angular-compiler" };
|
|
38
|
-
const mockTransformStylesheet = vi.fn();
|
|
39
|
-
|
|
40
|
-
vi.spyOn(angularPluginMod, "createAngularCompilerPlugin").mockReturnValue(mockAngularPlugin as any);
|
|
41
|
-
vi.spyOn(transformStylesheetMod, "createClientTransformStylesheet").mockReturnValue(mockTransformStylesheet as any);
|
|
42
|
-
|
|
43
|
-
const { createClientEsbuildContext, ClientSourceFileCache } = await import(
|
|
44
|
-
"../../src/esbuild/esbuild-client-config"
|
|
45
|
-
);
|
|
46
|
-
const esbuild = (await import("esbuild")).default;
|
|
47
|
-
const { createAngularCompilerPlugin } = await import(
|
|
48
|
-
"../../src/esbuild/esbuild-angular-compiler-plugin"
|
|
49
|
-
);
|
|
50
|
-
const { createClientTransformStylesheet } = await import(
|
|
51
|
-
"../../src/angular/client-transform-stylesheet"
|
|
52
|
-
);
|
|
53
|
-
const browserslistToEsbuild = (await import("browserslist-to-esbuild")).default;
|
|
54
|
-
|
|
55
|
-
describe("createClientEsbuildContext — Acceptance", () => {
|
|
56
|
-
beforeEach(() => {
|
|
57
|
-
vi.clearAllMocks();
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
// Scenario: Angular main.ts를 ESM 번들로 빌드
|
|
61
|
-
// + AngularCompilerPluginOptions로 플러그인 생성
|
|
62
|
-
// + ClientSourceFileCache로 증분 캐시
|
|
63
|
-
// + dev 모드 Angular 플래그 + 소스맵
|
|
64
|
-
it("dev 모드: ESM 번들 설정, Angular 플래그, 소스맵, ClientSourceFileCache로 esbuild context 생성", async () => {
|
|
65
|
-
const result = await createClientEsbuildContext({
|
|
66
|
-
pkgDir: "/workspace/packages/my-app",
|
|
67
|
-
cwd: "/workspace",
|
|
68
|
-
mode: "dev",
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
// esbuild.context가 호출됨
|
|
72
|
-
expect(esbuild.context).toHaveBeenCalledOnce();
|
|
73
|
-
const esbuildOptions = vi.mocked(esbuild.context).mock.calls[0][0];
|
|
74
|
-
|
|
75
|
-
// ESM 번들 설정
|
|
76
|
-
expect(esbuildOptions.entryPoints).toEqual([
|
|
77
|
-
path.join("/workspace/packages/my-app", "src", "main.ts"),
|
|
78
|
-
]);
|
|
79
|
-
expect(esbuildOptions.bundle).toBe(true);
|
|
80
|
-
expect(esbuildOptions.splitting).toBe(true);
|
|
81
|
-
expect(esbuildOptions.format).toBe("esm");
|
|
82
|
-
expect(esbuildOptions.platform).toBe("browser");
|
|
83
|
-
expect(esbuildOptions.outdir).toBe(
|
|
84
|
-
path.join("/workspace/packages/my-app", "dist"),
|
|
85
|
-
);
|
|
86
|
-
expect(esbuildOptions.metafile).toBe(true);
|
|
87
|
-
expect(esbuildOptions.write).toBe(true);
|
|
88
|
-
|
|
89
|
-
// dev 소스맵: linked
|
|
90
|
-
expect(esbuildOptions.sourcemap).toBe("linked");
|
|
91
|
-
|
|
92
|
-
// Angular 플래그 (dev)
|
|
93
|
-
expect(esbuildOptions.define).toBeDefined();
|
|
94
|
-
expect(esbuildOptions.define!["ngJitMode"]).toBe("false");
|
|
95
|
-
expect(esbuildOptions.define!["ngDevMode"]).toBeUndefined();
|
|
96
|
-
expect(esbuildOptions.define!["ngHmrMode"]).toBeUndefined();
|
|
97
|
-
|
|
98
|
-
// ClientSourceFileCache 인스턴스
|
|
99
|
-
expect(result.sourceFileCache).toBeInstanceOf(ClientSourceFileCache);
|
|
100
|
-
|
|
101
|
-
// createAngularCompilerPlugin 호출 검증
|
|
102
|
-
expect(createAngularCompilerPlugin).toHaveBeenCalledOnce();
|
|
103
|
-
const pluginOpts = vi.mocked(createAngularCompilerPlugin).mock.calls[0][0];
|
|
104
|
-
|
|
105
|
-
// AngularCompilerPluginOptions
|
|
106
|
-
expect(pluginOpts.tsconfig).toBe(
|
|
107
|
-
path.join("/workspace/packages/my-app", "tsconfig.json"),
|
|
108
|
-
);
|
|
109
|
-
expect(pluginOpts.sourcemap).toBe(true);
|
|
110
|
-
expect(pluginOpts.advancedOptimizations).toBe(false);
|
|
111
|
-
expect(pluginOpts.thirdPartySourcemaps).toBe(true);
|
|
112
|
-
expect(pluginOpts.incremental).toBe(true);
|
|
113
|
-
expect(pluginOpts.sourceFileCache).toBe(result.sourceFileCache);
|
|
114
|
-
expect(pluginOpts.typeScriptFileCache).toBe(result.sourceFileCache.typeScriptFileCache);
|
|
115
|
-
expect(pluginOpts.loadResultCache).toBe(result.sourceFileCache.loadResultCache);
|
|
116
|
-
expect(pluginOpts.includeTestMetadata).toBe(true);
|
|
117
|
-
expect(pluginOpts.persistentCachePath).toBe(
|
|
118
|
-
path.join("/workspace/packages/my-app", ".angular", "cache"),
|
|
119
|
-
);
|
|
120
|
-
|
|
121
|
-
// transformStylesheet 콜백 전달
|
|
122
|
-
expect(pluginOpts.transformStylesheet).toBe(mockTransformStylesheet);
|
|
123
|
-
expect(createClientTransformStylesheet).toHaveBeenCalledOnce();
|
|
124
|
-
|
|
125
|
-
// 반환값
|
|
126
|
-
expect(result.context).toBe(mockContext);
|
|
127
|
-
|
|
128
|
-
// angularPlugin이 plugins에 포함됨
|
|
129
|
-
expect(esbuildOptions.plugins).toContainEqual(mockAngularPlugin);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
// Scenario: dev 모드 + templateUpdates + non-legacy → ngHmrMode가 "true"
|
|
133
|
-
it("dev 모드 + templateUpdates + non-legacy: ngHmrMode가 true로 정의된다", async () => {
|
|
134
|
-
await createClientEsbuildContext({
|
|
135
|
-
pkgDir: "/workspace/packages/my-app",
|
|
136
|
-
cwd: "/workspace",
|
|
137
|
-
mode: "dev",
|
|
138
|
-
templateUpdates: new Map<string, string>(),
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
const esbuildOptions = vi.mocked(esbuild.context).mock.calls[0][0];
|
|
142
|
-
expect(esbuildOptions.define!["ngHmrMode"]).toBe("true");
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
// Scenario: build 모드 Angular 플래그 + 소스맵
|
|
146
|
-
it("build 모드: Angular 플래그 모두 false, 소스맵 비활성화, advancedOptimizations", async () => {
|
|
147
|
-
await createClientEsbuildContext({
|
|
148
|
-
pkgDir: "/workspace/packages/my-app",
|
|
149
|
-
cwd: "/workspace",
|
|
150
|
-
mode: "build",
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
const esbuildOptions = vi.mocked(esbuild.context).mock.calls[0][0];
|
|
154
|
-
|
|
155
|
-
// build 소스맵: false
|
|
156
|
-
expect(esbuildOptions.sourcemap).toBe(false);
|
|
157
|
-
|
|
158
|
-
// Angular 플래그 (build)
|
|
159
|
-
expect(esbuildOptions.define!["ngDevMode"]).toBe("false");
|
|
160
|
-
expect(esbuildOptions.define!["ngJitMode"]).toBe("false");
|
|
161
|
-
expect(esbuildOptions.define!["ngHmrMode"]).toBe("false");
|
|
162
|
-
|
|
163
|
-
// AngularCompilerPluginOptions
|
|
164
|
-
const pluginOpts = vi.mocked(createAngularCompilerPlugin).mock.calls[0][0];
|
|
165
|
-
expect(pluginOpts.sourcemap).toBe(false);
|
|
166
|
-
expect(pluginOpts.advancedOptimizations).toBe(true);
|
|
167
|
-
expect(pluginOpts.thirdPartySourcemaps).toBe(false);
|
|
168
|
-
expect(pluginOpts.incremental).toBe(false);
|
|
169
|
-
expect(pluginOpts.includeTestMetadata).toBe(false);
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
// Scenario: 커스텀 env 주입
|
|
173
|
-
it("env 설정 시 import.meta.env 객체로 define에 주입", async () => {
|
|
174
|
-
await createClientEsbuildContext({
|
|
175
|
-
pkgDir: "/workspace/packages/my-app",
|
|
176
|
-
cwd: "/workspace",
|
|
177
|
-
mode: "build",
|
|
178
|
-
env: { API_URL: "https://api.example.com", DEBUG: "true" },
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
const esbuildOptions = vi.mocked(esbuild.context).mock.calls[0][0];
|
|
182
|
-
expect(esbuildOptions.define!["import.meta.env"]).toBe(
|
|
183
|
-
JSON.stringify({ API_URL: "https://api.example.com", DEBUG: "true" }),
|
|
184
|
-
);
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
// Scenario: PostCSS 설정 — sd-postcss 등록 + transformStylesheet에 postcssPlugins 전달
|
|
188
|
-
it("postcssPlugins 전달 시 sd-postcss 플러그인이 등록되고 transformStylesheet에도 전달된다", async () => {
|
|
189
|
-
await createClientEsbuildContext({
|
|
190
|
-
pkgDir: "/workspace/packages/my-app",
|
|
191
|
-
cwd: "/workspace",
|
|
192
|
-
mode: "build",
|
|
193
|
-
postcssPlugins: [["autoprefixer", {}]],
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
const esbuildOptions = vi.mocked(esbuild.context).mock.calls[0][0];
|
|
197
|
-
const pluginNames = esbuildOptions.plugins!.map((p: any) => p.name);
|
|
198
|
-
expect(pluginNames).toContain("sd-postcss");
|
|
199
|
-
|
|
200
|
-
const transformOpts = vi.mocked(createClientTransformStylesheet).mock.calls[0][0];
|
|
201
|
-
expect(transformOpts.postcssPlugins).toBeDefined();
|
|
202
|
-
expect(transformOpts.postcssPlugins).toHaveLength(1);
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
// Scenario: 프로덕션 일회성 빌드
|
|
206
|
-
it("context.rebuild() 호출 후 결과 반환, context.dispose()로 정리", async () => {
|
|
207
|
-
const mockBuildResult = {
|
|
208
|
-
metafile: { outputs: { "dist/main.js": {} } },
|
|
209
|
-
errors: [],
|
|
210
|
-
warnings: [],
|
|
211
|
-
};
|
|
212
|
-
mockContext.rebuild.mockResolvedValueOnce(mockBuildResult);
|
|
213
|
-
|
|
214
|
-
const { context } = await createClientEsbuildContext({
|
|
215
|
-
pkgDir: "/workspace/packages/my-app",
|
|
216
|
-
cwd: "/workspace",
|
|
217
|
-
mode: "build",
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
const buildResult = await context.rebuild();
|
|
221
|
-
expect(buildResult.metafile).toBeDefined();
|
|
222
|
-
|
|
223
|
-
await context.dispose();
|
|
224
|
-
expect(mockContext.dispose).toHaveBeenCalledOnce();
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
// Scenario: watch 모드 증분 빌드 — onEnd 콜백으로 빌드 결과 수신
|
|
228
|
-
it("onEnd 콜백이 sd-on-end 플러그인으로 등록되고, context.watch()로 watch 시작 가능", async () => {
|
|
229
|
-
const onEndSpy = vi.fn();
|
|
230
|
-
|
|
231
|
-
const { context } = await createClientEsbuildContext({
|
|
232
|
-
pkgDir: "/workspace/packages/my-app",
|
|
233
|
-
cwd: "/workspace",
|
|
234
|
-
mode: "dev",
|
|
235
|
-
onEnd: onEndSpy,
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
// sd-on-end 플러그인이 등록됨
|
|
239
|
-
const esbuildOptions = vi.mocked(esbuild.context).mock.calls[0][0];
|
|
240
|
-
const onEndPlugin = esbuildOptions.plugins!.find(
|
|
241
|
-
(p: any) => p.name === "sd-on-end",
|
|
242
|
-
);
|
|
243
|
-
expect(onEndPlugin).toBeDefined();
|
|
244
|
-
|
|
245
|
-
// watch 시작 가능
|
|
246
|
-
await context.watch();
|
|
247
|
-
expect(mockContext.watch).toHaveBeenCalledOnce();
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
// Scenario: async onEnd 콜백의 Promise가 sd-on-end 플러그인을 통해 esbuild에 전달된다
|
|
251
|
-
it("async onEnd 콜백의 Promise가 sd-on-end 플러그인을 통해 esbuild에 전달된다", async () => {
|
|
252
|
-
const asyncOnEnd = vi.fn().mockResolvedValue(undefined);
|
|
253
|
-
await createClientEsbuildContext({
|
|
254
|
-
pkgDir: "/workspace/packages/my-app",
|
|
255
|
-
cwd: "/workspace",
|
|
256
|
-
mode: "dev",
|
|
257
|
-
onEnd: asyncOnEnd,
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
const esbuildOptions = vi.mocked(esbuild.context).mock.calls[0][0];
|
|
261
|
-
const sdOnEndPlugin = esbuildOptions.plugins!.find(
|
|
262
|
-
(p: any) => p.name === "sd-on-end",
|
|
263
|
-
)!;
|
|
264
|
-
|
|
265
|
-
let capturedCallback!: (result: any) => any;
|
|
266
|
-
(sdOnEndPlugin as any).setup({
|
|
267
|
-
onEnd(cb: (result: any) => any) {
|
|
268
|
-
capturedCallback = cb;
|
|
269
|
-
},
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
const fakeResult = { errors: [], warnings: [] };
|
|
273
|
-
const returnValue = capturedCallback(fakeResult);
|
|
274
|
-
|
|
275
|
-
expect(returnValue).toBeInstanceOf(Promise);
|
|
276
|
-
await returnValue;
|
|
277
|
-
expect(asyncOnEnd).toHaveBeenCalledWith(fakeResult);
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
// onEnd가 없으면 sd-on-end 플러그인 미등록
|
|
281
|
-
it("onEnd 미전달 시 sd-on-end 플러그인이 없음", async () => {
|
|
282
|
-
await createClientEsbuildContext({
|
|
283
|
-
pkgDir: "/workspace/packages/my-app",
|
|
284
|
-
cwd: "/workspace",
|
|
285
|
-
mode: "dev",
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
const esbuildOptions = vi.mocked(esbuild.context).mock.calls[0][0];
|
|
289
|
-
const onEndPlugin = esbuildOptions.plugins!.find(
|
|
290
|
-
(p: any) => p.name === "sd-on-end",
|
|
291
|
-
);
|
|
292
|
-
expect(onEndPlugin).toBeUndefined();
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
// Scenario: browserslist 미설정 시 기본 target "es2022"
|
|
296
|
-
it("browserslist 미설정 시 esbuild target이 [es2022]", async () => {
|
|
297
|
-
await createClientEsbuildContext({
|
|
298
|
-
pkgDir: "/workspace/packages/my-app",
|
|
299
|
-
cwd: "/workspace",
|
|
300
|
-
mode: "build",
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
const esbuildOptions = vi.mocked(esbuild.context).mock.calls[0][0];
|
|
304
|
-
expect(esbuildOptions.target).toEqual(["es2022"]);
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
// Scenario: browserslist 문자열 설정 시 변환
|
|
308
|
-
it("browserslist 문자열 설정 시 browserslistToEsbuild 결과가 target에 적용", async () => {
|
|
309
|
-
vi.mocked(browserslistToEsbuild).mockReturnValueOnce(["chrome61"]);
|
|
310
|
-
|
|
311
|
-
await createClientEsbuildContext({
|
|
312
|
-
pkgDir: "/workspace/packages/my-app",
|
|
313
|
-
cwd: "/workspace",
|
|
314
|
-
mode: "build",
|
|
315
|
-
browserslist: "Chrome 61",
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
expect(browserslistToEsbuild).toHaveBeenCalledWith(["Chrome 61"]);
|
|
319
|
-
|
|
320
|
-
const esbuildOptions = vi.mocked(esbuild.context).mock.calls[0][0];
|
|
321
|
-
expect(esbuildOptions.target).toEqual(["chrome61"]);
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
// Scenario: polyfills 경로 전달 시 entryPoints에 추가
|
|
325
|
-
it("polyfills 전달 시 entryPoints에 main.ts와 함께 절대경로로 추가", async () => {
|
|
326
|
-
await createClientEsbuildContext({
|
|
327
|
-
pkgDir: "/workspace/packages/my-app",
|
|
328
|
-
cwd: "/workspace",
|
|
329
|
-
mode: "build",
|
|
330
|
-
polyfills: ["src/polyfills.ts"],
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
const esbuildOptions = vi.mocked(esbuild.context).mock.calls[0][0];
|
|
334
|
-
expect(esbuildOptions.entryPoints).toEqual([
|
|
335
|
-
path.join("/workspace/packages/my-app", "src", "main.ts"),
|
|
336
|
-
path.join("/workspace/packages/my-app", "src/polyfills.ts"),
|
|
337
|
-
]);
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
// Scenario: polyfills 미전달 시 entryPoints 변경 없음
|
|
341
|
-
it("polyfills 미전달 시 entryPoints는 main.ts만 포함", async () => {
|
|
342
|
-
await createClientEsbuildContext({
|
|
343
|
-
pkgDir: "/workspace/packages/my-app",
|
|
344
|
-
cwd: "/workspace",
|
|
345
|
-
mode: "build",
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
const esbuildOptions = vi.mocked(esbuild.context).mock.calls[0][0];
|
|
349
|
-
expect(esbuildOptions.entryPoints).toEqual([
|
|
350
|
-
path.join("/workspace/packages/my-app", "src", "main.ts"),
|
|
351
|
-
]);
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
// Scenario: dev 모드 출력 네이밍 (entry/asset은 해시 없음, chunk은 해시 포함)
|
|
355
|
-
it("dev 모드: entryNames, assetNames는 [name], chunkNames는 [name]-[hash]", async () => {
|
|
356
|
-
await createClientEsbuildContext({
|
|
357
|
-
pkgDir: "/workspace/packages/my-app",
|
|
358
|
-
cwd: "/workspace",
|
|
359
|
-
mode: "dev",
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
const esbuildOptions = vi.mocked(esbuild.context).mock.calls[0][0];
|
|
363
|
-
expect(esbuildOptions.entryNames).toBe("[name]");
|
|
364
|
-
expect(esbuildOptions.chunkNames).toBe("[name]-[hash]");
|
|
365
|
-
expect(esbuildOptions.assetNames).toBe("[name]");
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
// Scenario: build 모드 출력 네이밍 (해시 포함)
|
|
369
|
-
it("build 모드: entryNames, chunkNames, assetNames 모두 [name]-[hash]", async () => {
|
|
370
|
-
await createClientEsbuildContext({
|
|
371
|
-
pkgDir: "/workspace/packages/my-app",
|
|
372
|
-
cwd: "/workspace",
|
|
373
|
-
mode: "build",
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
const esbuildOptions = vi.mocked(esbuild.context).mock.calls[0][0];
|
|
377
|
-
expect(esbuildOptions.entryNames).toBe("[name]-[hash]");
|
|
378
|
-
expect(esbuildOptions.chunkNames).toBe("[name]-[hash]");
|
|
379
|
-
expect(esbuildOptions.assetNames).toBe("[name]-[hash]");
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
// Scenario: browserslist 배열 설정 시 변환
|
|
383
|
-
it("browserslist 배열 설정 시 browserslistToEsbuild 결과가 target에 적용", async () => {
|
|
384
|
-
vi.mocked(browserslistToEsbuild).mockReturnValueOnce(["chrome61", "firefox60"]);
|
|
385
|
-
|
|
386
|
-
await createClientEsbuildContext({
|
|
387
|
-
pkgDir: "/workspace/packages/my-app",
|
|
388
|
-
cwd: "/workspace",
|
|
389
|
-
mode: "build",
|
|
390
|
-
browserslist: ["Chrome 61", "Firefox 60"],
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
expect(browserslistToEsbuild).toHaveBeenCalledWith(["Chrome 61", "Firefox 60"]);
|
|
394
|
-
|
|
395
|
-
const esbuildOptions = vi.mocked(esbuild.context).mock.calls[0][0];
|
|
396
|
-
expect(esbuildOptions.target).toEqual(["chrome61", "firefox60"]);
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
// Scenario: esbuild context에 tsconfig 옵션이 전달된다
|
|
400
|
-
it("esbuild context에 tsconfig 옵션이 기본값으로 pkgDir/tsconfig.json 전달된다", async () => {
|
|
401
|
-
await createClientEsbuildContext({
|
|
402
|
-
pkgDir: "/workspace/packages/my-app",
|
|
403
|
-
cwd: "/workspace",
|
|
404
|
-
mode: "dev",
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
const esbuildOptions = vi.mocked(esbuild.context).mock.calls[0][0];
|
|
408
|
-
expect(esbuildOptions.tsconfig).toBe(
|
|
409
|
-
path.join("/workspace/packages/my-app", "tsconfig.json"),
|
|
410
|
-
);
|
|
411
|
-
});
|
|
412
|
-
|
|
413
|
-
// Scenario: customPlugins가 angularPlugin 이전에 배치된다 (onStart에서 sourceFileCache 무효화 선행 필요)
|
|
414
|
-
it("plugins 배열이 [customPlugins, angularPlugin, scssPlugin, onEndPlugin] 순서로 구성된다", async () => {
|
|
415
|
-
const customPlugin = { name: "custom", setup: vi.fn() };
|
|
416
|
-
await createClientEsbuildContext({
|
|
417
|
-
pkgDir: "/workspace/packages/my-app",
|
|
418
|
-
cwd: "/workspace",
|
|
419
|
-
mode: "dev",
|
|
420
|
-
plugins: [customPlugin],
|
|
421
|
-
onEnd: vi.fn(),
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
const esbuildOptions = vi.mocked(esbuild.context).mock.calls[0][0];
|
|
425
|
-
const pluginNames = esbuildOptions.plugins!.map((p: any) => p.name);
|
|
426
|
-
|
|
427
|
-
expect(pluginNames).toContain("custom");
|
|
428
|
-
expect(pluginNames).toContain("sd-angular-compiler");
|
|
429
|
-
expect(pluginNames).toContain("sd-scss");
|
|
430
|
-
expect(pluginNames[pluginNames.length - 1]).toBe("sd-on-end");
|
|
431
|
-
|
|
432
|
-
const customIdx = pluginNames.indexOf("custom");
|
|
433
|
-
const angularIdx = pluginNames.indexOf("sd-angular-compiler");
|
|
434
|
-
const scssIdx = pluginNames.indexOf("sd-scss");
|
|
435
|
-
expect(customIdx).toBeLessThan(angularIdx);
|
|
436
|
-
expect(angularIdx).toBeLessThan(scssIdx);
|
|
437
|
-
});
|
|
438
|
-
});
|