@simplysm/sd-cli 12.16.38 → 12.16.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.
@@ -57,6 +57,7 @@ export class SdClientBuildRunner extends SdBuildRunnerBase {
57
57
  builderType: builderType,
58
58
  builderConfig: this._pkgConf.builder?.[builderType],
59
59
  env: this._pkgConf.env,
60
+ noLazyRoute: this._pkgConf.noLazyRoute,
60
61
  }));
61
62
  }
62
63
  if (modifiedFileSet) {
@@ -34,5 +34,6 @@ interface IConf<T extends keyof NonNullable<ISdClientPackageConfig["builder"]>>
34
34
  builderType: T;
35
35
  builderConfig: NonNullable<ISdClientPackageConfig["builder"]>[T];
36
36
  env: Record<string, string> | undefined;
37
+ noLazyRoute: boolean | undefined;
37
38
  }
38
39
  export {};
@@ -360,7 +360,7 @@ export class SdNgBundler {
360
360
  ...(!this._opt.watch?.dev ? { ngDevMode: "false" } : {}),
361
361
  "ngJitMode": "false",
362
362
  "global": "global",
363
- "process": "process",
363
+ // "process": "process",
364
364
  "Buffer": "Buffer",
365
365
  "process.env.SD_VERSION": JSON.stringify(this._pkgNpmConf.version),
366
366
  "process.env.NODE_ENV": JSON.stringify(this._opt.watch?.dev ? "development" : "production"),
@@ -372,7 +372,7 @@ export class SdNgBundler {
372
372
  entryNames: "[dir]/[name]",
373
373
  entryPoints: {
374
374
  "sd-polyfills": "virtual:sd-polyfills",
375
- main: this._mainFilePath,
375
+ "main": this._mainFilePath,
376
376
  ...(FsUtils.exists(path.resolve(this._opt.pkgPath, "src/polyfills.ts"))
377
377
  ? {
378
378
  polyfills: path.resolve(this._opt.pkgPath, "src/polyfills.ts"),
@@ -426,8 +426,9 @@ export class SdNgBundler {
426
426
  : {
427
427
  platform: "browser",
428
428
  target: this._browserTarget,
429
+ ...(this._conf.noLazyRoute ? {} : { supported: { "dynamic-import": true } }),
429
430
  format: "esm",
430
- splitting: true,
431
+ splitting: !this._conf.noLazyRoute,
431
432
  inject: [
432
433
  PathUtils.posix(fileURLToPath(import.meta.resolve("node-stdlib-browser/helpers/esbuild/shim"))),
433
434
  ],
@@ -1,7 +1,9 @@
1
1
  import compat from "core-js-compat";
2
2
  import { createRequire } from "module";
3
- import { PathUtils } from "@simplysm/sd-core-node";
3
+ import { fileURLToPath } from "url";
4
+ import path from "path";
4
5
  const _require = createRequire(import.meta.url);
6
+ const _resolveDir = path.dirname(fileURLToPath(import.meta.url));
5
7
  const SD_POLYFILL_NS = "sd-polyfill";
6
8
  const SD_POLYFILL_FILTER = /^virtual:sd-polyfills$/;
7
9
  export function SdPolyfillPlugin(browserslistQuery) {
@@ -17,11 +19,11 @@ export function SdPolyfillPlugin(browserslistQuery) {
17
19
  targets: browserslistQuery,
18
20
  });
19
21
  const lines = [];
20
- // core-js 모듈 (절대 경로로 resolve하여 소비 프로젝트의 node_modules 의존 제거)
22
+ // core-js 모듈 (bare specifier + resolveDir로 소비 프로젝트의 node_modules 의존 제거)
21
23
  for (const mod of list) {
22
24
  try {
23
- const absPath = _require.resolve(`core-js/modules/${mod}.js`);
24
- lines.push(`import "${PathUtils.posix(absPath)}";`);
25
+ _require.resolve(`core-js/modules/${mod}.js`);
26
+ lines.push(`import "core-js/modules/${mod}.js";`);
25
27
  }
26
28
  catch {
27
29
  // 모듈이 존재하지 않으면 skip (WeakRef 등 polyfill 불가 항목)
@@ -29,22 +31,30 @@ export function SdPolyfillPlugin(browserslistQuery) {
29
31
  }
30
32
  // AbortController (Chrome 66+)
31
33
  try {
32
- const absPath = _require.resolve("abortcontroller-polyfill/dist/abortcontroller-polyfill-only");
33
- lines.push(`import "${PathUtils.posix(absPath)}";`);
34
+ _require.resolve("abortcontroller-polyfill/dist/abortcontroller-polyfill-only");
35
+ lines.push(`import "abortcontroller-polyfill/dist/abortcontroller-polyfill-only";`);
34
36
  }
35
37
  catch {
36
38
  // skip
37
39
  }
38
40
  // ResizeObserver (Chrome 64+)
39
41
  try {
40
- const absPath = _require.resolve("resize-observer-polyfill");
41
- lines.push(`import RO from "${PathUtils.posix(absPath)}";`);
42
+ _require.resolve("resize-observer-polyfill");
43
+ lines.push(`import RO from "resize-observer-polyfill";`);
42
44
  lines.push(`if (typeof window !== "undefined" && !("ResizeObserver" in window)) { window.ResizeObserver = RO; }`);
43
45
  }
44
46
  catch {
45
47
  // skip
46
48
  }
47
- return { contents: lines.join("\n"), loader: "js" };
49
+ // TransformStream / ReadableStream / WritableStream (Chrome 67+)
50
+ try {
51
+ _require.resolve("web-streams-polyfill/polyfill");
52
+ lines.push(`import "web-streams-polyfill/polyfill";`);
53
+ }
54
+ catch {
55
+ // skip
56
+ }
57
+ return { contents: lines.join("\n"), loader: "js", resolveDir: _resolveDir };
48
58
  });
49
59
  },
50
60
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simplysm/sd-cli",
3
- "version": "12.16.38",
3
+ "version": "12.16.40",
4
4
  "description": "심플리즘 패키지 - CLI",
5
5
  "author": "김석래",
6
6
  "repository": {
@@ -17,10 +17,10 @@
17
17
  "@angular/compiler-cli": "^20.3.18",
18
18
  "@anthropic-ai/sdk": "^0.78.0",
19
19
  "@electron/rebuild": "^4.0.3",
20
- "@simplysm/sd-core-common": "12.16.38",
21
- "@simplysm/sd-core-node": "12.16.38",
22
- "@simplysm/sd-service-server": "12.16.38",
23
- "@simplysm/sd-storage": "12.16.38",
20
+ "@simplysm/sd-core-common": "12.16.40",
21
+ "@simplysm/sd-core-node": "12.16.40",
22
+ "@simplysm/sd-service-server": "12.16.40",
23
+ "@simplysm/sd-storage": "12.16.40",
24
24
  "abortcontroller-polyfill": "^1.7.8",
25
25
  "browserslist": "^4.28.1",
26
26
  "cordova": "^13.0.0",
@@ -42,6 +42,7 @@
42
42
  "ts-morph": "^27.0.2",
43
43
  "tslib": "^2.8.1",
44
44
  "typescript": "~5.8.3",
45
+ "web-streams-polyfill": "^4.2.0",
45
46
  "yargs": "^18.0.0"
46
47
  },
47
48
  "devDependencies": {
@@ -83,6 +83,7 @@ export class SdClientBuildRunner extends SdBuildRunnerBase<"client"> {
83
83
  builderType: builderType,
84
84
  builderConfig: this._pkgConf.builder?.[builderType],
85
85
  env: this._pkgConf.env,
86
+ noLazyRoute: this._pkgConf.noLazyRoute,
86
87
  }),
87
88
  );
88
89
  }
@@ -475,7 +475,7 @@ export class SdNgBundler {
475
475
  ...(!this._opt.watch?.dev ? { ngDevMode: "false" } : {}),
476
476
  "ngJitMode": "false",
477
477
  "global": "global",
478
- "process": "process",
478
+ // "process": "process",
479
479
  "Buffer": "Buffer",
480
480
  "process.env.SD_VERSION": JSON.stringify(this._pkgNpmConf.version),
481
481
  "process.env.NODE_ENV": JSON.stringify(this._opt.watch?.dev ? "development" : "production"),
@@ -490,7 +490,7 @@ export class SdNgBundler {
490
490
  entryNames: "[dir]/[name]",
491
491
  entryPoints: {
492
492
  "sd-polyfills": "virtual:sd-polyfills",
493
- main: this._mainFilePath,
493
+ "main": this._mainFilePath,
494
494
  ...(FsUtils.exists(path.resolve(this._opt.pkgPath, "src/polyfills.ts"))
495
495
  ? {
496
496
  polyfills: path.resolve(this._opt.pkgPath, "src/polyfills.ts"),
@@ -549,8 +549,9 @@ export class SdNgBundler {
549
549
  : {
550
550
  platform: "browser",
551
551
  target: this._browserTarget,
552
+ ...(this._conf.noLazyRoute ? {} : { supported: { "dynamic-import": true } }),
552
553
  format: "esm",
553
- splitting: true,
554
+ splitting: !this._conf.noLazyRoute,
554
555
  inject: [
555
556
  PathUtils.posix(
556
557
  fileURLToPath(import.meta.resolve("node-stdlib-browser/helpers/esbuild/shim")),
@@ -667,4 +668,5 @@ interface IConf<T extends keyof NonNullable<ISdClientPackageConfig["builder"]>>
667
668
  builderType: T;
668
669
  builderConfig: NonNullable<ISdClientPackageConfig["builder"]>[T];
669
670
  env: Record<string, string> | undefined;
671
+ noLazyRoute: boolean | undefined;
670
672
  }
@@ -1,9 +1,11 @@
1
1
  import { Plugin, PluginBuild } from "esbuild";
2
2
  import compat from "core-js-compat";
3
3
  import { createRequire } from "module";
4
- import { PathUtils } from "@simplysm/sd-core-node";
4
+ import { fileURLToPath } from "url";
5
+ import path from "path";
5
6
 
6
7
  const _require = createRequire(import.meta.url);
8
+ const _resolveDir = path.dirname(fileURLToPath(import.meta.url));
7
9
 
8
10
  const SD_POLYFILL_NS = "sd-polyfill";
9
11
  const SD_POLYFILL_FILTER = /^virtual:sd-polyfills$/;
@@ -24,11 +26,11 @@ export function SdPolyfillPlugin(browserslistQuery: string[]): Plugin {
24
26
 
25
27
  const lines: string[] = [];
26
28
 
27
- // core-js 모듈 (절대 경로로 resolve하여 소비 프로젝트의 node_modules 의존 제거)
29
+ // core-js 모듈 (bare specifier + resolveDir로 소비 프로젝트의 node_modules 의존 제거)
28
30
  for (const mod of list) {
29
31
  try {
30
- const absPath = _require.resolve(`core-js/modules/${mod}.js`);
31
- lines.push(`import "${PathUtils.posix(absPath)}";`);
32
+ _require.resolve(`core-js/modules/${mod}.js`);
33
+ lines.push(`import "core-js/modules/${mod}.js";`);
32
34
  } catch {
33
35
  // 모듈이 존재하지 않으면 skip (WeakRef 등 polyfill 불가 항목)
34
36
  }
@@ -36,18 +38,16 @@ export function SdPolyfillPlugin(browserslistQuery: string[]): Plugin {
36
38
 
37
39
  // AbortController (Chrome 66+)
38
40
  try {
39
- const absPath = _require.resolve(
40
- "abortcontroller-polyfill/dist/abortcontroller-polyfill-only",
41
- );
42
- lines.push(`import "${PathUtils.posix(absPath)}";`);
41
+ _require.resolve("abortcontroller-polyfill/dist/abortcontroller-polyfill-only");
42
+ lines.push(`import "abortcontroller-polyfill/dist/abortcontroller-polyfill-only";`);
43
43
  } catch {
44
44
  // skip
45
45
  }
46
46
 
47
47
  // ResizeObserver (Chrome 64+)
48
48
  try {
49
- const absPath = _require.resolve("resize-observer-polyfill");
50
- lines.push(`import RO from "${PathUtils.posix(absPath)}";`);
49
+ _require.resolve("resize-observer-polyfill");
50
+ lines.push(`import RO from "resize-observer-polyfill";`);
51
51
  lines.push(
52
52
  `if (typeof window !== "undefined" && !("ResizeObserver" in window)) { window.ResizeObserver = RO; }`,
53
53
  );
@@ -55,7 +55,15 @@ export function SdPolyfillPlugin(browserslistQuery: string[]): Plugin {
55
55
  // skip
56
56
  }
57
57
 
58
- return { contents: lines.join("\n"), loader: "js" };
58
+ // TransformStream / ReadableStream / WritableStream (Chrome 67+)
59
+ try {
60
+ _require.resolve("web-streams-polyfill/polyfill");
61
+ lines.push(`import "web-streams-polyfill/polyfill";`);
62
+ } catch {
63
+ // skip
64
+ }
65
+
66
+ return { contents: lines.join("\n"), loader: "js", resolveDir: _resolveDir };
59
67
  });
60
68
  },
61
69
  };
@@ -0,0 +1,87 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { SdPolyfillPlugin } from "../../src/pkg-builders/client/SdPolyfillPlugin";
3
+
4
+ function captureOnLoad(browserslistQuery: string[]) {
5
+ const plugin = SdPolyfillPlugin(browserslistQuery);
6
+
7
+ let onLoadHandler: (() => { contents: string; loader: string; resolveDir?: string }) | undefined;
8
+ const mockBuild = {
9
+ onResolve: () => {},
10
+ onLoad: (_opts: unknown, handler: () => { contents: string; loader: string; resolveDir?: string }) => {
11
+ onLoadHandler = handler;
12
+ },
13
+ };
14
+
15
+ plugin.setup(mockBuild as never);
16
+ return onLoadHandler!();
17
+ }
18
+
19
+ describe("SdPolyfillPlugin", () => {
20
+ describe("가상 polyfill 모듈의 import가 OS 무관하게 resolve되어야 한다", () => {
21
+ it("모든 import가 bare specifier이다 (절대 경로 없음)", () => {
22
+ const result = captureOnLoad(["Chrome >= 61"]);
23
+ const importLines = result.contents.split("\n").filter((l) => l.startsWith("import"));
24
+
25
+ expect(importLines.length).toBeGreaterThan(0);
26
+ for (const line of importLines) {
27
+ expect(line).not.toMatch(/["'][A-Z]:\//i); // Windows 드라이브 문자 금지
28
+ expect(line).not.toMatch(/["']\//); // Unix 절대 경로 금지
29
+ }
30
+ // core-js bare specifier 사용
31
+ expect(result.contents).toContain('import "core-js/modules/');
32
+ });
33
+
34
+ it("abortcontroller-polyfill import에 절대 경로가 포함되지 않는다", () => {
35
+ const result = captureOnLoad(["Chrome >= 61"]);
36
+
37
+ expect(result.contents).toContain("abortcontroller-polyfill");
38
+ // 절대 경로가 아닌 bare specifier
39
+ const abortLines = result.contents.split("\n").filter((l) => l.includes("abortcontroller"));
40
+ for (const line of abortLines) {
41
+ expect(line).not.toMatch(/[A-Z]:\//i);
42
+ }
43
+ });
44
+
45
+ it("resize-observer-polyfill import에 절대 경로가 포함되지 않는다", () => {
46
+ const result = captureOnLoad(["Chrome >= 61"]);
47
+
48
+ expect(result.contents).toContain("resize-observer-polyfill");
49
+ const roLines = result.contents.split("\n").filter((l) => l.includes("resize-observer"));
50
+ for (const line of roLines) {
51
+ expect(line).not.toMatch(/[A-Z]:\//i);
52
+ }
53
+ });
54
+
55
+ it("web-streams-polyfill import에 절대 경로가 포함되지 않는다", () => {
56
+ const result = captureOnLoad(["Chrome >= 61"]);
57
+
58
+ expect(result.contents).toContain("web-streams-polyfill");
59
+ const wsLines = result.contents.split("\n").filter((l) => l.includes("web-streams-polyfill"));
60
+ for (const line of wsLines) {
61
+ expect(line).not.toMatch(/[A-Z]:\//i);
62
+ }
63
+ });
64
+
65
+ it("resolveDir이 설정되어야 한다", () => {
66
+ const result = captureOnLoad(["Chrome >= 61"]);
67
+
68
+ expect(result.resolveDir).toBeDefined();
69
+ expect(typeof result.resolveDir).toBe("string");
70
+ expect(result.resolveDir!.length).toBeGreaterThan(0);
71
+ });
72
+ });
73
+
74
+ describe("core-js-compat 기반 동적 polyfill 목록이 유지되어야 한다", () => {
75
+ it("browserslistQuery에 따라 core-js 모듈이 포함된다", () => {
76
+ const result = captureOnLoad(["Chrome >= 61"]);
77
+
78
+ // Chrome 61에서 지원하지 않는 ES feature가 포함되어야 함
79
+ expect(result.contents).toContain("core-js/modules/");
80
+ });
81
+
82
+ it("존재하지 않는 모듈은 skip되고 에러 없이 완료된다", () => {
83
+ // core-js-compat이 반환하는 모듈 중 실제 존재하지 않는 것이 있어도 에러 없이 완료
84
+ expect(() => captureOnLoad(["Chrome >= 61"])).not.toThrow();
85
+ });
86
+ });
87
+ });
@@ -3,6 +3,7 @@ import * as ts from "typescript";
3
3
  import { PathUtils, TNormPath } from "@simplysm/sd-core-node";
4
4
  import { SdDepCache } from "../../src/ts-compiler/SdDepCache";
5
5
  import { SdDepAnalyzer } from "../../src/ts-compiler/SdDepAnalyzer";
6
+ import { ScopePathSet } from "../../src/ts-compiler/ScopePathSet";
6
7
 
7
8
  function createMockProgram(sources: Record<string, string>) {
8
9
  const fileNames = Object.keys(sources);
@@ -14,6 +15,11 @@ function createMockProgram(sources: Record<string, string>) {
14
15
  if (!sourceText) return undefined;
15
16
  return ts.createSourceFile(fileName, sourceText, languageVersion);
16
17
  };
18
+ const moduleResolutionCache = ts.createModuleResolutionCache(
19
+ compilerHost.getCurrentDirectory(),
20
+ (x) => x,
21
+ );
22
+ compilerHost.getModuleResolutionCache = () => moduleResolutionCache;
17
23
  const program = ts.createProgram(fileNames, {}, compilerHost);
18
24
  return { program, compilerHost };
19
25
  }
@@ -22,8 +28,27 @@ function norm(path: string): TNormPath {
22
28
  return PathUtils.norm(path);
23
29
  }
24
30
 
31
+ function getAffectedFileSet(depCache: SdDepCache, modifiedSet: Set<TNormPath>): Set<TNormPath> {
32
+ const map = depCache.getAffectedFileMap(modifiedSet);
33
+ const result = new Set<TNormPath>();
34
+ for (const set of map.values()) {
35
+ for (const p of set) result.add(p);
36
+ }
37
+ return result;
38
+ }
39
+
40
+ function createCache(dep: SdDepCache) {
41
+ return {
42
+ dep,
43
+ type: new WeakMap<ts.Node, ts.Type | undefined>(),
44
+ prop: new WeakMap<ts.Type, Map<string, ts.Symbol | undefined>>(),
45
+ declFiles: new WeakMap<ts.Symbol, TNormPath[]>(),
46
+ ngOrg: new Map<TNormPath, ts.SourceFile>(),
47
+ };
48
+ }
49
+
25
50
  describe("DependencyAnalyzer", () => {
26
- const scope = PathUtils.norm("/");
51
+ const scopePathSet = new ScopePathSet([PathUtils.norm("/")]);
27
52
  let depCache: SdDepCache;
28
53
 
29
54
  beforeEach(() => {
@@ -37,9 +62,9 @@ describe("DependencyAnalyzer", () => {
37
62
  };
38
63
 
39
64
  const { program, compilerHost } = createMockProgram(files);
40
- SdDepAnalyzer.analyze(program, compilerHost, [scope], depCache);
65
+ SdDepAnalyzer.analyze(program, compilerHost, scopePathSet, createCache(depCache));
41
66
 
42
- const result = depCache.getAffectedFileSet(new Set([norm("/a.ts")]));
67
+ const result = getAffectedFileSet(depCache, new Set([norm("/a.ts")]));
43
68
  expect(result).toEqual(new Set([norm("/a.ts"), norm("/b.ts")]));
44
69
  });
45
70
 
@@ -51,21 +76,17 @@ describe("DependencyAnalyzer", () => {
51
76
  };
52
77
 
53
78
  const { program, compilerHost } = createMockProgram(files);
54
- SdDepAnalyzer.analyze(program, compilerHost, [scope], depCache);
79
+ SdDepAnalyzer.analyze(program, compilerHost, scopePathSet, createCache(depCache));
55
80
 
56
81
  {
57
- const result = depCache.getAffectedFileSet(new Set([norm("/a.ts")]));
82
+ const result = getAffectedFileSet(depCache, new Set([norm("/a.ts")]));
58
83
  expect(result).toEqual(
59
- new Set([
60
- norm("/a.ts"),
61
- norm("/b.ts"), // a파일이 사라지거나 하면 b가 오류를 뱉어야 하므로
62
- norm("/c.ts"),
63
- ]),
84
+ new Set([norm("/a.ts"), norm("/b.ts"), norm("/c.ts")]),
64
85
  );
65
86
  }
66
87
 
67
88
  {
68
- const result = depCache.getAffectedFileSet(new Set([norm("/b.ts")]));
89
+ const result = getAffectedFileSet(depCache, new Set([norm("/b.ts")]));
69
90
  expect(result).toEqual(new Set([norm("/b.ts"), norm("/c.ts")]));
70
91
  }
71
92
  });
@@ -78,21 +99,17 @@ describe("DependencyAnalyzer", () => {
78
99
  };
79
100
 
80
101
  const { program, compilerHost } = createMockProgram(files);
81
- SdDepAnalyzer.analyze(program, compilerHost, [scope], depCache);
102
+ SdDepAnalyzer.analyze(program, compilerHost, scopePathSet, createCache(depCache));
82
103
 
83
104
  {
84
- const result = depCache.getAffectedFileSet(new Set([norm("/a.ts")]));
105
+ const result = getAffectedFileSet(depCache, new Set([norm("/a.ts")]));
85
106
  expect(result).toEqual(
86
- new Set([
87
- norm("/a.ts"),
88
- norm("/b.ts"), // a파일이 사라지거나 하면 b가 오류를 뱉어야 하므로
89
- norm("/c.ts"),
90
- ]),
107
+ new Set([norm("/a.ts"), norm("/b.ts"), norm("/c.ts")]),
91
108
  );
92
109
  }
93
110
 
94
111
  {
95
- const result = depCache.getAffectedFileSet(new Set([norm("/b.ts")]));
112
+ const result = getAffectedFileSet(depCache, new Set([norm("/b.ts")]));
96
113
  expect(result).toEqual(new Set([norm("/b.ts"), norm("/c.ts")]));
97
114
  }
98
115
  });
@@ -106,15 +123,15 @@ describe("DependencyAnalyzer", () => {
106
123
  };
107
124
 
108
125
  const { program, compilerHost } = createMockProgram(files);
109
- SdDepAnalyzer.analyze(program, compilerHost, [scope], depCache);
126
+ SdDepAnalyzer.analyze(program, compilerHost, scopePathSet, createCache(depCache));
110
127
 
111
128
  {
112
- const result = depCache.getAffectedFileSet(new Set([norm("/a.ts")]));
129
+ const result = getAffectedFileSet(depCache, new Set([norm("/a.ts")]));
113
130
  expect(result).toEqual(new Set([norm("/a.ts"), norm("/b.ts"), norm("/c.ts")]));
114
131
  }
115
132
 
116
133
  {
117
- const result = depCache.getAffectedFileSet(new Set([norm("/a_1.ts")]));
134
+ const result = getAffectedFileSet(depCache, new Set([norm("/a_1.ts")]));
118
135
  expect(result).toEqual(new Set([norm("/a_1.ts"), norm("/b.ts")]));
119
136
  }
120
137
  });
@@ -134,19 +151,17 @@ describe("DependencyAnalyzer", () => {
134
151
  `,
135
152
  "/c.ts": `
136
153
  import { A } from "./a";
137
-
154
+
138
155
  function doSomething() {
139
- // A.b.c 속성 접근을 통해 B에 간접 의존
140
156
  console.log(new A().b.c);
141
157
  }
142
158
  `,
143
159
  };
144
160
 
145
161
  const { program, compilerHost } = createMockProgram(files);
146
- SdDepAnalyzer.analyze(program, compilerHost, [scope], depCache);
162
+ SdDepAnalyzer.analyze(program, compilerHost, scopePathSet, createCache(depCache));
147
163
 
148
- // B가 변경되면 A와 C 모두 영향 받는지 확인
149
- const result = depCache.getAffectedFileSet(new Set([norm("/b.ts")]));
164
+ const result = getAffectedFileSet(depCache, new Set([norm("/b.ts")]));
150
165
  expect(result).toEqual(new Set([norm("/a.ts"), norm("/b.ts"), norm("/c.ts")]));
151
166
  });
152
167
 
@@ -165,19 +180,17 @@ describe("DependencyAnalyzer", () => {
165
180
  `,
166
181
  "/c.ts": `
167
182
  import { A } from "./a";
168
-
183
+
169
184
  function doSomething() {
170
- // A['b'].c 속성 접근을 통해 B에 간접 의존
171
185
  console.log(new A()['b'].c);
172
186
  }
173
187
  `,
174
188
  };
175
189
 
176
190
  const { program, compilerHost } = createMockProgram(files);
177
- SdDepAnalyzer.analyze(program, compilerHost, [scope], depCache);
191
+ SdDepAnalyzer.analyze(program, compilerHost, scopePathSet, createCache(depCache));
178
192
 
179
- // B가 변경되면 A와 C 모두 영향 받는지 확인
180
- const result = depCache.getAffectedFileSet(new Set([norm("/b.ts")]));
193
+ const result = getAffectedFileSet(depCache, new Set([norm("/b.ts")]));
181
194
  expect(result).toEqual(new Set([norm("/a.ts"), norm("/b.ts"), norm("/c.ts")]));
182
195
  });
183
196
 
@@ -188,9 +201,9 @@ describe("DependencyAnalyzer", () => {
188
201
  };
189
202
 
190
203
  const { program, compilerHost } = createMockProgram(files);
191
- SdDepAnalyzer.analyze(program, compilerHost, [scope], depCache);
204
+ SdDepAnalyzer.analyze(program, compilerHost, scopePathSet, createCache(depCache));
192
205
 
193
- const result = depCache.getAffectedFileSet(new Set([norm("/a.ts")]));
206
+ const result = getAffectedFileSet(depCache, new Set([norm("/a.ts")]));
194
207
  expect(result).toEqual(new Set([norm("/a.ts"), norm("/b.ts")]));
195
208
  });
196
209
 
@@ -202,9 +215,9 @@ describe("DependencyAnalyzer", () => {
202
215
  };
203
216
 
204
217
  const { program, compilerHost } = createMockProgram(files);
205
- SdDepAnalyzer.analyze(program, compilerHost, [scope], depCache);
218
+ SdDepAnalyzer.analyze(program, compilerHost, scopePathSet, createCache(depCache));
206
219
 
207
- const result = depCache.getAffectedFileSet(new Set([norm("/a.ts")]));
220
+ const result = getAffectedFileSet(depCache, new Set([norm("/a.ts")]));
208
221
  expect(result).toEqual(new Set([norm("/a.ts"), norm("/b.ts"), norm("/c.ts")]));
209
222
  });
210
223
 
@@ -215,13 +228,13 @@ describe("DependencyAnalyzer", () => {
215
228
  };
216
229
 
217
230
  const { program, compilerHost } = createMockProgram(files);
218
- SdDepAnalyzer.analyze(program, compilerHost, [scope], depCache);
231
+ SdDepAnalyzer.analyze(program, compilerHost, scopePathSet, createCache(depCache));
219
232
 
220
- const result = depCache.getAffectedFileSet(new Set([norm("/a.ts")]));
233
+ const result = getAffectedFileSet(depCache, new Set([norm("/a.ts")]));
221
234
  expect(result).toEqual(new Set([norm("/a.ts"), norm("/b.ts")]));
222
235
  });
223
236
 
224
- it("invalidates()는 영향 받은 모든 파일을 캐시에서 제거한다", () => {
237
+ it("invalidates()는 해당 파일의 자체 분석 데이터를 제거한다", () => {
225
238
  const files = {
226
239
  "/a.ts": `export const A = 1;`,
227
240
  "/b.ts": `import { A } from "./a";`,
@@ -229,13 +242,12 @@ describe("DependencyAnalyzer", () => {
229
242
  };
230
243
 
231
244
  const { program, compilerHost } = createMockProgram(files);
232
- SdDepAnalyzer.analyze(program, compilerHost, [scope], depCache);
245
+ SdDepAnalyzer.analyze(program, compilerHost, scopePathSet, createCache(depCache));
233
246
 
234
- // invalidate 처리
235
247
  depCache.invalidates(new Set([norm("/a.ts")]));
236
248
 
237
- const affected = depCache.getAffectedFileSet(new Set([norm("/a.ts")]));
238
- // 분석 안된 상태이므로, a만 남아야 함
239
- expect(affected).toEqual(new Set([norm("/a.ts")]));
249
+ // a의 자체 분석 데이터가 제거됨 (재분석 필요 상태)
250
+ expect(depCache["_exportCache"].has(norm("/a.ts"))).toBe(false);
251
+ expect(depCache["_collectedCache"].has(norm("/a.ts"))).toBe(false);
240
252
  });
241
253
  });
@@ -1,14 +1,22 @@
1
1
  import "@simplysm/sd-core-common";
2
- import { PathUtils } from "@simplysm/sd-core-node";
2
+ import { PathUtils, TNormPath } from "@simplysm/sd-core-node";
3
3
  import { beforeEach, describe, expect, it } from "vitest";
4
- import { ISdAffectedFileTreeNode, SdDepCache } from "../../src/ts-compiler/SdDepCache";
4
+ import { SdDepCache } from "../../src/ts-compiler/SdDepCache";
5
+
6
+ function getAffectedFileSet(depCache: SdDepCache, modifiedSet: Set<TNormPath>): Set<TNormPath> {
7
+ const map = depCache.getAffectedFileMap(modifiedSet);
8
+ const result = new Set<TNormPath>();
9
+ for (const set of map.values()) {
10
+ for (const p of set) result.add(p);
11
+ }
12
+ return result;
13
+ }
5
14
 
6
15
  describe("SdDependencyCache", () => {
7
16
  const a = PathUtils.norm("/a.ts");
8
17
  const b = PathUtils.norm("/b.ts");
9
18
  const c = PathUtils.norm("/c.ts");
10
19
  const html = PathUtils.norm("/comp.html");
11
- // const style = PathUtils.norm("/style.scss");
12
20
 
13
21
  let depCache: SdDepCache;
14
22
 
@@ -17,75 +25,55 @@ describe("SdDependencyCache", () => {
17
25
  });
18
26
 
19
27
  it("export * from 구문으로 재export된 심볼이 정확히 전파된다", () => {
20
- // a.ts → export const A
21
28
  depCache.addExport(a, "A");
22
-
23
- // b.ts → export * from './a.ts'
24
29
  depCache.addReexport(b, a, 0);
25
-
26
- // c.ts → import { A } from './b.ts'
27
30
  depCache.addImport(c, b, "A");
28
31
 
29
- const result = depCache.getAffectedFileSet(new Set([a]));
32
+ const result = getAffectedFileSet(depCache, new Set([a]));
30
33
  expect(result).toEqual(new Set([a, b, c]));
31
34
  });
32
35
 
33
36
  it("export { A as B } from 구문으로 이름이 바뀐 심볼도 추적된다", () => {
34
- // a.ts → export const A
35
37
  depCache.addExport(a, "A");
36
-
37
- // b.ts → export { A as B } from './a.ts'
38
38
  depCache.addReexport(b, a, {
39
39
  importSymbol: "A",
40
40
  exportSymbol: "B",
41
41
  });
42
-
43
- // c.ts → import { B } from './b.ts'
44
42
  depCache.addImport(c, b, "B");
45
43
 
46
- const result = depCache.getAffectedFileSet(new Set([a]));
44
+ const result = getAffectedFileSet(depCache, new Set([a]));
47
45
  expect(result).toEqual(new Set([a, b, c]));
48
46
  });
49
47
 
50
48
  it("import { X } 구문은 정확히 사용한 심볼만 추적한다", () => {
51
- // b.ts → export const Foo
52
49
  depCache.addExport(b, "Foo");
53
-
54
- // a.ts → import { Foo } from './b.ts'
55
50
  depCache.addImport(a, b, "Foo");
56
51
 
57
- const result = depCache.getAffectedFileSet(new Set([b]));
52
+ const result = getAffectedFileSet(depCache, new Set([b]));
58
53
  expect(result).toEqual(new Set([b, a]));
59
54
  });
60
55
 
61
56
  it("import * (namespace import)은 모든 export의 영향을 받는다", () => {
62
- // b.ts → export const Bar
63
57
  depCache.addExport(b, "Bar");
64
-
65
- // a.ts → import * as B from './b.ts'
66
58
  depCache.addImport(a, b, 0);
67
59
 
68
- const result = depCache.getAffectedFileSet(new Set([b]));
60
+ const result = getAffectedFileSet(depCache, new Set([b]));
69
61
  expect(result).toEqual(new Set([b, a]));
70
62
  });
71
63
 
72
64
  it("심볼이 일치하지 않으면 영향이 전파되지 않는다", () => {
73
- // b.ts → export const Foo
74
65
  depCache.addExport(b, "Foo");
75
-
76
- // a.ts → import { NotFoo } from './b.ts'
77
66
  depCache.addImport(a, b, "NotFoo");
78
67
 
79
- const result = depCache.getAffectedFileSet(new Set([b]));
80
- expect(result).toEqual(new Set([b])); // a.ts는 영향 없음
68
+ const result = getAffectedFileSet(depCache, new Set([b]));
69
+ expect(result).toEqual(new Set([b]));
81
70
  });
82
71
 
83
72
  it("리소스 의존은 역의존만 추적되고 심볼 전파는 없다", () => {
84
- // a.ts → templateUrl: "comp.html"
85
73
  depCache.addImport(a, html, 0);
86
74
 
87
- const result = depCache.getAffectedFileSet(new Set([html]));
88
- expect(result).toEqual(new Set([html, a])); // 단순 역참조만 전파
75
+ const result = getAffectedFileSet(depCache, new Set([html]));
76
+ expect(result).toEqual(new Set([html, a]));
89
77
  });
90
78
 
91
79
  it("reexport 후 재import된 경로의 역의존도 정확히 추적된다", () => {
@@ -93,50 +81,20 @@ describe("SdDependencyCache", () => {
93
81
  depCache.addReexport(b, a, { importSymbol: "X", exportSymbol: "Y" });
94
82
  depCache.addImport(c, b, "Y");
95
83
 
96
- const result = depCache.getAffectedFileSet(new Set([a]));
84
+ const result = getAffectedFileSet(depCache, new Set([a]));
97
85
  expect(result).toEqual(new Set([a, b, c]));
98
86
  });
99
87
 
100
- it("invalidates()는 캐시에서 모든 관련 정보를 제거한다", () => {
88
+ it("invalidates()는 해당 파일의 자체 분석 데이터를 제거한다", () => {
101
89
  depCache.addExport(a, "X");
102
90
  depCache.addImport(b, a, "X");
103
91
 
104
92
  depCache.invalidates(new Set([a]));
105
93
 
106
- // 내부 캐시 확인
94
+ // a의 자체 export/import/collected 캐시가 제거됨
107
95
  expect(depCache["_exportCache"].has(a)).toBe(false);
108
- expect(depCache["#revDepCache"].get(a)?.has(b)).toBeFalsy(); // unefined
109
- });
110
-
111
- it("getAffectedFileTree()는 영향도를 트리 형태로 표현한다", () => {
112
- // a.ts → export A
113
- depCache.addExport(a, "A");
114
-
115
- // b.ts → export { A as B } from './a.ts'
116
- depCache.addReexport(b, a, {
117
- importSymbol: "A",
118
- exportSymbol: "B",
119
- });
120
-
121
- // c.ts → import { B } from './b.ts'
122
- depCache.addImport(c, b, "B");
123
-
124
- const trees = depCache.getAffectedFileTree(new Set([a]));
125
-
126
- expect(trees.length).toBeGreaterThan(0);
127
- const aNode = trees.find((t) => t.fileNPath === a)!;
128
- expect(aNode.children.some((c1) => c1.fileNPath === b)).toBeTruthy();
129
- const bNode = aNode.children.find((c1) => c1.fileNPath === b)!;
130
- expect(bNode.children.some((c2) => c2.fileNPath === c)).toBeTruthy();
131
-
132
- const printTree = (node: ISdAffectedFileTreeNode, indent = "") => {
133
- console.log(indent + node.fileNPath);
134
- for (const child of node.children) {
135
- printTree(child, indent + " ");
136
- }
137
- };
138
-
139
- console.log(printTree(trees[0]));
96
+ expect(depCache["_importCache"].has(a)).toBe(false);
97
+ expect(depCache["_collectedCache"].has(a)).toBe(false);
140
98
  });
141
99
 
142
100
  it("d.ts를 입력하면 js도 함께 영향을 받는다", () => {
@@ -145,9 +103,9 @@ describe("SdDependencyCache", () => {
145
103
  const consumer = PathUtils.norm("/consumer.ts");
146
104
 
147
105
  depCache.addExport(dts, "Foo");
148
- depCache.addImport(consumer, js, "Foo"); // js 경로로 import
106
+ depCache.addImport(consumer, js, "Foo");
149
107
 
150
- const result = depCache.getAffectedFileSet(new Set([dts]));
108
+ const result = getAffectedFileSet(depCache, new Set([dts]));
151
109
  expect(result).toEqual(new Set([dts, js, consumer]));
152
110
  });
153
111
  });
package/vitest.config.js CHANGED
@@ -1,12 +1,9 @@
1
1
  import { defineConfig } from "vitest/config";
2
- import tsconfigPaths from "vite-tsconfig-paths";
3
2
 
4
3
  export default defineConfig({
5
- plugins: [
6
- tsconfigPaths({
7
- projects: ["../../tsconfig.base.json"],
8
- }),
9
- ],
4
+ resolve: {
5
+ tsconfigPaths: true,
6
+ },
10
7
  test: {
11
8
  globals: true,
12
9
  environment: "node",