@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.
Files changed (63) hide show
  1. package/dist/commands/init/generators/client.d.ts.map +1 -1
  2. package/dist/commands/init/generators/client.js +3 -0
  3. package/dist/commands/init/generators/client.js.map +1 -1
  4. package/dist/commands/init/normalize.d.ts.map +1 -1
  5. package/dist/commands/init/normalize.js +1 -0
  6. package/dist/commands/init/normalize.js.map +1 -1
  7. package/dist/commands/init/prompts.d.ts.map +1 -1
  8. package/dist/commands/init/prompts.js +8 -1
  9. package/dist/commands/init/prompts.js.map +1 -1
  10. package/dist/commands/init/types.d.ts +3 -0
  11. package/dist/commands/init/types.d.ts.map +1 -1
  12. package/dist/engines/EsbuildClientEngine.d.ts.map +1 -1
  13. package/dist/engines/EsbuildClientEngine.js +1 -0
  14. package/dist/engines/EsbuildClientEngine.js.map +1 -1
  15. package/dist/esbuild/esbuild-client-config.d.ts.map +1 -1
  16. package/dist/esbuild/esbuild-client-config.js +2 -11
  17. package/dist/esbuild/esbuild-client-config.js.map +1 -1
  18. package/dist/esbuild/esbuild-postcss-plugin.d.ts +4 -0
  19. package/dist/esbuild/esbuild-postcss-plugin.d.ts.map +1 -1
  20. package/dist/esbuild/esbuild-postcss-plugin.js +15 -0
  21. package/dist/esbuild/esbuild-postcss-plugin.js.map +1 -1
  22. package/dist/esbuild/esbuild-ssr-config.d.ts +27 -0
  23. package/dist/esbuild/esbuild-ssr-config.d.ts.map +1 -0
  24. package/dist/esbuild/esbuild-ssr-config.js +113 -0
  25. package/dist/esbuild/esbuild-ssr-config.js.map +1 -0
  26. package/dist/sd-config.types.d.ts +7 -0
  27. package/dist/sd-config.types.d.ts.map +1 -1
  28. package/dist/ssg/prerender.d.ts +19 -0
  29. package/dist/ssg/prerender.d.ts.map +1 -0
  30. package/dist/ssg/prerender.js +43 -0
  31. package/dist/ssg/prerender.js.map +1 -0
  32. package/dist/workers/client.worker.d.ts +2 -0
  33. package/dist/workers/client.worker.d.ts.map +1 -1
  34. package/dist/workers/client.worker.js +21 -0
  35. package/dist/workers/client.worker.js.map +1 -1
  36. package/package.json +6 -6
  37. package/src/commands/init/generators/client.ts +8 -0
  38. package/src/commands/init/normalize.ts +1 -0
  39. package/src/commands/init/prompts.ts +9 -1
  40. package/src/commands/init/templates/client/package.json.hbs +2 -1
  41. package/src/commands/init/templates/client/src/main.server.ts.hbs +24 -0
  42. package/src/commands/init/templates/client/src/main.ts.hbs +13 -0
  43. package/src/commands/init/templates/workspace-root/sd.config.ts.hbs +3 -0
  44. package/src/commands/init/types.ts +3 -0
  45. package/src/engines/EsbuildClientEngine.ts +1 -0
  46. package/src/esbuild/esbuild-client-config.ts +2 -12
  47. package/src/esbuild/esbuild-postcss-plugin.ts +18 -0
  48. package/src/esbuild/esbuild-ssr-config.ts +149 -0
  49. package/src/sd-config.types.ts +7 -0
  50. package/src/ssg/prerender.ts +65 -0
  51. package/src/workers/client.worker.ts +26 -0
  52. package/tests/engines/base-engine.spec.ts +1 -26
  53. package/tests/init/__snapshots__/render.spec.ts.snap +38 -20
  54. package/tests/init/render.spec.ts +113 -33
  55. package/tests/utils/hmr-client-script.acc.spec.ts +0 -21
  56. package/tests/angular/vite-angular-plugin.spec.ts +0 -102
  57. package/tests/engines/engine-adapter-isolation.spec.ts +0 -79
  58. package/tests/runtime/signal-handler.spec.ts +0 -21
  59. package/tests/utils/angular-build.spec.ts +0 -109
  60. package/tests/utils/esbuild-client-config.acc.spec.ts +0 -438
  61. package/tests/utils/esbuild-client-config.spec.ts +0 -659
  62. package/tests/utils/hmr-client-script.spec.ts +0 -44
  63. package/tests/utils/tsconfig-angular.spec.ts +0 -9
@@ -14,8 +14,7 @@ vi.spyOn(Worker, "create").mockReturnValue(mockWorker as any);
14
14
  import { TscEngine } from "../../src/engines/TscEngine";
15
15
  import { BaseEngine } from "../../src/engines/BaseEngine";
16
16
 
17
- import type { BuildPackageInfo, BuildOutput } from "../../src/engines/types";
18
- import type { BuildResult } from "../../src/runtime/ResultCollector";
17
+ import type { BuildPackageInfo } from "../../src/engines/types";
19
18
 
20
19
  // --- Helpers ---
21
20
 
@@ -373,18 +372,6 @@ describe("BaseEngine", () => {
373
372
  });
374
373
 
375
374
  describe("lint 통합", () => {
376
- describe("BuildOutput.lint flag controls lint execution", () => {
377
- it("BuildOutput이 lint 불리언 플래그를 수용", () => {
378
- const output: BuildOutput = { js: true, dts: true, lint: true };
379
- expect(output.lint).toBe(true);
380
- });
381
-
382
- it("BuildOutput.lint는 미설정 시 undefined가 기본값", () => {
383
- const output: BuildOutput = { js: true, dts: true };
384
- expect(output.lint).toBeUndefined();
385
- });
386
- });
387
-
388
375
  describe("EngineResult includes lint field", () => {
389
376
  it("run() returns EngineResult with lint field when worker provides it", async () => {
390
377
  mockWorker.build.mockResolvedValue({
@@ -496,17 +483,5 @@ describe("BaseEngine", () => {
496
483
  await engine.stop();
497
484
  });
498
485
  });
499
-
500
- describe("ResultCollector supports lint type", () => {
501
- it("BuildResult 타입에 lint 포함", () => {
502
- const lintResult: BuildResult = {
503
- name: "test-pkg",
504
- target: "node",
505
- type: "lint",
506
- status: "success",
507
- };
508
- expect(lintResult.type).toBe("lint");
509
- });
510
- });
511
486
  });
512
487
  });
@@ -1,68 +1,82 @@
1
1
  // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
3
  exports[`client/src/main.ts.hbs > 라우팅 N (mobile) 1`] = `
4
- "import consola from "consola";
5
- import { enableProdMode, inject, provideAppInitializer } from "@angular/core";
4
+ "import { enableProdMode, inject, provideAppInitializer } from "@angular/core";
6
5
  import { bootstrapApplication } from "@angular/platform-browser";
7
6
  import { provideHttpClient, withFetch } from "@angular/common/http";
8
- import { provideSdAngular } from "@simplysm/angular";
9
- import { AppServiceProvider } from "@demo/client-common";
7
+ import {
8
+ provideSdAngular,
9
+ } from "@simplysm/angular";
10
+ import { createLogger } from "@simplysm/core-common";
10
11
  import { AppRoot } from "./app.root";
11
12
  import "./styles.scss";
13
+ import {
14
+ AppServiceProvider,
15
+ } from "@demo/client-common";
16
+
17
+ const CLIENT_NAME = "client-pda";
18
+
19
+ const logger = createLogger(CLIENT_NAME);
12
20
 
13
21
  if (typeof ngDevMode !== "undefined" && !ngDevMode) {
14
22
  enableProdMode();
15
23
  } else {
16
- consola.log("dev mode");
24
+ logger.info("dev mode");
17
25
  }
18
26
 
19
27
  bootstrapApplication(AppRoot, {
20
28
  providers: [
21
29
  provideHttpClient(withFetch()),
22
- provideSdAngular({ clientName: "client-pda" }),
30
+ provideSdAngular({ clientName: CLIENT_NAME }),
23
31
 
24
- //-- app-service 연결
25
32
  provideAppInitializer(async () => {
26
33
  await inject(AppServiceProvider).connectAsync();
27
34
  }),
28
35
  ],
29
36
  }).catch((err: unknown) => {
30
- consola.error(err);
37
+ logger.error("부트스트랩 실패", err);
31
38
  });
32
39
  "
33
40
  `;
34
41
 
35
42
  exports[`client/src/main.ts.hbs > 라우팅 Y (web) 1`] = `
36
- "import consola from "consola";
37
- import { enableProdMode, inject, provideAppInitializer } from "@angular/core";
43
+ "import { enableProdMode, inject, provideAppInitializer } from "@angular/core";
38
44
  import { bootstrapApplication } from "@angular/platform-browser";
39
45
  import { provideHttpClient, withFetch } from "@angular/common/http";
40
46
  import { provideRouter, withHashLocation } from "@angular/router";
41
- import { provideSdAngular } from "@simplysm/angular";
42
- import { AppServiceProvider } from "@demo/client-common";
47
+ import {
48
+ provideSdAngular,
49
+ } from "@simplysm/angular";
50
+ import { createLogger } from "@simplysm/core-common";
43
51
  import { AppRoot } from "./app.root";
44
52
  import { routes } from "./routes";
45
53
  import "./styles.scss";
54
+ import {
55
+ AppServiceProvider,
56
+ } from "@demo/client-common";
57
+
58
+ const CLIENT_NAME = "client-admin";
59
+
60
+ const logger = createLogger(CLIENT_NAME);
46
61
 
47
62
  if (typeof ngDevMode !== "undefined" && !ngDevMode) {
48
63
  enableProdMode();
49
64
  } else {
50
- consola.log("dev mode");
65
+ logger.info("dev mode");
51
66
  }
52
67
 
53
68
  bootstrapApplication(AppRoot, {
54
69
  providers: [
55
70
  provideHttpClient(withFetch()),
56
71
  provideRouter(routes, withHashLocation()),
57
- provideSdAngular({ clientName: "client-admin" }),
72
+ provideSdAngular({ clientName: CLIENT_NAME }),
58
73
 
59
- //-- app-service 연결
60
74
  provideAppInitializer(async () => {
61
75
  await inject(AppServiceProvider).connectAsync();
62
76
  }),
63
77
  ],
64
78
  }).catch((err: unknown) => {
65
- consola.error(err);
79
+ logger.error("부트스트랩 실패", err);
66
80
  });
67
81
  "
68
82
  `;
@@ -177,7 +191,7 @@ exports[`server/src/main.ts.hbs > DB=N — OrmService 없음 1`] = `
177
191
  import { env, num, parseBoolEnv } from "@simplysm/core-common";
178
192
  import { setupConsola } from "@simplysm/core-node";
179
193
  import { createServiceServer, getConfig } from "@simplysm/service-server";
180
- import {EventEmitter} from "node:events";
194
+ import { EventEmitter } from "node:events";
181
195
 
182
196
  Error.stackTraceLimit = Infinity;
183
197
  EventEmitter.defaultMaxListeners = 0;
@@ -194,7 +208,8 @@ if (authConfig?.jwtSecret == null) {
194
208
 
195
209
  export const server = createServiceServer({
196
210
  rootPath: import.meta.dirname,
197
- services: [],
211
+ services: [
212
+ ],
198
213
  port: num.parseInt(env("SERVER_PORT"))!,
199
214
  auth: { jwtSecret: authConfig.jwtSecret },
200
215
  });
@@ -210,7 +225,7 @@ exports[`server/src/main.ts.hbs > DB=Y — OrmService 포함 1`] = `
210
225
  import { env, num, parseBoolEnv } from "@simplysm/core-common";
211
226
  import { setupConsola } from "@simplysm/core-node";
212
227
  import { createServiceServer, getConfig, OrmService } from "@simplysm/service-server";
213
- import {EventEmitter} from "node:events";
228
+ import { EventEmitter } from "node:events";
214
229
  import { DevService } from "./services/dev.service";
215
230
 
216
231
  Error.stackTraceLimit = Infinity;
@@ -228,7 +243,10 @@ if (authConfig?.jwtSecret == null) {
228
243
 
229
244
  export const server = createServiceServer({
230
245
  rootPath: import.meta.dirname,
231
- services: [OrmService, ...(parseBoolEnv(env("DEV")) ? [DevService] : [])],
246
+ services: [
247
+ OrmService,
248
+ ...(parseBoolEnv(env("DEV")) ? [DevService] : []),
249
+ ],
232
250
  port: num.parseInt(env("SERVER_PORT"))!,
233
251
  auth: { jwtSecret: authConfig.jwtSecret },
234
252
  });
@@ -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
- 'services: [OrmService, ...(parseBoolEnv(env("DEV")) ? [DevService] : [])],',
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
- 'services: [OrmService, AuthService, ...(parseBoolEnv(env("DEV")) ? [DevService] : [])],',
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(cfg.employeeId, e.id)");
212
- expect(out).toContain("(configs as Record<string, unknown>)[cfg.code] = JSON.parse(cfg.valueJson);");
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 없음", async () => {
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 appStructureItems: AppStructureItem[] = [");
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 { appStructureItems } from "@demo/common";');
452
- expect(out).toContain("inject(SdAppStructureProvider).initialize(appStructureItems);");
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('import type { AuthServiceMethods } from "@demo/server";');
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(this.authInfo()?.permissions);");
568
- expect(out).toContain("async login(email: string, password: string): Promise<void>");
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 { AppAuthProvider } from "./providers/app-auth.provider";');
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
- 'export const AuthInfoChangedEvent = defineEvent<{ employeeId: number }, void>("AuthInfoChanged");',
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("private async _registerAuthEvent(employeeId: number): Promise<void> {");
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(appStructureItems, undefined)");
1146
- expect(out).toContain("// TODO: 업무 테이블 초기 데이터 시드 추가");
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 { DevServiceMethods } from "./services/dev.service";');
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 { DevServiceMethods } from "./services/dev.service";');
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('import type { DevServiceMethods } from "@demo/server";');
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('@HostListener("document:keydown", ["$event"])');
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("HostListener");
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 호출 (아이콘 의존 없음)", async () => {
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).not.toContain("@ng-icons");
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,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
- });