@simplysm/sd-cli 14.0.15 → 14.0.17

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 (216) hide show
  1. package/README.md +4 -3
  2. package/dist/angular/client-transform-stylesheet.d.ts +2 -0
  3. package/dist/angular/client-transform-stylesheet.d.ts.map +1 -1
  4. package/dist/angular/client-transform-stylesheet.js +88 -2
  5. package/dist/angular/client-transform-stylesheet.js.map +1 -1
  6. package/dist/angular/vite-angular-plugin.d.ts +7 -0
  7. package/dist/angular/vite-angular-plugin.d.ts.map +1 -1
  8. package/dist/angular/vite-angular-plugin.js +78 -16
  9. package/dist/angular/vite-angular-plugin.js.map +1 -1
  10. package/dist/capacitor/capacitor.d.ts.map +1 -1
  11. package/dist/capacitor/capacitor.js +24 -13
  12. package/dist/capacitor/capacitor.js.map +1 -1
  13. package/dist/commands/check.d.ts.map +1 -1
  14. package/dist/commands/check.js +8 -9
  15. package/dist/commands/check.js.map +1 -1
  16. package/dist/commands/device.d.ts +1 -1
  17. package/dist/commands/device.d.ts.map +1 -1
  18. package/dist/commands/device.js +61 -12
  19. package/dist/commands/device.js.map +1 -1
  20. package/dist/commands/lint.d.ts +0 -1
  21. package/dist/commands/lint.d.ts.map +1 -1
  22. package/dist/commands/lint.js +2 -3
  23. package/dist/commands/lint.js.map +1 -1
  24. package/dist/commands/publish.js +3 -3
  25. package/dist/commands/publish.js.map +1 -1
  26. package/dist/commands/replace-deps.js +1 -1
  27. package/dist/commands/replace-deps.js.map +1 -1
  28. package/dist/commands/typecheck.d.ts.map +1 -1
  29. package/dist/commands/typecheck.js +1 -2
  30. package/dist/commands/typecheck.js.map +1 -1
  31. package/dist/electron/electron.d.ts +3 -2
  32. package/dist/electron/electron.d.ts.map +1 -1
  33. package/dist/electron/electron.js +54 -31
  34. package/dist/electron/electron.js.map +1 -1
  35. package/dist/engines/BaseEngine.js +1 -1
  36. package/dist/engines/BaseEngine.js.map +1 -1
  37. package/dist/engines/NgtscEngine.d.ts.map +1 -1
  38. package/dist/engines/NgtscEngine.js +0 -1
  39. package/dist/engines/NgtscEngine.js.map +1 -1
  40. package/dist/engines/ServerEsbuildEngine.d.ts.map +1 -1
  41. package/dist/engines/ServerEsbuildEngine.js +0 -1
  42. package/dist/engines/ServerEsbuildEngine.js.map +1 -1
  43. package/dist/engines/TscEngine.d.ts.map +1 -1
  44. package/dist/engines/TscEngine.js +0 -1
  45. package/dist/engines/TscEngine.js.map +1 -1
  46. package/dist/engines/ViteEngine.d.ts.map +1 -1
  47. package/dist/engines/ViteEngine.js +10 -1
  48. package/dist/engines/ViteEngine.js.map +1 -1
  49. package/dist/engines/index.d.ts +0 -10
  50. package/dist/engines/index.d.ts.map +1 -1
  51. package/dist/engines/index.js +0 -5
  52. package/dist/engines/index.js.map +1 -1
  53. package/dist/engines/types.d.ts +0 -1
  54. package/dist/engines/types.d.ts.map +1 -1
  55. package/dist/infra/SignalHandler.d.ts +1 -6
  56. package/dist/infra/SignalHandler.d.ts.map +1 -1
  57. package/dist/infra/SignalHandler.js +4 -13
  58. package/dist/infra/SignalHandler.js.map +1 -1
  59. package/dist/orchestrators/BuildOrchestrator.d.ts.map +1 -1
  60. package/dist/orchestrators/BuildOrchestrator.js +7 -12
  61. package/dist/orchestrators/BuildOrchestrator.js.map +1 -1
  62. package/dist/orchestrators/DevWatchOrchestrator.d.ts.map +1 -1
  63. package/dist/orchestrators/DevWatchOrchestrator.js +18 -11
  64. package/dist/orchestrators/DevWatchOrchestrator.js.map +1 -1
  65. package/dist/sd-cli-entry.d.ts +0 -1
  66. package/dist/sd-cli-entry.d.ts.map +1 -1
  67. package/dist/sd-cli-entry.js +13 -16
  68. package/dist/sd-cli-entry.js.map +1 -1
  69. package/dist/sd-cli.js +1 -1
  70. package/dist/sd-cli.js.map +1 -1
  71. package/dist/sd-config.types.d.ts +12 -2
  72. package/dist/sd-config.types.d.ts.map +1 -1
  73. package/dist/utils/angular-compiler.d.ts.map +1 -1
  74. package/dist/utils/angular-compiler.js +20 -13
  75. package/dist/utils/angular-compiler.js.map +1 -1
  76. package/dist/utils/esbuild-config.d.ts +1 -1
  77. package/dist/utils/esbuild-config.d.ts.map +1 -1
  78. package/dist/utils/esbuild-config.js +1 -4
  79. package/dist/utils/esbuild-config.js.map +1 -1
  80. package/dist/utils/ngtsc-build-core.d.ts.map +1 -1
  81. package/dist/utils/ngtsc-build-core.js +3 -0
  82. package/dist/utils/ngtsc-build-core.js.map +1 -1
  83. package/dist/utils/orchestrator-utils.js +1 -1
  84. package/dist/utils/orchestrator-utils.js.map +1 -1
  85. package/dist/utils/tsc-build.d.ts +5 -0
  86. package/dist/utils/tsc-build.d.ts.map +1 -1
  87. package/dist/utils/tsc-build.js +2 -1
  88. package/dist/utils/tsc-build.js.map +1 -1
  89. package/dist/utils/vite-config.d.ts +1 -1
  90. package/dist/utils/vite-config.d.ts.map +1 -1
  91. package/dist/utils/vite-config.js +22 -53
  92. package/dist/utils/vite-config.js.map +1 -1
  93. package/dist/utils/vite-pwa-plugin.d.ts +9 -0
  94. package/dist/utils/vite-pwa-plugin.d.ts.map +1 -0
  95. package/dist/utils/vite-pwa-plugin.js +139 -0
  96. package/dist/utils/vite-pwa-plugin.js.map +1 -0
  97. package/dist/utils/worker-utils.d.ts +2 -5
  98. package/dist/utils/worker-utils.d.ts.map +1 -1
  99. package/dist/utils/worker-utils.js +5 -11
  100. package/dist/utils/worker-utils.js.map +1 -1
  101. package/dist/workers/client.worker.d.ts.map +1 -1
  102. package/dist/workers/client.worker.js +9 -3
  103. package/dist/workers/client.worker.js.map +1 -1
  104. package/dist/workers/library-build.worker.d.ts.map +1 -1
  105. package/dist/workers/library-build.worker.js +6 -2
  106. package/dist/workers/library-build.worker.js.map +1 -1
  107. package/dist/workers/ngtsc-build.worker.js +2 -2
  108. package/dist/workers/ngtsc-build.worker.js.map +1 -1
  109. package/dist/workers/server-build.worker.d.ts.map +1 -1
  110. package/dist/workers/server-build.worker.js +6 -2
  111. package/dist/workers/server-build.worker.js.map +1 -1
  112. package/dist/workers/server-runtime.worker.js +4 -4
  113. package/dist/workers/server-runtime.worker.js.map +1 -1
  114. package/docs/config.md +30 -2
  115. package/docs/pwa-configuration-types.md +1 -1
  116. package/package.json +8 -10
  117. package/src/angular/client-transform-stylesheet.ts +104 -2
  118. package/src/angular/vite-angular-plugin.ts +92 -31
  119. package/src/capacitor/capacitor.ts +25 -26
  120. package/src/commands/check.ts +8 -11
  121. package/src/commands/device.ts +71 -17
  122. package/src/commands/lint.ts +2 -3
  123. package/src/commands/publish.ts +3 -3
  124. package/src/commands/replace-deps.ts +1 -1
  125. package/src/commands/typecheck.ts +1 -2
  126. package/src/electron/electron.ts +62 -43
  127. package/src/engines/BaseEngine.ts +1 -1
  128. package/src/engines/NgtscEngine.ts +0 -1
  129. package/src/engines/ServerEsbuildEngine.ts +0 -1
  130. package/src/engines/TscEngine.ts +0 -1
  131. package/src/engines/ViteEngine.ts +9 -1
  132. package/src/engines/index.ts +0 -10
  133. package/src/engines/types.ts +0 -1
  134. package/src/infra/SignalHandler.ts +4 -14
  135. package/src/orchestrators/BuildOrchestrator.ts +7 -9
  136. package/src/orchestrators/DevWatchOrchestrator.ts +22 -10
  137. package/src/sd-cli-entry.ts +17 -24
  138. package/src/sd-cli.ts +1 -1
  139. package/src/sd-config.types.ts +13 -2
  140. package/src/utils/angular-compiler.ts +21 -21
  141. package/src/utils/esbuild-config.ts +2 -5
  142. package/src/utils/ngtsc-build-core.ts +7 -0
  143. package/src/utils/orchestrator-utils.ts +1 -1
  144. package/src/utils/tsc-build.ts +7 -0
  145. package/src/utils/vite-config.ts +23 -55
  146. package/src/utils/vite-pwa-plugin.ts +168 -0
  147. package/src/utils/worker-utils.ts +5 -11
  148. package/src/workers/client.worker.ts +11 -3
  149. package/src/workers/library-build.worker.ts +6 -2
  150. package/src/workers/ngtsc-build.worker.ts +2 -2
  151. package/src/workers/server-build.worker.ts +7 -2
  152. package/src/workers/server-runtime.worker.ts +4 -4
  153. package/tests/angular/client-transform-stylesheet.spec.ts +43 -0
  154. package/tests/angular/find-affected-by-scss.spec.ts +37 -0
  155. package/tests/angular/fixtures/basic-app/scss/_colors.scss +1 -0
  156. package/tests/angular/fixtures/basic-app/scss/_variables.scss +3 -0
  157. package/tests/angular/fixtures/basic-app/src/styled.component.ts +14 -0
  158. package/tests/angular/linker-disk-cache.spec.ts +158 -0
  159. package/tests/angular/scss-disk-cache.spec.ts +162 -0
  160. package/tests/angular/vite-angular-plugin-hmr-fallback.spec.ts +15 -15
  161. package/tests/angular/vite-angular-plugin-hmr.spec.ts +9 -9
  162. package/tests/angular/vite-angular-plugin-lint.spec.ts +4 -4
  163. package/tests/angular/vite-angular-plugin-scss-hmr.spec.ts +87 -0
  164. package/tests/angular/vite-angular-plugin.spec.ts +15 -15
  165. package/tests/capacitor/capacitor-icon.spec.ts +2 -4
  166. package/tests/capacitor/capacitor-init.spec.ts +2 -4
  167. package/tests/capacitor/capacitor-workspace.spec.ts +2 -4
  168. package/tests/commands/device.spec.ts +108 -8
  169. package/tests/commands/publish.spec.ts +2 -2
  170. package/tests/commands/typecheck.spec.ts +1 -1
  171. package/tests/electron/electron.spec.ts +24 -17
  172. package/tests/engines/ngtsc-engine.spec.ts +0 -3
  173. package/tests/engines/server-esbuild-engine.spec.ts +0 -3
  174. package/tests/engines/tsc-engine.spec.ts +1 -2
  175. package/tests/engines/vite-engine.spec.ts +0 -2
  176. package/tests/infra/signal-handler.spec.ts +1 -12
  177. package/tests/orchestrators/build-orchestrator.spec.ts +1 -7
  178. package/tests/orchestrators/dev-watch-orchestrator.spec.ts +24 -66
  179. package/tests/utils/angular-compiler.spec.ts +1396 -32
  180. package/tests/utils/esbuild-config.spec.ts +4 -7
  181. package/tests/utils/{ngtsc-build-core-angular-compiler.spec.ts → ngtsc-build-core.spec.ts} +142 -11
  182. package/tests/utils/orchestrator-utils.spec.ts +2 -2
  183. package/tests/utils/sd-config.spec.ts +2 -2
  184. package/tests/utils/tsc-build.spec.ts +4 -1
  185. package/tests/utils/vite-config.spec.ts +130 -261
  186. package/tests/utils/vite-pwa-plugin.acc.spec.ts +143 -0
  187. package/tests/utils/vite-pwa-plugin.spec.ts +350 -0
  188. package/tests/utils/worker-utils.spec.ts +8 -7
  189. package/tests/workers/client-worker.spec.ts +50 -1
  190. package/tests/workers/dev-port-file.verify.md +6 -0
  191. package/tests/workers/library-build-lint.spec.ts +1 -1
  192. package/tests/workers/library-build-worker.spec.ts +1 -1
  193. package/tests/workers/ngtsc-build-lint.spec.ts +1 -1
  194. package/tests/workers/server-build-lint.spec.ts +1 -1
  195. package/tests/workers/server-build-worker.spec.ts +1 -1
  196. package/tests/workers/server-runtime-worker.spec.ts +8 -1
  197. package/dist/infra/WorkerManager.d.ts +0 -40
  198. package/dist/infra/WorkerManager.d.ts.map +0 -1
  199. package/dist/infra/WorkerManager.js +0 -59
  200. package/dist/infra/WorkerManager.js.map +0 -1
  201. package/dist/utils/SdCliReporter.d.ts +0 -18
  202. package/dist/utils/SdCliReporter.d.ts.map +0 -1
  203. package/dist/utils/SdCliReporter.js +0 -144
  204. package/dist/utils/SdCliReporter.js.map +0 -1
  205. package/src/infra/WorkerManager.ts +0 -65
  206. package/src/utils/SdCliReporter.ts +0 -177
  207. package/tests/angular/scss-compiler-async.spec.ts +0 -54
  208. package/tests/commands/dev.spec.ts +0 -53
  209. package/tests/commands/watch.spec.ts +0 -53
  210. package/tests/infra/worker-manager.spec.ts +0 -63
  211. package/tests/utils/angular-compiler-emit.spec.ts +0 -570
  212. package/tests/utils/angular-compiler-init.spec.ts +0 -705
  213. package/tests/utils/angular-compiler-update.spec.ts +0 -293
  214. package/tests/utils/build-env.spec.ts +0 -33
  215. package/tests/utils/ngtsc-build-core-transform-stylesheet.spec.ts +0 -124
  216. package/tests/utils/ngtsc-scss-refactor.spec.ts +0 -47
@@ -0,0 +1,168 @@
1
+ import type { PluginOption, ResolvedConfig } from "vite";
2
+ import type { SdPwaConfig } from "../sd-config.types.js";
3
+ import { generatePwaIcons } from "./generate-pwa-icons.js";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import { glob } from "glob";
7
+
8
+ export interface SdPwaPluginOptions {
9
+ pkgDir: string;
10
+ pkgName: string;
11
+ pwa?: SdPwaConfig;
12
+ }
13
+
14
+ export function sdPwaPlugin(options: SdPwaPluginOptions): PluginOption {
15
+ const pwaConfig = options.pwa ?? {};
16
+ let resolvedBase: string;
17
+ let resolvedOutDir: string;
18
+
19
+ return {
20
+ name: "sd-pwa",
21
+
22
+ configResolved(config: ResolvedConfig) {
23
+ resolvedBase = config.base;
24
+ resolvedOutDir = config.build.outDir;
25
+ },
26
+
27
+ transformIndexHtml() {
28
+ return [
29
+ {
30
+ tag: "link",
31
+ attrs: { rel: "manifest", href: "manifest.webmanifest" },
32
+ injectTo: "head" as const,
33
+ },
34
+ {
35
+ tag: "script",
36
+ children: generateRegistrationScript(),
37
+ injectTo: "body" as const,
38
+ },
39
+ ];
40
+ },
41
+
42
+ async closeBundle() {
43
+ // Read version from package.json
44
+ const pkgJsonPath = path.join(options.pkgDir, "package.json");
45
+ const pkgJson = JSON.parse(
46
+ fs.readFileSync(pkgJsonPath, "utf-8"),
47
+ ) as Record<string, unknown>;
48
+ const version = pkgJson["version"] as string;
49
+
50
+ // Icons
51
+ let iconsField: Record<string, unknown> = {};
52
+ if (pwaConfig.manifest?.icons != null) {
53
+ iconsField = { icons: pwaConfig.manifest.icons };
54
+ } else {
55
+ const generated = await generatePwaIcons(options.pkgDir);
56
+ if (generated.length > 0) {
57
+ iconsField = { icons: generated };
58
+ }
59
+ }
60
+
61
+ // Manifest
62
+ const manifest = {
63
+ name: pwaConfig.manifest?.name ?? options.pkgName,
64
+ short_name: pwaConfig.manifest?.short_name ?? options.pkgName,
65
+ display: pwaConfig.manifest?.display ?? "standalone",
66
+ theme_color: pwaConfig.manifest?.theme_color ?? "#ffffff",
67
+ background_color: pwaConfig.manifest?.background_color ?? "#ffffff",
68
+ start_url: ".",
69
+ scope: ".",
70
+ ...iconsField,
71
+ };
72
+
73
+ fs.writeFileSync(
74
+ path.join(resolvedOutDir, "manifest.webmanifest"),
75
+ JSON.stringify(manifest, null, 2),
76
+ );
77
+
78
+ // Precache file list
79
+ const globPatterns = pwaConfig.workbox?.globPatterns ?? [
80
+ "**/*.{js,css,html,ico,png,svg,woff2}",
81
+ ];
82
+ const globResults = await Promise.all(
83
+ globPatterns.map((pattern) => glob(pattern, { cwd: resolvedOutDir })),
84
+ );
85
+ const precacheUrls = [...new Set(globResults.flat())]
86
+ .filter((f) => f !== "sw.js" && f !== "manifest.webmanifest")
87
+ .map((f) => f.replace(/\\/g, "/"));
88
+
89
+ // Service Worker
90
+ const swContent = generateSwContent(version, resolvedBase, precacheUrls);
91
+ fs.writeFileSync(path.join(resolvedOutDir, "sw.js"), swContent);
92
+ },
93
+ };
94
+ }
95
+
96
+ function generateRegistrationScript(): string {
97
+ return `(function(){
98
+ if(!("serviceWorker" in navigator))return;
99
+ navigator.serviceWorker.register("sw.js").then(function(reg){
100
+ if(reg.waiting){d(reg.waiting);return}
101
+ reg.addEventListener("updatefound",function(){
102
+ var w=reg.installing;
103
+ if(!w)return;
104
+ w.addEventListener("statechange",function(){
105
+ if(w.state==="installed"&&navigator.serviceWorker.controller)d(w);
106
+ });
107
+ });
108
+ });
109
+ var r=false;
110
+ navigator.serviceWorker.addEventListener("controllerchange",function(){
111
+ if(!r){r=true;window.location.reload()}
112
+ });
113
+ function d(w){
114
+ window.dispatchEvent(new CustomEvent("sd-pwa-update-ready",{
115
+ detail:{update:function(){w.postMessage({type:"SKIP_WAITING"})}}
116
+ }));
117
+ }
118
+ })();`;
119
+ }
120
+
121
+ function generateSwContent(
122
+ version: string,
123
+ base: string,
124
+ precacheUrls: string[],
125
+ ): string {
126
+ const urlsArray = JSON.stringify(precacheUrls, null, 2);
127
+ return `const APP_VERSION = ${JSON.stringify(version)};
128
+ const CACHE_NAME = "precache-" + APP_VERSION;
129
+ const BASE_URL = ${JSON.stringify(base)};
130
+ const PRECACHE_URLS = ${urlsArray};
131
+
132
+ self.addEventListener("install", (event) => {
133
+ event.waitUntil(
134
+ caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE_URLS))
135
+ );
136
+ });
137
+
138
+ self.addEventListener("activate", (event) => {
139
+ event.waitUntil(
140
+ caches.keys().then((names) =>
141
+ Promise.all(
142
+ names
143
+ .filter((name) => name.startsWith("precache-") && name !== CACHE_NAME)
144
+ .map((name) => caches.delete(name))
145
+ )
146
+ ).then(() => self.clients.claim())
147
+ );
148
+ });
149
+
150
+ self.addEventListener("fetch", (event) => {
151
+ event.respondWith(
152
+ caches.match(event.request).then((cached) => {
153
+ if (cached) return cached;
154
+ if (event.request.mode === "navigate") {
155
+ return caches.match(BASE_URL + "index.html").then((resp) => resp || fetch(event.request));
156
+ }
157
+ return fetch(event.request);
158
+ })
159
+ );
160
+ });
161
+
162
+ self.addEventListener("message", (event) => {
163
+ if (event.data && event.data.type === "SKIP_WAITING") {
164
+ self.skipWaiting();
165
+ }
166
+ });
167
+ `;
168
+ }
@@ -1,18 +1,12 @@
1
- import consola, { type ConsolaInstance, LogLevels } from "consola";
2
- import { SdCliReporter } from "./SdCliReporter";
1
+ import { type ConsolaInstance } from "consola";
2
+ import { setupConsola } from "@simplysm/core-node";
3
3
 
4
4
  /**
5
- * 워커 스레드에서 sd-cli 리포터와 디버그 로그 레벨을 적용한다.
6
- *
7
- * SD_DEBUG 환경변수(메인 프로세스의 --debug 플래그로 설정)를 확인하고
8
- * 현재 워커 스레드의 consola에 디버그 로그 레벨을 적용한다.
5
+ * 워커 스레드에서 consola를 설정한다.
9
6
  * 워커 모듈 최상위에서 호출해야 한다.
10
7
  */
11
- export function applyDebugLevel(): void {
12
- consola.options.reporters = [new SdCliReporter()];
13
- if (process.env["SD_DEBUG"] === "true") {
14
- consola.level = LogLevels.debug;
15
- }
8
+ export function setupWorkerConsola(): void {
9
+ setupConsola({ cli: true });
16
10
  }
17
11
 
18
12
  /**
@@ -6,13 +6,13 @@ import mime from "mime";
6
6
  import { createWorker } from "@simplysm/core-node";
7
7
  import { err as errNs } from "@simplysm/core-common";
8
8
  import { consola } from "consola";
9
- import { registerCleanupHandlers, applyDebugLevel } from "../utils/worker-utils.js";
9
+ import { registerCleanupHandlers, setupWorkerConsola } from "../utils/worker-utils.js";
10
10
  import { createClientViteConfig } from "../utils/vite-config.js";
11
11
  import type { ScopeWatchReplaceDep } from "../utils/vite-scope-watch-plugin.js";
12
12
  import type { SdBrowserSupportConfig, SdPwaConfig } from "../sd-config.types.js";
13
13
  import type { LintWithProgramResult } from "../utils/lint-with-program.js";
14
14
 
15
- applyDebugLevel();
15
+ setupWorkerConsola();
16
16
 
17
17
  //#region Types
18
18
 
@@ -247,8 +247,13 @@ async function startWatch(info: ClientBuildInfo): Promise<ClientBuildResult> {
247
247
 
248
248
  sender.send("serverReady", { port: actualPort });
249
249
 
250
+ // .dev-port 기록 (device 명령어에서 자동 탐지용)
251
+ const distDir = path.join(info.pkgDir, "dist");
252
+ fs.mkdirSync(distDir, { recursive: true });
253
+ fs.writeFileSync(path.join(distDir, ".dev-port"), String(actualPort));
254
+
250
255
  // .config.json 생성
251
- writeConfigJson(path.join(info.pkgDir, "dist"), info.configs);
256
+ writeConfigJson(distDir, info.configs);
252
257
 
253
258
  return { success: true };
254
259
  } catch (err) {
@@ -323,6 +328,9 @@ async function startLegacyWatch(info: ClientBuildInfo): Promise<ClientBuildResul
323
328
 
324
329
  sender.send("serverReady", { port: serverPort });
325
330
 
331
+ // .dev-port 기록 (device 명령어에서 자동 탐지용)
332
+ fs.writeFileSync(path.join(info.pkgDir, "dist", ".dev-port"), String(serverPort));
333
+
326
334
  // 첫 빌드 완료 대기
327
335
  return await new Promise<ClientBuildResult>((resolve) => {
328
336
  let firstBuildResolved = false;
@@ -9,9 +9,9 @@ import type { LintWithProgramResult } from "../utils/lint-with-program";
9
9
  import { runTscPackageBuild } from "../utils/tsc-build";
10
10
  import { LintWithProgramRunner } from "../utils/lint-with-program";
11
11
  import { collectDeps } from "../utils/package-utils";
12
- import { registerCleanupHandlers, createOnceGuard, applyDebugLevel } from "../utils/worker-utils";
12
+ import { registerCleanupHandlers, createOnceGuard, setupWorkerConsola } from "../utils/worker-utils";
13
13
 
14
- applyDebugLevel();
14
+ setupWorkerConsola();
15
15
 
16
16
  //#region Types
17
17
 
@@ -53,6 +53,7 @@ async function cleanup(): Promise<void> {
53
53
  const watcherToClose = fsWatcher;
54
54
  fsWatcher = undefined;
55
55
  lastSourceFilePaths = undefined;
56
+ lastBuilderProgram = undefined;
56
57
  await watcherToClose?.close();
57
58
  }
58
59
 
@@ -106,6 +107,7 @@ const guardStartWatch = createOnceGuard("startWatch");
106
107
  let watchInfo: LibraryBuildInfo | undefined;
107
108
  let watchLintRunner: LintWithProgramRunner | undefined;
108
109
  let lastSourceFilePaths: Set<string> | undefined;
110
+ let lastBuilderProgram: ts.EmitAndSemanticDiagnosticsBuilderProgram | undefined;
109
111
 
110
112
  function extractSourceFilePaths(program: ts.Program | undefined): Set<string> | undefined {
111
113
  if (program == null) return undefined;
@@ -126,7 +128,9 @@ async function rebuildAll(): Promise<CombinedBuildEvent> {
126
128
  output: info.output,
127
129
  env: info.output.env,
128
130
  includeTests: info.output.includeTests,
131
+ oldBuilderProgram: lastBuilderProgram,
129
132
  });
133
+ lastBuilderProgram = tscResult.builderProgram ?? lastBuilderProgram;
130
134
 
131
135
  // 의존성 필터링을 위한 소스 파일 경로 업데이트
132
136
  lastSourceFilePaths = extractSourceFilePaths(tscResult.program) ?? lastSourceFilePaths;
@@ -3,7 +3,7 @@ import ts from "typescript";
3
3
  import { createWorker, FsWatcher, pathx } from "@simplysm/core-node";
4
4
  import { err as errNs } from "@simplysm/core-common";
5
5
  import { consola } from "consola";
6
- import { registerCleanupHandlers, createOnceGuard, applyDebugLevel } from "../utils/worker-utils";
6
+ import { registerCleanupHandlers, createOnceGuard, setupWorkerConsola } from "../utils/worker-utils";
7
7
  import {
8
8
  runNgtscBuild,
9
9
  buildCompilerOptions,
@@ -28,7 +28,7 @@ import {
28
28
  import { AngularCompiler, AngularSourceFileCache } from "../utils/angular-compiler";
29
29
  import { collectDeps } from "../utils/package-utils";
30
30
 
31
- applyDebugLevel();
31
+ setupWorkerConsola();
32
32
 
33
33
  //#region 타입 (워커 인터페이스용 re-export)
34
34
 
@@ -1,3 +1,4 @@
1
+ import type ts from "typescript";
1
2
  import path from "path";
2
3
  import fs from "fs";
3
4
  import esbuild from "esbuild";
@@ -18,7 +19,7 @@ import {
18
19
  } from "../utils/esbuild-config";
19
20
  import { runTscPackageBuild } from "../utils/tsc-build";
20
21
  import { LintWithProgramRunner } from "../utils/lint-with-program";
21
- import { registerCleanupHandlers, createOnceGuard, applyDebugLevel } from "../utils/worker-utils";
22
+ import { registerCleanupHandlers, createOnceGuard, setupWorkerConsola } from "../utils/worker-utils";
22
23
  import { collectDeps } from "../utils/package-utils";
23
24
  import { copyPublicFiles, watchPublicFiles } from "../utils/copy-public";
24
25
 
@@ -96,7 +97,7 @@ export interface ServerBuildWorkerEvents extends Record<string, unknown> {
96
97
 
97
98
  //#region Resource Management
98
99
 
99
- applyDebugLevel();
100
+ setupWorkerConsola();
100
101
 
101
102
  const logger = consola.withTag("sd:cli:server-build:worker");
102
103
 
@@ -116,6 +117,7 @@ async function cleanup(): Promise<void> {
116
117
  const contextToDispose = esbuildContext;
117
118
  esbuildContext = undefined;
118
119
  lastMetafile = undefined;
120
+ lastBuilderProgram = undefined;
119
121
 
120
122
  const watcherToClose = publicWatcher;
121
123
  publicWatcher = undefined;
@@ -427,6 +429,7 @@ const guardStartWatch = createOnceGuard("startWatch");
427
429
  // watch 모드용 가변 상태
428
430
  let watchInfo: ServerWatchInfo | undefined;
429
431
  let watchLintRunner: LintWithProgramRunner | undefined;
432
+ let lastBuilderProgram: ts.EmitAndSemanticDiagnosticsBuilderProgram | undefined;
430
433
 
431
434
  /**
432
435
  * esbuild + tsc 병렬 리빌드 (watch 모드)
@@ -467,7 +470,9 @@ async function rebuildAll(): Promise<ServerCombinedBuildEvent> {
467
470
  parsedConfig,
468
471
  env: info.output.env,
469
472
  includeTests: info.output.includeTests,
473
+ oldBuilderProgram: lastBuilderProgram,
470
474
  });
475
+ lastBuilderProgram = tscResult.builderProgram ?? lastBuilderProgram;
471
476
 
472
477
  // lint 실행 (활성화 + program 사용 가능 시)
473
478
  let lint: LintWithProgramResult | undefined;
@@ -1,10 +1,10 @@
1
1
  import { createWorker } from "@simplysm/core-node";
2
- import { err as errNs } from "@simplysm/core-common";
2
+ import { env, err as errNs } from "@simplysm/core-common";
3
3
  import { consola } from "consola";
4
4
  import proxy from "@fastify/http-proxy";
5
5
  import net from "net";
6
6
  import { pathToFileURL } from "url";
7
- import { registerCleanupHandlers, applyDebugLevel } from "../utils/worker-utils";
7
+ import { registerCleanupHandlers, setupWorkerConsola } from "../utils/worker-utils";
8
8
 
9
9
  //#region Types
10
10
 
@@ -42,7 +42,7 @@ export interface ServerRuntimeWorkerEvents extends Record<string, unknown> {
42
42
 
43
43
  //#endregion
44
44
 
45
- applyDebugLevel();
45
+ setupWorkerConsola();
46
46
 
47
47
  const logger = consola.withTag("sd:cli:server-runtime:worker");
48
48
 
@@ -122,7 +122,7 @@ async function start(info: ServerRuntimeStartInfo): Promise<void> {
122
122
  // main.js import 전에 환경변수를 process.env에 주입
123
123
  if (info.env != null) {
124
124
  for (const [key, value] of Object.entries(info.env)) {
125
- process.env[key] = value;
125
+ env(key, value);
126
126
  }
127
127
  }
128
128
 
@@ -3,6 +3,7 @@ import path from "path";
3
3
  import fs from "fs";
4
4
  import os from "os";
5
5
  import { createClientTransformStylesheet } from "../../src/angular/client-transform-stylesheet.js";
6
+ import { compileScssFileAsync, compileScssStringAsync } from "../../src/utils/scss-compiler.js";
6
7
 
7
8
  const TMP_DIR = path.join(os.tmpdir(), "sd-cli-stylesheet-test");
8
9
 
@@ -151,3 +152,45 @@ describe("createClientTransformStylesheet", () => {
151
152
  expect(deps.size).toBeGreaterThan(0);
152
153
  });
153
154
  });
155
+
156
+ // ─── scss-compiler async (low-level) ───
157
+
158
+ describe("scss-compiler async", () => {
159
+ // Scenario: 외부 .scss 파일 변환
160
+ it("compiles external .scss file asynchronously", async () => {
161
+ ensureTmpDir();
162
+ const scssPath = path.join(TMP_DIR, "test.scss");
163
+ fs.writeFileSync(scssPath, "$color: red;\n.host { color: $color; }");
164
+
165
+ const result = await compileScssFileAsync(scssPath, []);
166
+
167
+ expect(result.css).toContain("color: red");
168
+ expect(result.css).not.toContain("$color");
169
+ expect(result.dependencies).toBeInstanceOf(Array);
170
+ });
171
+
172
+ // Scenario: 인라인 SCSS 문자열 변환
173
+ it("compiles inline SCSS string asynchronously", async () => {
174
+ ensureTmpDir();
175
+ const containingFile = path.join(TMP_DIR, "component.ts");
176
+
177
+ const result = await compileScssStringAsync(
178
+ "$size: 16px;\n.text { font-size: $size; }",
179
+ containingFile,
180
+ [],
181
+ );
182
+
183
+ expect(result.css).toContain("font-size: 16px");
184
+ expect(result.css).not.toContain("$size");
185
+ expect(result.dependencies).toBeInstanceOf(Array);
186
+ });
187
+
188
+ // Scenario: SCSS 컴파일 에러 시 에러
189
+ it("throws on invalid SCSS syntax", async () => {
190
+ ensureTmpDir();
191
+ const scssPath = path.join(TMP_DIR, "invalid.scss");
192
+ fs.writeFileSync(scssPath, ".host { @include nonexistent-mixin(); }");
193
+
194
+ await expect(compileScssFileAsync(scssPath, [])).rejects.toThrow();
195
+ });
196
+ });
@@ -0,0 +1,37 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { findAffectedByScss } from "../../src/angular/vite-angular-plugin.js";
3
+
4
+ describe("findAffectedByScss", () => {
5
+ it("returns owner file when SCSS is in its dependency set", () => {
6
+ const deps = new Map<string, Set<string>>();
7
+ deps.set("/app/src/comp.ts", new Set(["/app/scss/_vars.scss"]));
8
+
9
+ const result = findAffectedByScss("/app/scss/_vars.scss", deps);
10
+ expect(result).toEqual(["/app/src/comp.ts"]);
11
+ });
12
+
13
+ it("returns multiple owners when multiple components depend on the same SCSS", () => {
14
+ const deps = new Map<string, Set<string>>();
15
+ deps.set("/app/src/comp-a.ts", new Set(["/app/scss/_vars.scss"]));
16
+ deps.set("/app/src/comp-b.ts", new Set(["/app/scss/_vars.scss", "/app/scss/_extra.scss"]));
17
+ deps.set("/app/src/comp-c.ts", new Set(["/app/scss/_other.scss"]));
18
+
19
+ const result = findAffectedByScss("/app/scss/_vars.scss", deps);
20
+ expect(result).toEqual(expect.arrayContaining(["/app/src/comp-a.ts", "/app/src/comp-b.ts"]));
21
+ expect(result).not.toContain("/app/src/comp-c.ts");
22
+ });
23
+
24
+ it("returns empty array when no component depends on the SCSS", () => {
25
+ const deps = new Map<string, Set<string>>();
26
+ deps.set("/app/src/comp.ts", new Set(["/app/scss/_vars.scss"]));
27
+
28
+ const result = findAffectedByScss("/app/scss/_unknown.scss", deps);
29
+ expect(result).toEqual([]);
30
+ });
31
+
32
+ it("returns empty array when scssDependencies is empty", () => {
33
+ const deps = new Map<string, Set<string>>();
34
+ const result = findAffectedByScss("/app/scss/_vars.scss", deps);
35
+ expect(result).toEqual([]);
36
+ });
37
+ });
@@ -0,0 +1 @@
1
+ $base: blue;
@@ -0,0 +1,3 @@
1
+ @use 'colors' as c;
2
+
3
+ $primary: c.$base;
@@ -0,0 +1,14 @@
1
+ import { Component } from "@angular/core";
2
+
3
+ @Component({
4
+ selector: "app-styled",
5
+ standalone: true,
6
+ template: `<p>styled</p>`,
7
+ styles: [`
8
+ @use 'variables' as vars;
9
+ :host {
10
+ color: vars.$primary;
11
+ }
12
+ `],
13
+ })
14
+ export class StyledComponent {}
@@ -0,0 +1,158 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import os from "os";
5
+
6
+ const mockTransformFile = vi.fn(() => Promise.resolve("transformed-code"));
7
+
8
+ vi.mock("@angular/build/private", () => ({
9
+ JavaScriptTransformer: class MockJSTransformer {
10
+ transformFile = mockTransformFile;
11
+ transformData = vi.fn(() => Promise.resolve(new TextEncoder().encode("transformed")));
12
+ close = vi.fn(() => Promise.resolve());
13
+ },
14
+ }));
15
+
16
+ const { sdAngularPlugin } = await import("../../src/angular/vite-angular-plugin.js");
17
+
18
+ const TMP_DIR = path.join(os.tmpdir(), "sd-cli-linker-cache-test");
19
+ const CACHE_DIR = path.join(TMP_DIR, "cache");
20
+
21
+ type LoadHandler = (id: string) => Promise<string | null>;
22
+
23
+ function getLoadHandler(plugin: ReturnType<typeof sdAngularPlugin>): LoadHandler {
24
+ const config = (plugin as any).config();
25
+ const rolldownPlugin = config.optimizeDeps.rolldownOptions.plugins[0] as {
26
+ load: (id: string) => Promise<string | null>;
27
+ };
28
+ return (id: string) => rolldownPlugin.load(id);
29
+ }
30
+
31
+ describe("Linker disk cache (optimizeDeps)", () => {
32
+ beforeEach(() => {
33
+ fs.mkdirSync(TMP_DIR, { recursive: true });
34
+ mockTransformFile.mockClear();
35
+ mockTransformFile.mockResolvedValue("transformed-code");
36
+ });
37
+
38
+ afterEach(() => {
39
+ fs.rmSync(TMP_DIR, { recursive: true, force: true });
40
+ });
41
+
42
+ // Acceptance: cache miss → transform + store, cache hit → load from disk
43
+ it("caches transformFile result on miss and returns cached on hit", async () => {
44
+ const jsFile = path.join(TMP_DIR, "module.js");
45
+ fs.writeFileSync(jsFile, "export const x = 1;");
46
+
47
+ const plugin = sdAngularPlugin({
48
+ tsconfig: path.join(TMP_DIR, "tsconfig.json"),
49
+ dev: true,
50
+ linkerCacheDir: CACHE_DIR,
51
+ });
52
+ const load = getLoadHandler(plugin);
53
+
54
+ // First call: cache miss
55
+ const result1 = await load(jsFile);
56
+ expect(mockTransformFile).toHaveBeenCalledOnce();
57
+ expect(result1).toBe("transformed-code");
58
+
59
+ // Cache file created
60
+ const cacheFiles = fs.readdirSync(CACHE_DIR);
61
+ expect(cacheFiles.length).toBe(1);
62
+ expect(cacheFiles[0]).toMatch(/^[a-f0-9]+\.js$/);
63
+
64
+ // Second call: cache hit
65
+ mockTransformFile.mockClear();
66
+ const result2 = await load(jsFile);
67
+ expect(mockTransformFile).not.toHaveBeenCalled();
68
+ expect(result2).toBe("transformed-code");
69
+ });
70
+
71
+ // Acceptance: file content change → cache miss
72
+ it("invalidates cache when file content changes", async () => {
73
+ const jsFile = path.join(TMP_DIR, "changing.js");
74
+ fs.writeFileSync(jsFile, "export const v = 1;");
75
+
76
+ const plugin = sdAngularPlugin({
77
+ tsconfig: path.join(TMP_DIR, "tsconfig.json"),
78
+ dev: true,
79
+ linkerCacheDir: CACHE_DIR,
80
+ });
81
+ const load = getLoadHandler(plugin);
82
+
83
+ await load(jsFile);
84
+ expect(mockTransformFile).toHaveBeenCalledOnce();
85
+
86
+ // Change content
87
+ fs.writeFileSync(jsFile, "export const v = 2;");
88
+ mockTransformFile.mockClear();
89
+ mockTransformFile.mockResolvedValue("transformed-v2");
90
+
91
+ const result = await load(jsFile);
92
+ expect(mockTransformFile).toHaveBeenCalledOnce();
93
+ expect(result).toBe("transformed-v2");
94
+ });
95
+
96
+ // Unit: corrupted cache file → graceful fallback
97
+ it("falls back to transform when cache file is corrupted", async () => {
98
+ const jsFile = path.join(TMP_DIR, "fallback.js");
99
+ fs.writeFileSync(jsFile, "export const y = 1;");
100
+
101
+ const plugin = sdAngularPlugin({
102
+ tsconfig: path.join(TMP_DIR, "tsconfig.json"),
103
+ dev: true,
104
+ linkerCacheDir: CACHE_DIR,
105
+ });
106
+ const load = getLoadHandler(plugin);
107
+
108
+ // First call to populate cache
109
+ await load(jsFile);
110
+
111
+ // Remove the cache file to simulate corruption/missing cache
112
+ const cacheFiles = fs.readdirSync(CACHE_DIR);
113
+ fs.rmSync(path.join(CACHE_DIR, cacheFiles[0]));
114
+
115
+ mockTransformFile.mockClear();
116
+ const result = await load(jsFile);
117
+ expect(mockTransformFile).toHaveBeenCalledOnce();
118
+ expect(result).toBe("transformed-code");
119
+ });
120
+
121
+ // Unit: Uint8Array result from transformFile is handled
122
+ it("handles Uint8Array result from transformFile", async () => {
123
+ const jsFile = path.join(TMP_DIR, "uint8.js");
124
+ fs.writeFileSync(jsFile, "export const z = 1;");
125
+
126
+ mockTransformFile.mockResolvedValue(new TextEncoder().encode("uint8-transformed") as unknown as string);
127
+
128
+ const plugin = sdAngularPlugin({
129
+ tsconfig: path.join(TMP_DIR, "tsconfig.json"),
130
+ dev: true,
131
+ linkerCacheDir: CACHE_DIR,
132
+ });
133
+ const load = getLoadHandler(plugin);
134
+
135
+ const result = await load(jsFile);
136
+ expect(result).toBe("uint8-transformed");
137
+
138
+ // Second call: cache hit should also return string
139
+ mockTransformFile.mockClear();
140
+ const result2 = await load(jsFile);
141
+ expect(mockTransformFile).not.toHaveBeenCalled();
142
+ expect(result2).toBe("uint8-transformed");
143
+ });
144
+
145
+ // Unit: non-.js file returns null (filter)
146
+ it("returns null for non-js files", async () => {
147
+ const plugin = sdAngularPlugin({
148
+ tsconfig: path.join(TMP_DIR, "tsconfig.json"),
149
+ dev: true,
150
+ linkerCacheDir: CACHE_DIR,
151
+ });
152
+ const load = getLoadHandler(plugin);
153
+
154
+ const result = await load(path.join(TMP_DIR, "data.json"));
155
+ expect(result).toBeNull();
156
+ expect(mockTransformFile).not.toHaveBeenCalled();
157
+ });
158
+ });