@simplysm/sd-cli 14.0.38 → 14.0.40
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/angular-build-pipeline.d.ts +1 -1
- package/dist/angular/angular-build-pipeline.js +1 -1
- package/dist/angular/client-transform-stylesheet.d.ts +1 -1
- package/dist/angular/client-transform-stylesheet.js +3 -3
- package/dist/dev-server/hmr-client-script.d.ts +2 -2
- package/dist/dev-server/hmr-client-script.d.ts.map +1 -1
- package/dist/dev-server/hmr-client-script.js +4 -4
- package/dist/dev-server/hmr-client-script.js.map +1 -1
- package/dist/esbuild/esbuild-client-config.d.ts +0 -2
- package/dist/esbuild/esbuild-client-config.d.ts.map +1 -1
- package/dist/esbuild/esbuild-client-config.js +20 -10
- package/dist/esbuild/esbuild-client-config.js.map +1 -1
- package/dist/esbuild/esbuild-postcss-plugin.d.ts +8 -0
- package/dist/esbuild/esbuild-postcss-plugin.d.ts.map +1 -0
- package/dist/esbuild/esbuild-postcss-plugin.js +105 -0
- package/dist/esbuild/esbuild-postcss-plugin.js.map +1 -0
- package/dist/esbuild/esbuild-tsc-plugin.d.ts +23 -0
- package/dist/esbuild/esbuild-tsc-plugin.d.ts.map +1 -0
- package/dist/esbuild/esbuild-tsc-plugin.js +60 -0
- package/dist/esbuild/esbuild-tsc-plugin.js.map +1 -0
- package/dist/workers/client.worker.d.ts.map +1 -1
- package/dist/workers/client.worker.js +94 -26
- package/dist/workers/client.worker.js.map +1 -1
- package/dist/workers/server-build.worker.d.ts.map +1 -1
- package/dist/workers/server-build.worker.js +129 -90
- package/dist/workers/server-build.worker.js.map +1 -1
- package/dist/workers/server-esbuild-context.d.ts +27 -0
- package/dist/workers/server-esbuild-context.d.ts.map +1 -1
- package/dist/workers/server-esbuild-context.js +57 -4
- package/dist/workers/server-esbuild-context.js.map +1 -1
- package/package.json +6 -4
- package/src/angular/angular-build-pipeline.ts +2 -2
- package/src/angular/client-transform-stylesheet.ts +4 -4
- package/src/dev-server/hmr-client-script.ts +4 -4
- package/src/esbuild/esbuild-client-config.ts +22 -13
- package/src/esbuild/esbuild-postcss-plugin.ts +117 -0
- package/src/esbuild/esbuild-tsc-plugin.ts +83 -0
- package/src/workers/client.worker.ts +96 -29
- package/src/workers/server-build.worker.ts +136 -97
- package/src/workers/server-esbuild-context.ts +72 -4
- package/tests/angular/client-transform-stylesheet.spec.ts +1 -1
- package/tests/esbuild/esbuild-tsc-plugin.acc.spec.ts +349 -0
- package/tests/esbuild/esbuild-tsc-plugin.spec.ts +230 -0
- package/tests/utils/esbuild-client-config-postcss.verify.md +6 -0
- package/tests/utils/esbuild-client-config.acc.spec.ts +34 -20
- package/tests/utils/esbuild-client-config.spec.ts +79 -16
- package/tests/utils/esbuild-postcss-plugin.acc.spec.ts +299 -0
- package/tests/utils/esbuild-postcss-plugin.spec.ts +290 -0
- package/tests/utils/esbuild-scss-plugin.acc.spec.ts +1 -0
- package/tests/utils/hmr-client-script.acc.spec.ts +8 -8
- package/tests/utils/hmr-client-script.spec.ts +5 -5
- package/tests/workers/server-build-lint.spec.ts +43 -0
- package/tests/workers/server-build-worker-refactoring.verify.md +14 -0
- package/tests/workers/server-build-worker.spec.ts +122 -9
- package/tests/workers/server-esbuild-context-tsc.verify.md +7 -0
- package/tests/workers/server-esbuild-context.acc.spec.ts +188 -2
- package/tests/workers/server-esbuild-context.spec.ts +401 -2
|
@@ -37,6 +37,19 @@ vi.mock("browserslist-to-esbuild", () => ({
|
|
|
37
37
|
default: vi.fn(() => ["chrome61"]),
|
|
38
38
|
}));
|
|
39
39
|
|
|
40
|
+
vi.mock("module", async (importOriginal) => {
|
|
41
|
+
const actual = await importOriginal<typeof import("module")>();
|
|
42
|
+
return {
|
|
43
|
+
...actual,
|
|
44
|
+
createRequire: vi.fn(() => (name: string) => {
|
|
45
|
+
if (name === "nonexistent-plugin") {
|
|
46
|
+
throw new Error(`Cannot find module '${name}'`);
|
|
47
|
+
}
|
|
48
|
+
return (..._args: any[]) => ({ postcssPlugin: name });
|
|
49
|
+
}),
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
|
|
40
53
|
// --- Imports (after mocks) ---
|
|
41
54
|
|
|
42
55
|
const { createClientEsbuildContext } = await import(
|
|
@@ -171,7 +184,7 @@ describe("createClientEsbuildContext — Acceptance", () => {
|
|
|
171
184
|
});
|
|
172
185
|
|
|
173
186
|
// Scenario: 커스텀 env 주입
|
|
174
|
-
it("env 설정 시 import.meta.env
|
|
187
|
+
it("env 설정 시 import.meta.env 객체로 define에 주입", async () => {
|
|
175
188
|
await createClientEsbuildContext({
|
|
176
189
|
pkgDir: "/workspace/packages/my-app",
|
|
177
190
|
cwd: "/workspace",
|
|
@@ -180,27 +193,26 @@ describe("createClientEsbuildContext — Acceptance", () => {
|
|
|
180
193
|
});
|
|
181
194
|
|
|
182
195
|
const esbuildOptions = vi.mocked(esbuild.context).mock.calls[0][0];
|
|
183
|
-
expect(esbuildOptions.define!["import.meta.env
|
|
184
|
-
|
|
196
|
+
expect(esbuildOptions.define!["import.meta.env"]).toBe(
|
|
197
|
+
JSON.stringify({ API_URL: "https://api.example.com", DEBUG: "true" }),
|
|
185
198
|
);
|
|
186
|
-
expect(esbuildOptions.define!["import.meta.env.DEBUG"]).toBe('"true"');
|
|
187
199
|
});
|
|
188
200
|
|
|
189
|
-
// Scenario: PostCSS 설정
|
|
190
|
-
it("postcssPlugins 전달 시
|
|
201
|
+
// Scenario: PostCSS 설정 — postcssConfiguration 비활성화 + sd-postcss 등록
|
|
202
|
+
it("postcssPlugins 전달 시 postcssConfiguration은 undefined이고 sd-postcss 플러그인이 등록된다", async () => {
|
|
191
203
|
await createClientEsbuildContext({
|
|
192
204
|
pkgDir: "/workspace/packages/my-app",
|
|
193
205
|
cwd: "/workspace",
|
|
194
206
|
mode: "build",
|
|
195
207
|
postcssPlugins: [["autoprefixer", {}]],
|
|
196
|
-
postcssConfigPath: "/workspace/packages/my-app",
|
|
197
208
|
});
|
|
198
209
|
|
|
199
210
|
const [, styleOpts] = vi.mocked(createCompilerPlugin).mock.calls[0];
|
|
200
|
-
expect(styleOpts.postcssConfiguration).
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
211
|
+
expect(styleOpts.postcssConfiguration).toBeUndefined();
|
|
212
|
+
|
|
213
|
+
const esbuildOptions = vi.mocked(esbuild.context).mock.calls[0][0];
|
|
214
|
+
const pluginNames = esbuildOptions.plugins!.map((p: any) => p.name);
|
|
215
|
+
expect(pluginNames).toContain("sd-postcss");
|
|
204
216
|
});
|
|
205
217
|
|
|
206
218
|
// Scenario: 프로덕션 일회성 빌드
|
|
@@ -358,8 +370,8 @@ describe("createClientEsbuildContext — Acceptance", () => {
|
|
|
358
370
|
]);
|
|
359
371
|
});
|
|
360
372
|
|
|
361
|
-
// Scenario: dev 모드 출력 네이밍 (해시
|
|
362
|
-
it("dev 모드: entryNames,
|
|
373
|
+
// Scenario: dev 모드 출력 네이밍 (entry/asset은 해시 없음, chunk은 해시 포함)
|
|
374
|
+
it("dev 모드: entryNames, assetNames는 [name], chunkNames는 [name]-[hash]", async () => {
|
|
363
375
|
await createClientEsbuildContext({
|
|
364
376
|
pkgDir: "/workspace/packages/my-app",
|
|
365
377
|
cwd: "/workspace",
|
|
@@ -368,7 +380,7 @@ describe("createClientEsbuildContext — Acceptance", () => {
|
|
|
368
380
|
|
|
369
381
|
const esbuildOptions = vi.mocked(esbuild.context).mock.calls[0][0];
|
|
370
382
|
expect(esbuildOptions.entryNames).toBe("[name]");
|
|
371
|
-
expect(esbuildOptions.chunkNames).toBe("[name]");
|
|
383
|
+
expect(esbuildOptions.chunkNames).toBe("[name]-[hash]");
|
|
372
384
|
expect(esbuildOptions.assetNames).toBe("[name]");
|
|
373
385
|
});
|
|
374
386
|
|
|
@@ -420,8 +432,8 @@ describe("createClientEsbuildContext — Acceptance", () => {
|
|
|
420
432
|
);
|
|
421
433
|
});
|
|
422
434
|
|
|
423
|
-
// Scenario:
|
|
424
|
-
it("plugins 배열이 [angularPlugin, scssPlugin,
|
|
435
|
+
// Scenario: customPlugins가 angularPlugin 이전에 배치된다 (onStart에서 sourceFileCache 무효화 선행 필요)
|
|
436
|
+
it("plugins 배열이 [customPlugins, angularPlugin, scssPlugin, onEndPlugin] 순서로 구성된다", async () => {
|
|
425
437
|
const customPlugin = { name: "custom", setup: vi.fn() };
|
|
426
438
|
await createClientEsbuildContext({
|
|
427
439
|
pkgDir: "/workspace/packages/my-app",
|
|
@@ -434,13 +446,15 @@ describe("createClientEsbuildContext — Acceptance", () => {
|
|
|
434
446
|
const esbuildOptions = vi.mocked(esbuild.context).mock.calls[0][0];
|
|
435
447
|
const pluginNames = esbuildOptions.plugins!.map((p: any) => p.name);
|
|
436
448
|
|
|
437
|
-
expect(pluginNames[0]).toBe("angular-compiler");
|
|
438
|
-
expect(pluginNames[1]).toBe("sd-scss");
|
|
439
449
|
expect(pluginNames).toContain("custom");
|
|
450
|
+
expect(pluginNames).toContain("angular-compiler");
|
|
451
|
+
expect(pluginNames).toContain("sd-scss");
|
|
440
452
|
expect(pluginNames[pluginNames.length - 1]).toBe("sd-on-end");
|
|
441
453
|
|
|
442
|
-
const scssIdx = pluginNames.indexOf("sd-scss");
|
|
443
454
|
const customIdx = pluginNames.indexOf("custom");
|
|
444
|
-
|
|
455
|
+
const angularIdx = pluginNames.indexOf("angular-compiler");
|
|
456
|
+
const scssIdx = pluginNames.indexOf("sd-scss");
|
|
457
|
+
expect(customIdx).toBeLessThan(angularIdx);
|
|
458
|
+
expect(angularIdx).toBeLessThan(scssIdx);
|
|
445
459
|
});
|
|
446
460
|
});
|
|
@@ -39,6 +39,19 @@ vi.mock("browserslist-to-esbuild", () => ({
|
|
|
39
39
|
default: vi.fn(() => ["chrome61"]),
|
|
40
40
|
}));
|
|
41
41
|
|
|
42
|
+
vi.mock("module", async (importOriginal) => {
|
|
43
|
+
const actual = await importOriginal<typeof import("module")>();
|
|
44
|
+
return {
|
|
45
|
+
...actual,
|
|
46
|
+
createRequire: vi.fn(() => (name: string) => {
|
|
47
|
+
if (name === "nonexistent-plugin") {
|
|
48
|
+
throw new Error(`Cannot find module '${name}'`);
|
|
49
|
+
}
|
|
50
|
+
return (..._args: any[]) => ({ postcssPlugin: name });
|
|
51
|
+
}),
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
|
|
42
55
|
// --- Imports (after mocks) ---
|
|
43
56
|
|
|
44
57
|
const { createClientEsbuildContext } = await import(
|
|
@@ -108,22 +121,19 @@ describe("createClientEsbuildContext — define 생성", () => {
|
|
|
108
121
|
expect(opts.define!["ngHmrMode"]).toBe("false");
|
|
109
122
|
});
|
|
110
123
|
|
|
111
|
-
it("env가 없으면 import.meta.env
|
|
124
|
+
it("env가 없으면 import.meta.env define 없음", async () => {
|
|
112
125
|
await createClientEsbuildContext(baseDev);
|
|
113
126
|
const opts = vi.mocked(esbuild.context).mock.calls[0][0];
|
|
114
|
-
|
|
115
|
-
k.startsWith("import.meta.env."),
|
|
116
|
-
);
|
|
117
|
-
expect(envKeys).toHaveLength(0);
|
|
127
|
+
expect(opts.define!["import.meta.env"]).toBeUndefined();
|
|
118
128
|
});
|
|
119
129
|
|
|
120
|
-
it("env
|
|
130
|
+
it("env 설정 시 import.meta.env 객체로 define에 주입", async () => {
|
|
121
131
|
await createClientEsbuildContext({
|
|
122
132
|
...baseBuild,
|
|
123
133
|
env: { MSG: 'hello "world"' },
|
|
124
134
|
});
|
|
125
135
|
const opts = vi.mocked(esbuild.context).mock.calls[0][0];
|
|
126
|
-
expect(opts.define!["import.meta.env
|
|
136
|
+
expect(opts.define!["import.meta.env"]).toBe(JSON.stringify({ MSG: 'hello "world"' }));
|
|
127
137
|
});
|
|
128
138
|
});
|
|
129
139
|
|
|
@@ -160,13 +170,65 @@ describe("createClientEsbuildContext — PostCSS 설정", () => {
|
|
|
160
170
|
expect(styleOpts.postcssConfiguration).toBeUndefined();
|
|
161
171
|
});
|
|
162
172
|
|
|
163
|
-
it("
|
|
173
|
+
it("postcssPlugins 전달해도 postcssConfiguration은 항상 undefined", async () => {
|
|
164
174
|
await createClientEsbuildContext({
|
|
165
175
|
...baseBuild,
|
|
166
176
|
postcssPlugins: [["autoprefixer"]],
|
|
167
177
|
});
|
|
168
178
|
const [, styleOpts] = vi.mocked(createCompilerPlugin).mock.calls[0];
|
|
169
|
-
expect(styleOpts.postcssConfiguration
|
|
179
|
+
expect(styleOpts.postcssConfiguration).toBeUndefined();
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe("createClientEsbuildContext — PostCSS 플러그인 통합", () => {
|
|
184
|
+
beforeEach(() => vi.clearAllMocks());
|
|
185
|
+
|
|
186
|
+
it("postcssPlugins 전달 시 sd-postcss 플러그인이 plugins에 등록된다", async () => {
|
|
187
|
+
await createClientEsbuildContext({
|
|
188
|
+
...baseBuild,
|
|
189
|
+
postcssPlugins: [["autoprefixer"]],
|
|
190
|
+
});
|
|
191
|
+
const opts = vi.mocked(esbuild.context).mock.calls[0][0];
|
|
192
|
+
const pluginNames = opts.plugins!.map((p: any) => p.name);
|
|
193
|
+
expect(pluginNames).toContain("sd-postcss");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("postcssPlugins 미전달 시 sd-postcss 미등록", async () => {
|
|
197
|
+
await createClientEsbuildContext(baseBuild);
|
|
198
|
+
const opts = vi.mocked(esbuild.context).mock.calls[0][0];
|
|
199
|
+
const pluginNames = opts.plugins!.map((p: any) => p.name);
|
|
200
|
+
expect(pluginNames).not.toContain("sd-postcss");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("sd-postcss가 customPlugins 뒤, sd-legacy-strip-dynamic-import 앞에 배치된다", async () => {
|
|
204
|
+
const customPlugin = { name: "custom", setup: vi.fn() };
|
|
205
|
+
await createClientEsbuildContext({
|
|
206
|
+
...baseBuild,
|
|
207
|
+
postcssPlugins: [["autoprefixer"]],
|
|
208
|
+
plugins: [customPlugin as any],
|
|
209
|
+
legacyModule: true,
|
|
210
|
+
onEnd: vi.fn(),
|
|
211
|
+
});
|
|
212
|
+
const opts = vi.mocked(esbuild.context).mock.calls[0][0];
|
|
213
|
+
const pluginNames = opts.plugins!.map((p: any) => p.name);
|
|
214
|
+
|
|
215
|
+
const customIdx = pluginNames.indexOf("custom");
|
|
216
|
+
const postcssIdx = pluginNames.indexOf("sd-postcss");
|
|
217
|
+
const stripIdx = pluginNames.indexOf("sd-legacy-strip-dynamic-import");
|
|
218
|
+
const onEndIdx = pluginNames.indexOf("sd-on-end");
|
|
219
|
+
|
|
220
|
+
expect(postcssIdx).toBeGreaterThan(customIdx);
|
|
221
|
+
expect(postcssIdx).toBeLessThan(stripIdx);
|
|
222
|
+
expect(postcssIdx).toBeLessThan(onEndIdx);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("존재하지 않는 플러그인 이름으로 에러가 throw된다", async () => {
|
|
226
|
+
await expect(
|
|
227
|
+
createClientEsbuildContext({
|
|
228
|
+
...baseBuild,
|
|
229
|
+
postcssPlugins: [["nonexistent-plugin"]],
|
|
230
|
+
}),
|
|
231
|
+
).rejects.toThrow("nonexistent-plugin");
|
|
170
232
|
});
|
|
171
233
|
});
|
|
172
234
|
|
|
@@ -377,14 +439,17 @@ describe("createClientEsbuildContext — onEnd 플러그인", () => {
|
|
|
377
439
|
expect(pluginNames[pluginNames.length - 1]).toBe("sd-on-end");
|
|
378
440
|
});
|
|
379
441
|
|
|
380
|
-
it("angularPlugin
|
|
442
|
+
it("customPlugins가 angularPlugin 이전에 위치 (onStart에서 sourceFileCache 무효화 선행)", async () => {
|
|
381
443
|
await createClientEsbuildContext({
|
|
382
444
|
...baseDev,
|
|
383
445
|
plugins: [{ name: "custom", setup: vi.fn() } as any],
|
|
384
446
|
onEnd: vi.fn(),
|
|
385
447
|
});
|
|
386
448
|
const opts = vi.mocked(esbuild.context).mock.calls[0][0];
|
|
387
|
-
|
|
449
|
+
const pluginNames = opts.plugins!.map((p: any) => p.name);
|
|
450
|
+
const customIdx = pluginNames.indexOf("custom");
|
|
451
|
+
const angularIdx = pluginNames.indexOf("angular-compiler");
|
|
452
|
+
expect(customIdx).toBeLessThan(angularIdx);
|
|
388
453
|
});
|
|
389
454
|
});
|
|
390
455
|
|
|
@@ -419,7 +484,7 @@ describe("createClientEsbuildContext — SCSS 플러그인 통합", () => {
|
|
|
419
484
|
expect(pluginNames).toContain("sd-scss");
|
|
420
485
|
});
|
|
421
486
|
|
|
422
|
-
it("sd-scss 플러그인이 angularPlugin
|
|
487
|
+
it("sd-scss 플러그인이 angularPlugin 다음에 위치", async () => {
|
|
423
488
|
const customPlugin = { name: "custom", setup: vi.fn() };
|
|
424
489
|
await createClientEsbuildContext({
|
|
425
490
|
...baseDev,
|
|
@@ -430,10 +495,8 @@ describe("createClientEsbuildContext — SCSS 플러그인 통합", () => {
|
|
|
430
495
|
|
|
431
496
|
const angularIdx = pluginNames.indexOf("angular-compiler");
|
|
432
497
|
const scssIdx = pluginNames.indexOf("sd-scss");
|
|
433
|
-
const customIdx = pluginNames.indexOf("custom");
|
|
434
498
|
|
|
435
499
|
expect(scssIdx).toBe(angularIdx + 1);
|
|
436
|
-
expect(scssIdx).toBeLessThan(customIdx);
|
|
437
500
|
});
|
|
438
501
|
});
|
|
439
502
|
|
|
@@ -471,8 +534,8 @@ describe("createClientEsbuildContext — legacyModule 설정", () => {
|
|
|
471
534
|
env: { API_URL: "https://api.example.com" },
|
|
472
535
|
});
|
|
473
536
|
const opts = vi.mocked(esbuild.context).mock.calls[0][0];
|
|
474
|
-
expect(opts.define!["import.meta.env
|
|
475
|
-
JSON.stringify("https://api.example.com"),
|
|
537
|
+
expect(opts.define!["import.meta.env"]).toBe(
|
|
538
|
+
JSON.stringify({ API_URL: "https://api.example.com" }),
|
|
476
539
|
);
|
|
477
540
|
expect(opts.supported).toEqual({ "import-meta": false });
|
|
478
541
|
});
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import postcss from "postcss";
|
|
6
|
+
import type esbuild from "esbuild";
|
|
7
|
+
import { createPostcssPlugin } from "../../src/esbuild/esbuild-postcss-plugin";
|
|
8
|
+
|
|
9
|
+
// --- Helpers ---
|
|
10
|
+
|
|
11
|
+
/** PostCSS 플러그인: CSS 앞에 마커 주석을 추가한다 */
|
|
12
|
+
function markerPlugin(): postcss.Plugin {
|
|
13
|
+
return {
|
|
14
|
+
postcssPlugin: "test-marker",
|
|
15
|
+
Once(root) {
|
|
16
|
+
root.prepend(new postcss.Comment({ text: "marker" }));
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** PostCSS 플러그인: 항상 예외를 발생시킨다 */
|
|
22
|
+
function throwingPlugin(): postcss.Plugin {
|
|
23
|
+
return {
|
|
24
|
+
postcssPlugin: "test-throwing",
|
|
25
|
+
Once() {
|
|
26
|
+
throw new Error("postcss-boom");
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** esbuild 플러그인에서 onEnd 콜백을 캡처한다 */
|
|
32
|
+
function captureOnEnd(
|
|
33
|
+
plugin: esbuild.Plugin,
|
|
34
|
+
): (result: esbuild.BuildResult) => Promise<void> | void {
|
|
35
|
+
let cb!: (result: esbuild.BuildResult) => Promise<void> | void;
|
|
36
|
+
void plugin.setup({
|
|
37
|
+
onEnd(fn: (result: esbuild.BuildResult) => Promise<void> | void) {
|
|
38
|
+
cb = fn;
|
|
39
|
+
},
|
|
40
|
+
} as unknown as esbuild.PluginBuild);
|
|
41
|
+
return cb;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 최소한의 esbuild BuildResult를 생성한다 */
|
|
45
|
+
function buildResult(outputs: Record<string, unknown>): esbuild.BuildResult {
|
|
46
|
+
return {
|
|
47
|
+
errors: [] as esbuild.Message[],
|
|
48
|
+
warnings: [] as esbuild.Message[],
|
|
49
|
+
metafile: { inputs: {}, outputs } as esbuild.Metafile,
|
|
50
|
+
outputFiles: [],
|
|
51
|
+
mangleCache: {},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// --- Tests ---
|
|
56
|
+
|
|
57
|
+
describe("createPostcssPlugin — Acceptance", () => {
|
|
58
|
+
let tmpDir: string;
|
|
59
|
+
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "postcss-test-"));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
afterEach(() => {
|
|
65
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Scenario: .css 파일에 PostCSS 적용
|
|
69
|
+
it(".css 파일의 내용이 PostCSS 플러그인으로 변환되어 덮어쓰기된다", async () => {
|
|
70
|
+
const cssFile = path.join(tmpDir, "main.css");
|
|
71
|
+
fs.writeFileSync(cssFile, ".host { display: flex; }");
|
|
72
|
+
|
|
73
|
+
const plugin = createPostcssPlugin({ plugins: [markerPlugin()] });
|
|
74
|
+
const onEnd = captureOnEnd(plugin);
|
|
75
|
+
|
|
76
|
+
await onEnd(
|
|
77
|
+
buildResult({
|
|
78
|
+
[cssFile]: { bytes: 100, inputs: {}, imports: [], exports: [] },
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const output = fs.readFileSync(cssFile, "utf-8");
|
|
83
|
+
expect(output).toContain("/* marker */");
|
|
84
|
+
expect(output).toContain("display: flex");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Scenario: .css 확장자가 아닌 파일은 CSS 전체 처리 대상에서 제외
|
|
88
|
+
it(".js 파일은 CSS 전체 적용 로직에서 처리되지 않아 원본이 유지된다", async () => {
|
|
89
|
+
const jsFile = path.join(tmpDir, "main.js");
|
|
90
|
+
const original = 'console.log("hello");';
|
|
91
|
+
fs.writeFileSync(jsFile, original);
|
|
92
|
+
|
|
93
|
+
const plugin = createPostcssPlugin({ plugins: [markerPlugin()] });
|
|
94
|
+
const onEnd = captureOnEnd(plugin);
|
|
95
|
+
|
|
96
|
+
await onEnd(
|
|
97
|
+
buildResult({
|
|
98
|
+
[jsFile]: { bytes: 100, inputs: {}, imports: [], exports: [] },
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
expect(fs.readFileSync(jsFile, "utf-8")).toBe(original);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Scenario: 빈 PostCSS 플러그인 배열로 생성 시 파일 처리 없음
|
|
106
|
+
it("빈 플러그인 배열이면 .css 파일을 읽지도 수정하지도 않는다", async () => {
|
|
107
|
+
const cssFile = path.join(tmpDir, "main.css");
|
|
108
|
+
const original = ".host { display: flex; }";
|
|
109
|
+
fs.writeFileSync(cssFile, original);
|
|
110
|
+
|
|
111
|
+
const plugin = createPostcssPlugin({ plugins: [] });
|
|
112
|
+
const onEnd = captureOnEnd(plugin);
|
|
113
|
+
|
|
114
|
+
await onEnd(
|
|
115
|
+
buildResult({
|
|
116
|
+
[cssFile]: { bytes: 100, inputs: {}, imports: [], exports: [] },
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
expect(fs.readFileSync(cssFile, "utf-8")).toBe(original);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Scenario: .css 파일 PostCSS 적용 실패 시 esbuild error 추가
|
|
124
|
+
it("PostCSS 실패 시 result.errors에 파일 경로와 에러 메시지가 추가된다", async () => {
|
|
125
|
+
const cssFile = path.join(tmpDir, "broken.css");
|
|
126
|
+
fs.writeFileSync(cssFile, ".host { display: flex; }");
|
|
127
|
+
|
|
128
|
+
const plugin = createPostcssPlugin({ plugins: [throwingPlugin()] });
|
|
129
|
+
const onEnd = captureOnEnd(plugin);
|
|
130
|
+
|
|
131
|
+
const result = buildResult({
|
|
132
|
+
[cssFile]: { bytes: 100, inputs: {}, imports: [], exports: [] },
|
|
133
|
+
});
|
|
134
|
+
await onEnd(result);
|
|
135
|
+
|
|
136
|
+
expect(result.errors).toHaveLength(1);
|
|
137
|
+
expect(result.errors[0].text).toContain("postcss-boom");
|
|
138
|
+
expect(result.errors[0].text).toContain(cssFile);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// --- Slice 2: .js 파일 AST 기반 inline CSS ---
|
|
142
|
+
|
|
143
|
+
// Scenario: ɵɵdefineComponent 내 단일 styles 문자열에 PostCSS 적용
|
|
144
|
+
it("ɵɵdefineComponent 내 styles 문자열에 PostCSS가 적용되어 내용이 변환된다", async () => {
|
|
145
|
+
const jsFile = path.join(tmpDir, "main.js");
|
|
146
|
+
const code = [
|
|
147
|
+
'import * as i0 from "@angular/core";',
|
|
148
|
+
"class MyComp {}",
|
|
149
|
+
"MyComp.ɵcmp = i0.ɵɵdefineComponent({",
|
|
150
|
+
' styles: [".host { display: flex; }"]',
|
|
151
|
+
"});",
|
|
152
|
+
].join("\n");
|
|
153
|
+
fs.writeFileSync(jsFile, code);
|
|
154
|
+
|
|
155
|
+
const plugin = createPostcssPlugin({ plugins: [markerPlugin()] });
|
|
156
|
+
const onEnd = captureOnEnd(plugin);
|
|
157
|
+
|
|
158
|
+
await onEnd(
|
|
159
|
+
buildResult({
|
|
160
|
+
[jsFile]: { bytes: 200, inputs: {}, imports: [], exports: [] },
|
|
161
|
+
}),
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const output = fs.readFileSync(jsFile, "utf-8");
|
|
165
|
+
expect(output).toContain("/* marker */");
|
|
166
|
+
expect(output).toContain("display: flex");
|
|
167
|
+
// 원본 JS 구조가 유지되어야 한다
|
|
168
|
+
expect(output).toContain("ɵɵdefineComponent");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Scenario: ɵɵdefineComponent 내 복수 styles 문자열에 각각 PostCSS 적용
|
|
172
|
+
it("복수 styles 문자열에 각각 독립적으로 PostCSS가 적용된다", async () => {
|
|
173
|
+
const jsFile = path.join(tmpDir, "multi.js");
|
|
174
|
+
const code = [
|
|
175
|
+
'import * as i0 from "@angular/core";',
|
|
176
|
+
"class MyComp {}",
|
|
177
|
+
"MyComp.ɵcmp = i0.ɵɵdefineComponent({",
|
|
178
|
+
' styles: [".a { color: red; }", ".b { color: blue; }"]',
|
|
179
|
+
"});",
|
|
180
|
+
].join("\n");
|
|
181
|
+
fs.writeFileSync(jsFile, code);
|
|
182
|
+
|
|
183
|
+
const plugin = createPostcssPlugin({ plugins: [markerPlugin()] });
|
|
184
|
+
const onEnd = captureOnEnd(plugin);
|
|
185
|
+
|
|
186
|
+
await onEnd(
|
|
187
|
+
buildResult({
|
|
188
|
+
[jsFile]: { bytes: 200, inputs: {}, imports: [], exports: [] },
|
|
189
|
+
}),
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const output = fs.readFileSync(jsFile, "utf-8");
|
|
193
|
+
// 마커가 각 문자열에 독립적으로 적용됨 → 2회 등장
|
|
194
|
+
const markerCount = (output.match(/\/\* marker \*\//g) ?? []).length;
|
|
195
|
+
expect(markerCount).toBe(2);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Scenario: styles 배열이 비어 있으면 파일 수정 안 함
|
|
199
|
+
it("styles 배열이 비어 있으면 .js 파일이 수정되지 않는다", async () => {
|
|
200
|
+
const jsFile = path.join(tmpDir, "empty.js");
|
|
201
|
+
const code = [
|
|
202
|
+
'import * as i0 from "@angular/core";',
|
|
203
|
+
"class MyComp {}",
|
|
204
|
+
"MyComp.ɵcmp = i0.ɵɵdefineComponent({",
|
|
205
|
+
" styles: []",
|
|
206
|
+
"});",
|
|
207
|
+
].join("\n");
|
|
208
|
+
fs.writeFileSync(jsFile, code);
|
|
209
|
+
|
|
210
|
+
const plugin = createPostcssPlugin({ plugins: [markerPlugin()] });
|
|
211
|
+
const onEnd = captureOnEnd(plugin);
|
|
212
|
+
|
|
213
|
+
await onEnd(
|
|
214
|
+
buildResult({
|
|
215
|
+
[jsFile]: { bytes: 200, inputs: {}, imports: [], exports: [] },
|
|
216
|
+
}),
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
expect(fs.readFileSync(jsFile, "utf-8")).toBe(code);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Scenario: ɵɵdefineComponent 외부의 styles 속성은 무시
|
|
223
|
+
it("ɵɵdefineComponent 외부의 styles 속성은 처리되지 않는다", async () => {
|
|
224
|
+
const jsFile = path.join(tmpDir, "outside.js");
|
|
225
|
+
const code = [
|
|
226
|
+
'const config = { styles: [".host { display: flex; }"] };',
|
|
227
|
+
"export default config;",
|
|
228
|
+
].join("\n");
|
|
229
|
+
fs.writeFileSync(jsFile, code);
|
|
230
|
+
|
|
231
|
+
const plugin = createPostcssPlugin({ plugins: [markerPlugin()] });
|
|
232
|
+
const onEnd = captureOnEnd(plugin);
|
|
233
|
+
|
|
234
|
+
await onEnd(
|
|
235
|
+
buildResult({
|
|
236
|
+
[jsFile]: { bytes: 200, inputs: {}, imports: [], exports: [] },
|
|
237
|
+
}),
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
expect(fs.readFileSync(jsFile, "utf-8")).toBe(code);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Scenario: 한 .js 파일 내 여러 ɵɵdefineComponent의 styles 모두 처리
|
|
244
|
+
it("한 파일 내 여러 ɵɵdefineComponent의 styles 모두에 PostCSS가 적용된다", async () => {
|
|
245
|
+
const jsFile = path.join(tmpDir, "bundle.js");
|
|
246
|
+
const code = [
|
|
247
|
+
'import * as i0 from "@angular/core";',
|
|
248
|
+
"class CompA {}",
|
|
249
|
+
"CompA.ɵcmp = i0.ɵɵdefineComponent({",
|
|
250
|
+
' styles: [".a { color: red; }"]',
|
|
251
|
+
"});",
|
|
252
|
+
"class CompB {}",
|
|
253
|
+
"CompB.ɵcmp = i0.ɵɵdefineComponent({",
|
|
254
|
+
' styles: [".b { color: blue; }"]',
|
|
255
|
+
"});",
|
|
256
|
+
].join("\n");
|
|
257
|
+
fs.writeFileSync(jsFile, code);
|
|
258
|
+
|
|
259
|
+
const plugin = createPostcssPlugin({ plugins: [markerPlugin()] });
|
|
260
|
+
const onEnd = captureOnEnd(plugin);
|
|
261
|
+
|
|
262
|
+
await onEnd(
|
|
263
|
+
buildResult({
|
|
264
|
+
[jsFile]: { bytes: 200, inputs: {}, imports: [], exports: [] },
|
|
265
|
+
}),
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
const output = fs.readFileSync(jsFile, "utf-8");
|
|
269
|
+
const markerCount = (output.match(/\/\* marker \*\//g) ?? []).length;
|
|
270
|
+
expect(markerCount).toBe(2);
|
|
271
|
+
expect(output).toContain("color: red");
|
|
272
|
+
expect(output).toContain("color: blue");
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Scenario: .js 파일 inline CSS PostCSS 적용 실패 시 esbuild error 추가
|
|
276
|
+
it(".js inline CSS PostCSS 실패 시 result.errors에 에러가 추가된다", async () => {
|
|
277
|
+
const jsFile = path.join(tmpDir, "fail.js");
|
|
278
|
+
const code = [
|
|
279
|
+
'import * as i0 from "@angular/core";',
|
|
280
|
+
"class MyComp {}",
|
|
281
|
+
"MyComp.ɵcmp = i0.ɵɵdefineComponent({",
|
|
282
|
+
' styles: [".host { display: flex; }"]',
|
|
283
|
+
"});",
|
|
284
|
+
].join("\n");
|
|
285
|
+
fs.writeFileSync(jsFile, code);
|
|
286
|
+
|
|
287
|
+
const plugin = createPostcssPlugin({ plugins: [throwingPlugin()] });
|
|
288
|
+
const onEnd = captureOnEnd(plugin);
|
|
289
|
+
|
|
290
|
+
const result = buildResult({
|
|
291
|
+
[jsFile]: { bytes: 200, inputs: {}, imports: [], exports: [] },
|
|
292
|
+
});
|
|
293
|
+
await onEnd(result);
|
|
294
|
+
|
|
295
|
+
expect(result.errors.length).toBeGreaterThanOrEqual(1);
|
|
296
|
+
expect(result.errors[0].text).toContain("postcss-boom");
|
|
297
|
+
expect(result.errors[0].text).toContain(jsFile);
|
|
298
|
+
});
|
|
299
|
+
});
|