@simplysm/sd-cli 14.0.96 → 14.0.98
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/generators/root.d.ts.map +1 -1
- package/dist/commands/init/generators/root.js +0 -1
- package/dist/commands/init/generators/root.js.map +1 -1
- package/dist/commands/init/init.d.ts.map +1 -1
- package/dist/commands/init/init.js +1 -2
- package/dist/commands/init/init.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/generators/root.ts +0 -1
- package/src/commands/init/init.ts +63 -64
- 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/src/commands/init/templates/workspace-root/.prettierignore +0 -1
- 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
|
@@ -141,9 +141,8 @@ describe("server/src/main.ts.hbs", () => {
|
|
|
141
141
|
serverPort: 40080,
|
|
142
142
|
});
|
|
143
143
|
const out = await renderTemplate(path.join(TPL_ROOT, "server/src/main.ts.hbs"), data);
|
|
144
|
-
expect(out).toContain(
|
|
145
|
-
|
|
146
|
-
);
|
|
144
|
+
expect(out).toContain("OrmService,");
|
|
145
|
+
expect(out).toContain('...(parseBoolEnv(env("DEV")) ? [DevService] : []),');
|
|
147
146
|
expect(out).not.toContain("AuthService");
|
|
148
147
|
});
|
|
149
148
|
|
|
@@ -161,9 +160,9 @@ describe("server/src/main.ts.hbs", () => {
|
|
|
161
160
|
const out = await renderTemplate(path.join(TPL_ROOT, "server/src/main.ts.hbs"), data);
|
|
162
161
|
expect(out).toContain('import { AuthService } from "./services/auth.service";');
|
|
163
162
|
expect(out).toContain('import { DevService } from "./services/dev.service";');
|
|
164
|
-
expect(out).toContain(
|
|
165
|
-
|
|
166
|
-
);
|
|
163
|
+
expect(out).toContain("OrmService,");
|
|
164
|
+
expect(out).toContain("AuthService,");
|
|
165
|
+
expect(out).toContain('...(parseBoolEnv(env("DEV")) ? [DevService] : []),');
|
|
167
166
|
});
|
|
168
167
|
});
|
|
169
168
|
|
|
@@ -208,8 +207,8 @@ describe("server 인증 템플릿", () => {
|
|
|
208
207
|
expect(out).toContain("roleName: employee.role!.name,");
|
|
209
208
|
expect(out).toContain(".include((item) => item.role)");
|
|
210
209
|
expect(out).toContain(".from(EmployeeConfig)");
|
|
211
|
-
expect(out).toContain("expr.eq(
|
|
212
|
-
expect(out).toContain("(configs as Record<string, unknown>)[
|
|
210
|
+
expect(out).toContain("expr.eq(ec.employeeId, e.id)");
|
|
211
|
+
expect(out).toContain("(configs as Record<string, unknown>)[c.code] = JSON.parse(c.valueJson);");
|
|
213
212
|
expect(out).toContain("유효하지 않은 직원입니다.");
|
|
214
213
|
|
|
215
214
|
// update(내정보수정) 메서드 + 인증정보 변경 이벤트 emit
|
|
@@ -371,8 +370,24 @@ describe("client 인증 로그인 (routes / login.view / ng-icons)", () => {
|
|
|
371
370
|
expect(out).toContain('"@ng-icons/tabler-icons": "^33.2.3"');
|
|
372
371
|
});
|
|
373
372
|
|
|
374
|
-
it("client/package.json: 인증 OFF → @ng-icons
|
|
373
|
+
it("client/package.json: 인증 OFF + DB ON → dev 모달용 @ng-icons 포함", async () => {
|
|
375
374
|
const out = await renderTemplate(path.join(TPL_ROOT, "client/package.json.hbs"), authClientCtx(false));
|
|
375
|
+
expect(out).toContain('"@ng-icons/core": "^33.2.3"');
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("client/package.json: 인증 OFF + DB OFF → @ng-icons 없음", async () => {
|
|
379
|
+
const data = buildData({
|
|
380
|
+
workspaceName: "demo",
|
|
381
|
+
description: "Demo",
|
|
382
|
+
hasServer: true,
|
|
383
|
+
hasDb: false,
|
|
384
|
+
clients: [{ name: "admin", type: "web", hasRouter: true }],
|
|
385
|
+
serverPort: 40080,
|
|
386
|
+
});
|
|
387
|
+
const out = await renderTemplate(path.join(TPL_ROOT, "client/package.json.hbs"), {
|
|
388
|
+
...data,
|
|
389
|
+
client: data.clients[0],
|
|
390
|
+
});
|
|
376
391
|
expect(out).not.toContain("@ng-icons");
|
|
377
392
|
});
|
|
378
393
|
});
|
|
@@ -397,7 +412,7 @@ describe("관리자 셸 (home/main/app-structure/main.ts 와이어링)", () => {
|
|
|
397
412
|
it("common/app-structure: 사용자 엔티티 메뉴 항목에 선택 네이밍 반영", async () => {
|
|
398
413
|
const out = await renderTemplate(path.join(TPL_ROOT, "common/src/app-structure.ts.hbs"), authClientCtx(true));
|
|
399
414
|
expect(out).toContain('import type { AppStructureItem } from "@simplysm/service-common";');
|
|
400
|
-
expect(out).toContain("export const
|
|
415
|
+
expect(out).toContain("export const adminAppStructureItems: AppStructureItem[] = [");
|
|
401
416
|
expect(out).toContain('{ code: "employee", title: "직원", perms: ["use", "edit"] }');
|
|
402
417
|
expect(out).toContain('{ code: "role-permission", title: "역할/권한", perms: ["use", "edit"] }');
|
|
403
418
|
});
|
|
@@ -448,8 +463,8 @@ describe("관리자 셸 (home/main/app-structure/main.ts 와이어링)", () => {
|
|
|
448
463
|
it("client/main.ts: 인증 ON + router → app-structure 초기화 와이어링", async () => {
|
|
449
464
|
const out = await renderTemplate(path.join(TPL_ROOT, "client/src/main.ts.hbs"), authClientCtx(true));
|
|
450
465
|
expect(out).toContain("SdAppStructureProvider");
|
|
451
|
-
expect(out).toContain('import {
|
|
452
|
-
expect(out).toContain("inject(SdAppStructureProvider).initialize(
|
|
466
|
+
expect(out).toContain('import { adminAppStructureItems } from "@demo/common";');
|
|
467
|
+
expect(out).toContain("inject(SdAppStructureProvider).initialize(adminAppStructureItems);");
|
|
453
468
|
});
|
|
454
469
|
|
|
455
470
|
it("client/main.ts: 인증 OFF → app-structure 초기화 없음", async () => {
|
|
@@ -534,7 +549,8 @@ describe("client-common/src/providers/app-service.provider.ts.hbs", () => {
|
|
|
534
549
|
serverPort: 40080,
|
|
535
550
|
});
|
|
536
551
|
const out = await renderTemplate(path.join(TPL_ROOT, svcTpl), data);
|
|
537
|
-
expect(out).toContain(
|
|
552
|
+
expect(out).toContain("AuthServiceMethods,");
|
|
553
|
+
expect(out).toContain('} from "@demo/server";');
|
|
538
554
|
expect(out).toContain("type ServiceProxy,");
|
|
539
555
|
expect(out).toContain("private _auth?: ServiceProxy<AuthServiceMethods>;");
|
|
540
556
|
expect(out).toContain("get auth(): ServiceProxy<AuthServiceMethods> {");
|
|
@@ -564,8 +580,8 @@ describe("client-common/src/providers/app-auth.provider.ts.hbs", () => {
|
|
|
564
580
|
expect(out).toContain("export class AppAuthProvider {");
|
|
565
581
|
expect(out).toContain("authInfo = signal<IAuthData | undefined>(undefined);");
|
|
566
582
|
expect(out).toContain("inject(SdAppStructureProvider)");
|
|
567
|
-
expect(out).toContain("this._sdAppStructure.permRecord.set(
|
|
568
|
-
expect(out).toContain("async login(
|
|
583
|
+
expect(out).toContain("this._sdAppStructure.permRecord.set(authData.permissions);");
|
|
584
|
+
expect(out).toContain("async login(loginId: string, password: string): Promise<void>");
|
|
569
585
|
expect(out).toContain("async tryReloadAuth(): Promise<boolean>");
|
|
570
586
|
expect(out).toContain("await this._appService.client.auth(token);");
|
|
571
587
|
});
|
|
@@ -584,7 +600,7 @@ describe("client-common/src/index.ts.hbs auth export", () => {
|
|
|
584
600
|
serverPort: 40080,
|
|
585
601
|
});
|
|
586
602
|
const out = await renderTemplate(path.join(TPL_ROOT, "client-common/src/index.ts.hbs"), data);
|
|
587
|
-
expect(out).toContain('export
|
|
603
|
+
expect(out).toContain('export * from "./providers/app-auth.provider";');
|
|
588
604
|
});
|
|
589
605
|
|
|
590
606
|
it("인증 OFF → AppAuthProvider export 없음", async () => {
|
|
@@ -616,9 +632,7 @@ describe("server/src/index.ts.hbs", () => {
|
|
|
616
632
|
serverPort: 40080,
|
|
617
633
|
});
|
|
618
634
|
const out = await renderTemplate(path.join(TPL_ROOT, "server/src/index.ts.hbs"), data);
|
|
619
|
-
expect(out).toContain(
|
|
620
|
-
'export type { AuthServiceMethods, IAuthData, IAuthResult } from "./services/auth.service";',
|
|
621
|
-
);
|
|
635
|
+
expect(out).toContain('export type * from "./services/auth.service";');
|
|
622
636
|
});
|
|
623
637
|
});
|
|
624
638
|
|
|
@@ -928,6 +942,68 @@ describe("common/src/index.ts.hbs", () => {
|
|
|
928
942
|
});
|
|
929
943
|
});
|
|
930
944
|
|
|
945
|
+
describe("SSG 클라이언트 스캐폴드", () => {
|
|
946
|
+
function ssgCtx(useSsg: boolean) {
|
|
947
|
+
const data = buildData({
|
|
948
|
+
workspaceName: "demo",
|
|
949
|
+
description: "Demo",
|
|
950
|
+
hasServer: true,
|
|
951
|
+
hasDb: false,
|
|
952
|
+
clients: [{ name: "portal", type: "web", hasRouter: true, useSsg }],
|
|
953
|
+
serverPort: 40080,
|
|
954
|
+
});
|
|
955
|
+
return { ...data, client: data.clients[0] };
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
it("sd.config: SSG ON → prerender 설정 포함", async () => {
|
|
959
|
+
const ctx = ssgCtx(true);
|
|
960
|
+
const out = await renderTemplate(path.join(TPL_ROOT, "workspace-root/sd.config.ts.hbs"), ctx);
|
|
961
|
+
expect(out).toContain('prerender: ["/"],');
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
it("sd.config: SSG OFF → prerender 없음", async () => {
|
|
965
|
+
const ctx = ssgCtx(false);
|
|
966
|
+
const out = await renderTemplate(path.join(TPL_ROOT, "workspace-root/sd.config.ts.hbs"), ctx);
|
|
967
|
+
expect(out).not.toContain("prerender");
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
it("client/package.json: SSG ON → @angular/platform-server 의존성 포함", async () => {
|
|
971
|
+
const out = await renderTemplate(path.join(TPL_ROOT, "client/package.json.hbs"), ssgCtx(true));
|
|
972
|
+
expect(out).toContain('"@angular/platform-server": "^21.2.0"');
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
it("client/package.json: SSG OFF → @angular/platform-server 없음", async () => {
|
|
976
|
+
const out = await renderTemplate(path.join(TPL_ROOT, "client/package.json.hbs"), ssgCtx(false));
|
|
977
|
+
expect(out).not.toContain("@angular/platform-server");
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
it("client/main.ts: SSG ON → path 라우팅 + hydration", async () => {
|
|
981
|
+
const out = await renderTemplate(path.join(TPL_ROOT, "client/src/main.ts.hbs"), ssgCtx(true));
|
|
982
|
+
expect(out).toContain('import { provideRouter } from "@angular/router";');
|
|
983
|
+
expect(out).not.toContain("withHashLocation");
|
|
984
|
+
expect(out).toContain("provideClientHydration");
|
|
985
|
+
expect(out).toContain("provideRouter(routes),");
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
it("client/main.ts: SSG OFF → hash 라우팅 유지 + hydration 없음", async () => {
|
|
989
|
+
const out = await renderTemplate(path.join(TPL_ROOT, "client/src/main.ts.hbs"), ssgCtx(false));
|
|
990
|
+
expect(out).toContain("provideRouter(routes, withHashLocation()),");
|
|
991
|
+
expect(out).not.toContain("provideClientHydration");
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
it("client/main.server.ts: 서버 부트스트랩 default export + 최소 provider", async () => {
|
|
995
|
+
const out = await renderTemplate(path.join(TPL_ROOT, "client/src/main.server.ts.hbs"), ssgCtx(true));
|
|
996
|
+
expect(out).toContain('import { provideServerRendering } from "@angular/platform-server";');
|
|
997
|
+
expect(out).toContain("const bootstrap = (context: BootstrapContext) =>");
|
|
998
|
+
expect(out).toContain("provideRouter(routes),");
|
|
999
|
+
expect(out).toContain('provideSdAngular({ clientName: "client-portal" }),');
|
|
1000
|
+
expect(out).toContain("provideServerRendering(),");
|
|
1001
|
+
expect(out).toContain("export default bootstrap;");
|
|
1002
|
+
// 프리렌더는 빌드 시점 — 서버 연결·로그 배선 제외
|
|
1003
|
+
expect(out).not.toContain("connectAsync");
|
|
1004
|
+
});
|
|
1005
|
+
});
|
|
1006
|
+
|
|
931
1007
|
describe("root/package.json.hbs", () => {
|
|
932
1008
|
it("hasMobile=true 시 run-device 스크립트 포함", async () => {
|
|
933
1009
|
const data = buildData({
|
|
@@ -984,8 +1060,9 @@ describe("common/src/auth-info-changed.event.ts.hbs", () => {
|
|
|
984
1060
|
);
|
|
985
1061
|
expect(out).toContain('import { defineEvent } from "@simplysm/service-common";');
|
|
986
1062
|
expect(out).toContain(
|
|
987
|
-
|
|
1063
|
+
"export const AuthInfoChangedEvent = defineEvent<{ employeeId: number; roleId: number }, void>(",
|
|
988
1064
|
);
|
|
1065
|
+
expect(out).toContain('"AuthInfoChanged",');
|
|
989
1066
|
});
|
|
990
1067
|
});
|
|
991
1068
|
|
|
@@ -1057,8 +1134,10 @@ describe("client-common 인증정보 변경 구독 배선", () => {
|
|
|
1057
1134
|
);
|
|
1058
1135
|
expect(out).toContain("async logout(): Promise<void> {");
|
|
1059
1136
|
expect(out).toContain("await this._unregisterAuthEvent();");
|
|
1060
|
-
expect(out).toContain("await this._registerAuthEvent(authData.employeeId);");
|
|
1061
|
-
expect(out).toContain(
|
|
1137
|
+
expect(out).toContain("await this._registerAuthEvent(authData.employeeId, authData.roleId);");
|
|
1138
|
+
expect(out).toContain(
|
|
1139
|
+
"private async _registerAuthEvent(employeeId: number, roleId: number): Promise<void> {",
|
|
1140
|
+
);
|
|
1062
1141
|
expect(out).toContain("this._appService.authInfoChangedEvent.addListener(");
|
|
1063
1142
|
});
|
|
1064
1143
|
});
|
|
@@ -1094,7 +1173,7 @@ describe("client/src/app/home/my-info/my-info.detail.ts.hbs", () => {
|
|
|
1094
1173
|
expect(out).toContain("this._appAuth.authInfo()?.employeeId");
|
|
1095
1174
|
expect(out).toContain("lastModifiedBy: item.lastDataLog?.employeeName,");
|
|
1096
1175
|
expect(out).toContain("await this._appService.auth.update({");
|
|
1097
|
-
expect(out).toContain('{{ flatMenu.titleChain.join("
|
|
1176
|
+
expect(out).toContain('{{ flatMenu.titleChain.join("» ") }}');
|
|
1098
1177
|
expect(out).not.toContain("\\{{");
|
|
1099
1178
|
});
|
|
1100
1179
|
});
|
|
@@ -1142,8 +1221,8 @@ describe("server/src/services/dev.service.ts.hbs", () => {
|
|
|
1142
1221
|
expect(out).toContain('import { ExcelWorkbook } from "@simplysm/excel";');
|
|
1143
1222
|
expect(out).toContain('import { appStructureItems } from "@demo/common";');
|
|
1144
1223
|
expect(out).toContain(".employee().insert(");
|
|
1145
|
-
expect(out).toContain("getFlatPermissions(
|
|
1146
|
-
expect(out).toContain("
|
|
1224
|
+
expect(out).toContain("getFlatPermissions(");
|
|
1225
|
+
expect(out).toContain("appStructureItems,");
|
|
1147
1226
|
});
|
|
1148
1227
|
|
|
1149
1228
|
it("인증 OFF (DB만) → 시드 없이 db.initialize + TODO", async () => {
|
|
@@ -1182,7 +1261,7 @@ describe("server/src/index.ts.hbs dev export", () => {
|
|
|
1182
1261
|
serverPort: 40080,
|
|
1183
1262
|
});
|
|
1184
1263
|
const out = await renderTemplate(path.join(TPL_ROOT, "server/src/index.ts.hbs"), data);
|
|
1185
|
-
expect(out).toContain('export type
|
|
1264
|
+
expect(out).toContain('export type * from "./services/dev.service";');
|
|
1186
1265
|
expect(out).not.toContain("auth.service");
|
|
1187
1266
|
});
|
|
1188
1267
|
|
|
@@ -1198,8 +1277,8 @@ describe("server/src/index.ts.hbs dev export", () => {
|
|
|
1198
1277
|
serverPort: 40080,
|
|
1199
1278
|
});
|
|
1200
1279
|
const out = await renderTemplate(path.join(TPL_ROOT, "server/src/index.ts.hbs"), data);
|
|
1201
|
-
expect(out).toContain('from "./services/auth.service";');
|
|
1202
|
-
expect(out).toContain('export type
|
|
1280
|
+
expect(out).toContain('export type * from "./services/auth.service";');
|
|
1281
|
+
expect(out).toContain('export type * from "./services/dev.service";');
|
|
1203
1282
|
});
|
|
1204
1283
|
});
|
|
1205
1284
|
|
|
@@ -1219,7 +1298,8 @@ describe("client-common app-service dev getter", () => {
|
|
|
1219
1298
|
path.join(TPL_ROOT, "client-common/src/providers/app-service.provider.ts.hbs"),
|
|
1220
1299
|
data,
|
|
1221
1300
|
);
|
|
1222
|
-
expect(out).toContain(
|
|
1301
|
+
expect(out).toContain("DevServiceMethods,");
|
|
1302
|
+
expect(out).toContain('} from "@demo/server";');
|
|
1223
1303
|
expect(out).toContain("type ServiceProxy,");
|
|
1224
1304
|
expect(out).toContain("get dev(): ServiceProxy<DevServiceMethods> {");
|
|
1225
1305
|
expect(out).toContain('this._dev ??= this.client.getService<DevServiceMethods>("DevService")');
|
|
@@ -1259,7 +1339,7 @@ describe("client/src/app.root.ts.hbs dev 도구 배선", () => {
|
|
|
1259
1339
|
|
|
1260
1340
|
it("DB ON → HostListener keydown + DevModal 호출", async () => {
|
|
1261
1341
|
const out = await renderTemplate(path.join(TPL_ROOT, "client/src/app.root.ts.hbs"), devCtx(true));
|
|
1262
|
-
expect(out).toContain('
|
|
1342
|
+
expect(out).toContain('"(document:keydown)": "onKeydown($event)",');
|
|
1263
1343
|
expect(out).toContain('import { DevModal } from "./modals/dev.modal";');
|
|
1264
1344
|
expect(out).toContain("this._sdModal.showAsync({ type: DevModal");
|
|
1265
1345
|
expect(out).toContain("export class AppRoot {");
|
|
@@ -1268,13 +1348,13 @@ describe("client/src/app.root.ts.hbs dev 도구 배선", () => {
|
|
|
1268
1348
|
it("DB OFF → 빈 AppRoot (dev 배선 없음)", async () => {
|
|
1269
1349
|
const out = await renderTemplate(path.join(TPL_ROOT, "client/src/app.root.ts.hbs"), devCtx(false));
|
|
1270
1350
|
expect(out).toContain("export class AppRoot {}");
|
|
1271
|
-
expect(out).not.toContain("
|
|
1351
|
+
expect(out).not.toContain("onKeydown");
|
|
1272
1352
|
expect(out).not.toContain("DevModal");
|
|
1273
1353
|
});
|
|
1274
1354
|
});
|
|
1275
1355
|
|
|
1276
1356
|
describe("client/src/modals/dev.modal.ts.hbs", () => {
|
|
1277
|
-
it("dev 모달: client-common import + initDb 호출
|
|
1357
|
+
it("dev 모달: client-common import + initDb 호출 + 아이콘 사용", async () => {
|
|
1278
1358
|
const data = buildData({
|
|
1279
1359
|
workspaceName: "demo",
|
|
1280
1360
|
description: "Demo",
|
|
@@ -1293,6 +1373,6 @@ describe("client/src/modals/dev.modal.ts.hbs", () => {
|
|
|
1293
1373
|
expect(out).toContain("export class DevModal implements SdModalContentDef<void> {");
|
|
1294
1374
|
expect(out).toContain('import { AppServiceProvider } from "@demo/client-common";');
|
|
1295
1375
|
expect(out).toContain("await this._appService.dev.initDb();");
|
|
1296
|
-
expect(out).
|
|
1376
|
+
expect(out).toContain('import { NgIcon } from "@ng-icons/core";');
|
|
1297
1377
|
});
|
|
1298
1378
|
});
|
|
@@ -14,27 +14,6 @@ describe("HMR 클라이언트 스크립트 통합", () => {
|
|
|
14
14
|
// logical assignment (&&=, ||=, ??=) 미사용
|
|
15
15
|
expect(script).not.toMatch(/&&=|(\|\|=)|(\?\?=)/);
|
|
16
16
|
});
|
|
17
|
-
|
|
18
|
-
it("WebSocket 연결 코드를 포함한다", () => {
|
|
19
|
-
const script = getHmrClientScript("/app/", 4200);
|
|
20
|
-
expect(script).toContain("WebSocket");
|
|
21
|
-
expect(script).toContain("ws://");
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("component-update, css-update, full-reload 메시지 핸들러를 포함한다", () => {
|
|
25
|
-
const script = getHmrClientScript("/app/", 4200);
|
|
26
|
-
expect(script).toContain("component-update");
|
|
27
|
-
expect(script).toContain("css-update");
|
|
28
|
-
expect(script).toContain("full-reload");
|
|
29
|
-
expect(script).toContain("__hmr_dispatch");
|
|
30
|
-
expect(script).toContain("location.reload");
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("자동 재연결 로직을 포함한다", () => {
|
|
34
|
-
const script = getHmrClientScript("/app/", 4200);
|
|
35
|
-
expect(script).toContain("setTimeout");
|
|
36
|
-
expect(script).toContain("connect");
|
|
37
|
-
});
|
|
38
17
|
});
|
|
39
18
|
|
|
40
19
|
describe("Scenario: css-update 메시지의 files와 매칭되는 link만 업데이트", () => {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
*.hbs
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import {
|
|
4
|
-
FIXTURE_DIR,
|
|
5
|
-
PKG_DIR,
|
|
6
|
-
initPlugin,
|
|
7
|
-
} from "./_vite-angular-plugin-test-setup";
|
|
8
|
-
|
|
9
|
-
const { sdAngularPlugin } = await import("../../src/angular/vite-angular-plugin.js");
|
|
10
|
-
|
|
11
|
-
beforeEach(() => {
|
|
12
|
-
vi.clearAllMocks();
|
|
13
|
-
vi.spyOn(process, "cwd").mockReturnValue(FIXTURE_DIR);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
describe("sdAngularPlugin", () => {
|
|
17
|
-
// Scenario: non-Angular .ts 파일은 기본 처리
|
|
18
|
-
it("returns undefined for non-emitted .ts files", async () => {
|
|
19
|
-
const plugin = sdAngularPlugin({ pkg: "basic-app" });
|
|
20
|
-
await initPlugin(plugin);
|
|
21
|
-
|
|
22
|
-
const result = (plugin as any).transform?.call(
|
|
23
|
-
{},
|
|
24
|
-
"export const x = 1;",
|
|
25
|
-
"/some/unknown/file.ts",
|
|
26
|
-
);
|
|
27
|
-
|
|
28
|
-
expect(result).toBeUndefined();
|
|
29
|
-
(plugin as any).buildEnd?.call({});
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
// Scenario: buildStart에서 초기화 및 emit + buildEnd에서 리소스 정리
|
|
33
|
-
it("initializes facade in buildStart and disposes in buildEnd", async () => {
|
|
34
|
-
const plugin = sdAngularPlugin({ pkg: "basic-app" });
|
|
35
|
-
await initPlugin(plugin);
|
|
36
|
-
(plugin as any).buildEnd?.call({});
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
// Scenario: .mjs/.js 파일은 처리하지 않는다
|
|
40
|
-
it("returns undefined for .mjs files", async () => {
|
|
41
|
-
const plugin = sdAngularPlugin({ pkg: "basic-app" });
|
|
42
|
-
await initPlugin(plugin);
|
|
43
|
-
|
|
44
|
-
const result = (plugin as any).transform?.call(
|
|
45
|
-
{},
|
|
46
|
-
"export const x = 1;",
|
|
47
|
-
"/some/library/module.mjs",
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
expect(result).toBeUndefined();
|
|
51
|
-
(plugin as any).buildEnd?.call({});
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("returns undefined for .js files", async () => {
|
|
55
|
-
const plugin = sdAngularPlugin({ pkg: "basic-app" });
|
|
56
|
-
await initPlugin(plugin);
|
|
57
|
-
|
|
58
|
-
const result = (plugin as any).transform?.call(
|
|
59
|
-
{},
|
|
60
|
-
"export const y = 2;",
|
|
61
|
-
"/some/library/module.js",
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
expect(result).toBeUndefined();
|
|
65
|
-
(plugin as any).buildEnd?.call({});
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
// Scenario: 비대상 파일(.css 등)은 transform하지 않는다
|
|
69
|
-
it("returns undefined for non-JS files like .css", async () => {
|
|
70
|
-
const plugin = sdAngularPlugin({ pkg: "basic-app" });
|
|
71
|
-
await initPlugin(plugin);
|
|
72
|
-
|
|
73
|
-
const result = (plugin as any).transform?.call(
|
|
74
|
-
{},
|
|
75
|
-
"body { color: red; }",
|
|
76
|
-
"/some/styles.css",
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
expect(result).toBeUndefined();
|
|
80
|
-
(plugin as any).buildEnd?.call({});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
// Scenario: Angular .ts 파일 transform
|
|
84
|
-
it("transforms emitted .ts files with compiled JS", async () => {
|
|
85
|
-
const plugin = sdAngularPlugin({ pkg: "basic-app" });
|
|
86
|
-
await initPlugin(plugin);
|
|
87
|
-
|
|
88
|
-
const appComponentPath = path
|
|
89
|
-
.join(PKG_DIR, "src/app.component.ts")
|
|
90
|
-
.replace(/\\/g, "/");
|
|
91
|
-
|
|
92
|
-
const result = (plugin as any).transform?.call({}, "", appComponentPath);
|
|
93
|
-
|
|
94
|
-
if (result != null) {
|
|
95
|
-
expect(result).toHaveProperty("code");
|
|
96
|
-
expect(typeof result.code).toBe("string");
|
|
97
|
-
expect(result.code.length).toBeGreaterThan(0);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
(plugin as any).buildEnd?.call({});
|
|
101
|
-
});
|
|
102
|
-
});
|
|
@@ -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
|
-
});
|