@simplysm/sd-cli 14.0.18 → 14.0.21

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 (124) hide show
  1. package/dist/angular/vite-angular-plugin.d.ts +2 -0
  2. package/dist/angular/vite-angular-plugin.d.ts.map +1 -1
  3. package/dist/angular/vite-angular-plugin.js +57 -28
  4. package/dist/angular/vite-angular-plugin.js.map +1 -1
  5. package/dist/angular/vite-postcss-inline-plugin.d.ts.map +1 -1
  6. package/dist/angular/vite-postcss-inline-plugin.js +4 -1
  7. package/dist/angular/vite-postcss-inline-plugin.js.map +1 -1
  8. package/dist/capacitor/capacitor-android.d.ts +16 -0
  9. package/dist/capacitor/capacitor-android.d.ts.map +1 -0
  10. package/dist/capacitor/capacitor-android.js +289 -0
  11. package/dist/capacitor/capacitor-android.js.map +1 -0
  12. package/dist/capacitor/capacitor.d.ts +0 -50
  13. package/dist/capacitor/capacitor.d.ts.map +1 -1
  14. package/dist/capacitor/capacitor.js +16 -281
  15. package/dist/capacitor/capacitor.js.map +1 -1
  16. package/dist/commands/check.js +2 -2
  17. package/dist/commands/check.js.map +1 -1
  18. package/dist/commands/device.d.ts.map +1 -1
  19. package/dist/commands/device.js +3 -2
  20. package/dist/commands/device.js.map +1 -1
  21. package/dist/commands/lint.d.ts +1 -42
  22. package/dist/commands/lint.d.ts.map +1 -1
  23. package/dist/commands/lint.js +1 -151
  24. package/dist/commands/lint.js.map +1 -1
  25. package/dist/commands/publish.d.ts.map +1 -1
  26. package/dist/commands/publish.js +2 -1
  27. package/dist/commands/publish.js.map +1 -1
  28. package/dist/commands/typecheck.d.ts +3 -40
  29. package/dist/commands/typecheck.d.ts.map +1 -1
  30. package/dist/commands/typecheck.js +3 -232
  31. package/dist/commands/typecheck.js.map +1 -1
  32. package/dist/electron/electron.d.ts.map +1 -1
  33. package/dist/electron/electron.js +20 -8
  34. package/dist/electron/electron.js.map +1 -1
  35. package/dist/orchestrators/DevWatchOrchestrator.d.ts +1 -0
  36. package/dist/orchestrators/DevWatchOrchestrator.d.ts.map +1 -1
  37. package/dist/orchestrators/DevWatchOrchestrator.js +16 -0
  38. package/dist/orchestrators/DevWatchOrchestrator.js.map +1 -1
  39. package/dist/orchestrators/TypecheckOrchestrator.d.ts +74 -0
  40. package/dist/orchestrators/TypecheckOrchestrator.d.ts.map +1 -0
  41. package/dist/orchestrators/TypecheckOrchestrator.js +285 -0
  42. package/dist/orchestrators/TypecheckOrchestrator.js.map +1 -0
  43. package/dist/sd-cli.js +6 -1
  44. package/dist/sd-cli.js.map +1 -1
  45. package/dist/utils/lint-core.d.ts +43 -0
  46. package/dist/utils/lint-core.d.ts.map +1 -0
  47. package/dist/utils/lint-core.js +154 -0
  48. package/dist/utils/lint-core.js.map +1 -0
  49. package/dist/utils/lint-utils.d.ts +1 -1
  50. package/dist/utils/lint-utils.d.ts.map +1 -1
  51. package/dist/utils/server-production-files.d.ts +22 -0
  52. package/dist/utils/server-production-files.d.ts.map +1 -0
  53. package/dist/utils/server-production-files.js +162 -0
  54. package/dist/utils/server-production-files.js.map +1 -0
  55. package/dist/utils/vite-config.d.ts +1 -1
  56. package/dist/utils/vite-config.d.ts.map +1 -1
  57. package/dist/utils/vite-config.js +76 -26
  58. package/dist/utils/vite-config.js.map +1 -1
  59. package/dist/utils/vite-scope-watch-plugin.d.ts.map +1 -1
  60. package/dist/utils/vite-scope-watch-plugin.js +7 -1
  61. package/dist/utils/vite-scope-watch-plugin.js.map +1 -1
  62. package/dist/workers/lint.worker.d.ts +1 -1
  63. package/dist/workers/lint.worker.d.ts.map +1 -1
  64. package/dist/workers/lint.worker.js +1 -1
  65. package/dist/workers/lint.worker.js.map +1 -1
  66. package/dist/workers/server-build.worker.d.ts.map +1 -1
  67. package/dist/workers/server-build.worker.js +11 -161
  68. package/dist/workers/server-build.worker.js.map +1 -1
  69. package/dist/workers/server-runtime.worker.d.ts.map +1 -1
  70. package/dist/workers/server-runtime.worker.js +15 -0
  71. package/dist/workers/server-runtime.worker.js.map +1 -1
  72. package/package.json +9 -7
  73. package/src/angular/vite-angular-plugin.ts +88 -34
  74. package/src/angular/vite-postcss-inline-plugin.ts +5 -1
  75. package/src/capacitor/capacitor-android.ts +368 -0
  76. package/src/capacitor/capacitor.ts +18 -363
  77. package/src/commands/check.ts +2 -2
  78. package/src/commands/device.ts +3 -2
  79. package/src/commands/lint.ts +1 -201
  80. package/src/commands/publish.ts +2 -1
  81. package/src/commands/typecheck.ts +7 -292
  82. package/src/electron/electron.ts +15 -8
  83. package/src/orchestrators/DevWatchOrchestrator.ts +18 -0
  84. package/src/orchestrators/TypecheckOrchestrator.ts +364 -0
  85. package/src/sd-cli.ts +6 -1
  86. package/src/utils/lint-core.ts +205 -0
  87. package/src/utils/lint-utils.ts +1 -1
  88. package/src/utils/server-production-files.ts +186 -0
  89. package/src/utils/vite-config.ts +83 -27
  90. package/src/utils/vite-scope-watch-plugin.ts +6 -1
  91. package/src/workers/lint.worker.ts +1 -1
  92. package/src/workers/server-build.worker.ts +10 -185
  93. package/src/workers/server-runtime.worker.ts +15 -0
  94. package/tests/angular/linker-disk-cache.spec.ts +31 -25
  95. package/tests/angular/vite-angular-plugin-hmr-fallback.spec.ts +15 -15
  96. package/tests/angular/vite-angular-plugin-hmr.spec.ts +9 -9
  97. package/tests/angular/vite-angular-plugin-legacy-watch.spec.ts +108 -0
  98. package/tests/angular/vite-angular-plugin-lint.spec.ts +4 -4
  99. package/tests/angular/vite-angular-plugin-scss-hmr.spec.ts +10 -15
  100. package/tests/angular/vite-angular-plugin.spec.ts +80 -15
  101. package/tests/angular/vite-postcss-inline-plugin.spec.ts +10 -0
  102. package/tests/capacitor/capacitor-android-exports.verify.md +11 -0
  103. package/tests/capacitor/capacitor-android.spec.ts +219 -0
  104. package/tests/capacitor/capacitor-build.spec.ts +17 -21
  105. package/tests/capacitor/capacitor-icon.spec.ts +17 -19
  106. package/tests/capacitor/capacitor-init.spec.ts +18 -14
  107. package/tests/capacitor/capacitor-run.spec.ts +10 -24
  108. package/tests/capacitor/capacitor-workspace.spec.ts +30 -25
  109. package/tests/commands/check.spec.ts +2 -2
  110. package/tests/commands/device.spec.ts +12 -7
  111. package/tests/commands/lint.spec.ts +33 -194
  112. package/tests/commands/publish-set.verify.md +7 -0
  113. package/tests/electron/electron-symlink-cleanup.verify.md +8 -0
  114. package/tests/electron/electron.spec.ts +27 -2
  115. package/tests/orchestrators/dist-delete-watcher.verify.md +10 -0
  116. package/tests/orchestrators/typecheck-orchestrator.spec.ts +180 -0
  117. package/tests/sd-cli-catch-all.verify.md +7 -0
  118. package/tests/utils/lint-core-import-paths.verify.md +10 -0
  119. package/tests/utils/lint-core.spec.ts +188 -0
  120. package/tests/utils/server-production-files-import-paths.verify.md +14 -0
  121. package/tests/utils/vite-config.spec.ts +255 -133
  122. package/tests/utils/vite-scope-watch-plugin.spec.ts +22 -0
  123. package/tests/workers/server-build-context-dispose.verify.md +8 -0
  124. package/tests/workers/server-runtime-worker.spec.ts +48 -4
@@ -0,0 +1,368 @@
1
+ import { fsx, pathx } from "@simplysm/core-node";
2
+ import { env, xml } from "@simplysm/core-common";
3
+ import { consola } from "consola";
4
+ import type { NpmConfig, SdCapacitorAndroidConfig, SdCapacitorConfig } from "../sd-config.types.js";
5
+
6
+ const _logger = consola.withTag("sd:cli:capacitor");
7
+
8
+ /**
9
+ * Android 네이티브 설정 구성
10
+ *
11
+ * JAVA_HOME, Android SDK 경로, AndroidManifest.xml, build.gradle, styles.xml을 설정한다.
12
+ */
13
+ export async function configureAndroid(
14
+ capPath: string,
15
+ config: SdCapacitorConfig,
16
+ npmConfig: NpmConfig,
17
+ ): Promise<void> {
18
+ const androidPath = pathx.posixResolve(capPath, "android");
19
+
20
+ // Android 디렉토리 존재 확인
21
+ if (!(await fsx.exists(androidPath))) {
22
+ throw new Error(`Android 프로젝트 디렉토리를 찾을 수 없습니다: ${androidPath}`);
23
+ }
24
+
25
+ _logger.debug("JAVA_HOME 설정 시작");
26
+ await _configureJavaHomePath(androidPath);
27
+ _logger.debug("JAVA_HOME 설정 완료");
28
+
29
+ _logger.debug("Android SDK 경로 설정 시작");
30
+ await _configureSdkPath(androidPath);
31
+ _logger.debug("Android SDK 경로 설정 완료");
32
+
33
+ _logger.debug("AndroidManifest.xml 설정 시작");
34
+ await _configureManifest(androidPath, config.platform?.android);
35
+ _logger.debug("AndroidManifest.xml 설정 완료");
36
+
37
+ _logger.debug("루트 build.gradle Kotlin 플러그인 설정 시작");
38
+ await _configureRootBuildGradle(androidPath);
39
+ _logger.debug("루트 build.gradle Kotlin 플러그인 설정 완료");
40
+
41
+ _logger.debug("build.gradle 설정 시작");
42
+ await _configureBuildGradle(androidPath, npmConfig.version, config.platform?.android?.sdkVersion);
43
+ _logger.debug("build.gradle 설정 완료");
44
+
45
+ _logger.debug("styles.xml 설정 시작");
46
+ await _configureStyles(androidPath);
47
+ _logger.debug("styles.xml 설정 완료");
48
+ }
49
+
50
+ /**
51
+ * Java 21 경로 자동 탐색
52
+ */
53
+ export async function findJava21(): Promise<string | undefined> {
54
+ const patterns = [
55
+ "C:/Program Files/Amazon Corretto/jdk21*",
56
+ "C:/Program Files/Eclipse Adoptium/jdk-21*",
57
+ "C:/Program Files/Java/jdk-21*",
58
+ "C:/Program Files/Microsoft/jdk-21*",
59
+ "/usr/lib/jvm/java-21*",
60
+ "/usr/lib/jvm/temurin-21*",
61
+ ];
62
+
63
+ for (const pattern of patterns) {
64
+ const matches = await fsx.glob(pattern);
65
+ if (matches.length > 0) {
66
+ return matches.sort().at(-1);
67
+ }
68
+ }
69
+
70
+ return undefined;
71
+ }
72
+
73
+ /**
74
+ * Android SDK 경로 탐색
75
+ */
76
+ export async function findAndroidSdk(): Promise<string | undefined> {
77
+ const androidHome =
78
+ env("ANDROID_HOME") ??
79
+ env("ANDROID_SDK_ROOT");
80
+ if (androidHome != null && (await fsx.exists(androidHome))) {
81
+ return androidHome;
82
+ }
83
+
84
+ const candidates = [
85
+ pathx.posixResolve(env("LOCALAPPDATA") ?? "", "Android/Sdk"),
86
+ pathx.posixResolve(env("HOME") ?? "", "Android/Sdk"),
87
+ "C:/Program Files/Android/Sdk",
88
+ "C:/Android/Sdk",
89
+ ];
90
+
91
+ for (const candidate of candidates) {
92
+ if (await fsx.exists(candidate)) {
93
+ return candidate;
94
+ }
95
+ }
96
+
97
+ return undefined;
98
+ }
99
+
100
+ /**
101
+ * JAVA_HOME 경로 설정 (gradle.properties)
102
+ */
103
+ async function _configureJavaHomePath(androidPath: string): Promise<void> {
104
+ const gradlePropsPath = pathx.posixResolve(androidPath, "gradle.properties");
105
+
106
+ if (!(await fsx.exists(gradlePropsPath))) {
107
+ _logger.warn(`gradle.properties 파일을 찾을 수 없습니다: ${gradlePropsPath}`);
108
+ return;
109
+ }
110
+
111
+ let content = await fsx.read(gradlePropsPath);
112
+
113
+ const java21Path = await findJava21();
114
+ if (java21Path != null && !content.includes("org.gradle.java.home")) {
115
+ // Windows 경로 이스케이프
116
+ const escapedPath = java21Path.replace(/\\/g, "\\\\");
117
+ content += `\norg.gradle.java.home=${escapedPath}\n`;
118
+ await fsx.write(gradlePropsPath, content);
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Android SDK 경로 설정 (local.properties)
124
+ */
125
+ async function _configureSdkPath(androidPath: string): Promise<void> {
126
+ const localPropsPath = pathx.posixResolve(androidPath, "local.properties");
127
+
128
+ const sdkPath = await findAndroidSdk();
129
+ if (sdkPath != null) {
130
+ // Gradle 호환: 항상 forward slash 사용
131
+ await fsx.write(localPropsPath, `sdk.dir=${pathx.posix(sdkPath)}\n`);
132
+ } else {
133
+ throw new Error(
134
+ "Android SDK를 찾을 수 없습니다.\n" +
135
+ "1. Android Studio를 설치하거나\n" +
136
+ "2. ANDROID_HOME 또는 ANDROID_SDK_ROOT 환경 변수를 설정하세요.",
137
+ );
138
+ }
139
+ }
140
+
141
+ /**
142
+ * AndroidManifest.xml 설정 (XML 파서 기반)
143
+ */
144
+ async function _configureManifest(
145
+ androidPath: string,
146
+ androidConfig?: SdCapacitorAndroidConfig,
147
+ ): Promise<void> {
148
+ const manifestPath = pathx.posixResolve(androidPath, "app/src/main/AndroidManifest.xml");
149
+
150
+ if (!(await fsx.exists(manifestPath))) {
151
+ throw new Error(`AndroidManifest.xml 파일을 찾을 수 없습니다: ${manifestPath}`);
152
+ }
153
+
154
+ const content = await fsx.read(manifestPath);
155
+
156
+ // XML 선언 보존 (xml.parse는 선언을 무시하므로 수동 보존)
157
+ const declMatch = content.match(/^(<\?xml[^?]*\?>\s*)/);
158
+ const xmlDecl = declMatch?.[1] ?? "";
159
+ const xmlBody = declMatch != null ? content.slice(declMatch[0].length) : content;
160
+
161
+ type Attrs = Record<string, string>;
162
+ type XmlNode = { $?: Attrs; [key: string]: unknown };
163
+
164
+ const parsed = xml.parse(xmlBody) as { manifest?: XmlNode };
165
+ const manifest = parsed.manifest;
166
+ if (manifest == null) {
167
+ _logger.warn("AndroidManifest.xml에 manifest 요소가 없습니다");
168
+ return;
169
+ }
170
+
171
+ const apps = manifest["application"] as Array<XmlNode | string> | undefined;
172
+ if (apps == null || apps.length === 0) {
173
+ _logger.warn("AndroidManifest.xml에 application 요소가 없습니다");
174
+ return;
175
+ }
176
+ // 자식 요소 없는 <application></application>은 텍스트 노드로 파싱될 수 있음
177
+ if (typeof apps[0] !== "object") {
178
+ apps[0] = {};
179
+ }
180
+ const app = apps[0];
181
+ app.$ ??= {};
182
+
183
+ // usesCleartextTraffic 설정
184
+ app.$["android:usesCleartextTraffic"] ??= "true";
185
+
186
+ // 추가 권한 설정
187
+ const permissions = androidConfig?.permissions ?? [];
188
+ if (permissions.length > 0) {
189
+ if (manifest["uses-permission"] == null) {
190
+ manifest["uses-permission"] = [];
191
+ }
192
+ const permArray = manifest["uses-permission"] as XmlNode[];
193
+ for (const perm of permissions) {
194
+ const permName = `android.permission.${perm.name}`;
195
+ const exists = permArray.some((p) => p.$?.["android:name"] === permName);
196
+ if (!exists) {
197
+ const attrs: Attrs = { "android:name": permName };
198
+ if (perm.maxSdkVersion != null) {
199
+ attrs["android:maxSdkVersion"] = String(perm.maxSdkVersion);
200
+ }
201
+ if (perm.ignore != null) {
202
+ attrs["tools:ignore"] = perm.ignore;
203
+ manifest.$ ??= {};
204
+ manifest.$["xmlns:tools"] ??= "http://schemas.android.com/tools";
205
+ }
206
+ permArray.push({ $: attrs });
207
+ }
208
+ }
209
+ }
210
+
211
+ // 추가 application 속성 설정
212
+ const appConfig = androidConfig?.config;
213
+ if (appConfig != null) {
214
+ for (const [key, value] of Object.entries(appConfig)) {
215
+ const attrName = `android:${key}`;
216
+ app.$[attrName] ??= String(value);
217
+ }
218
+ }
219
+
220
+ // intentFilters 설정
221
+ const intentFilters = androidConfig?.intentFilters ?? [];
222
+ if (intentFilters.length > 0) {
223
+ const activities = app["activity"] as XmlNode[] | undefined;
224
+ const mainActivity = activities?.find((a) => a.$?.["android:name"] === ".MainActivity");
225
+ if (mainActivity != null) {
226
+ if (mainActivity["intent-filter"] == null) {
227
+ mainActivity["intent-filter"] = [];
228
+ }
229
+ const filterArray = mainActivity["intent-filter"] as XmlNode[];
230
+ for (const filter of intentFilters) {
231
+ const filterKey = filter.action ?? filter.category ?? "";
232
+ if (filterKey === "") continue;
233
+ const exists = filterArray.some((f) => {
234
+ const actions = f["action"] as XmlNode[] | undefined;
235
+ const categories = f["category"] as XmlNode[] | undefined;
236
+ return (
237
+ (filter.action != null &&
238
+ actions?.some((a) => a.$?.["android:name"] === filter.action)) ||
239
+ (filter.category != null &&
240
+ categories?.some((c) => c.$?.["android:name"] === filter.category))
241
+ );
242
+ });
243
+ if (!exists) {
244
+ const newFilter: XmlNode = {};
245
+ if (filter.action != null) {
246
+ newFilter["action"] = [{ $: { "android:name": filter.action } }];
247
+ }
248
+ if (filter.category != null) {
249
+ newFilter["category"] = [{ $: { "android:name": filter.category } }];
250
+ }
251
+ filterArray.push(newFilter);
252
+ }
253
+ }
254
+ }
255
+ }
256
+
257
+ const result = xml.stringify(parsed, { format: true, indentBy: " ", suppressEmptyNode: true });
258
+ await fsx.write(manifestPath, xmlDecl + result);
259
+ }
260
+
261
+ /**
262
+ * 루트 build.gradle에 Kotlin Gradle 플러그인 classpath 추가
263
+ */
264
+ async function _configureRootBuildGradle(androidPath: string): Promise<void> {
265
+ const rootBuildGradlePath = pathx.posixResolve(androidPath, "build.gradle");
266
+
267
+ if (!(await fsx.exists(rootBuildGradlePath))) {
268
+ _logger.warn(`루트 build.gradle 파일을 찾을 수 없습니다: ${rootBuildGradlePath}`);
269
+ return;
270
+ }
271
+
272
+ let content = await fsx.read(rootBuildGradlePath);
273
+
274
+ if (!content.includes("kotlin-gradle-plugin")) {
275
+ content = content.replace(
276
+ /classpath 'com\.android\.tools\.build:gradle:[^']+'/,
277
+ `$&\n classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.20'`,
278
+ );
279
+ await fsx.write(rootBuildGradlePath, content);
280
+ }
281
+ }
282
+
283
+ /**
284
+ * build.gradle 수정 (서명 설정 제외)
285
+ */
286
+ async function _configureBuildGradle(
287
+ androidPath: string,
288
+ version: string,
289
+ sdkVersion?: number,
290
+ ): Promise<void> {
291
+ const buildGradlePath = pathx.posixResolve(androidPath, "app/build.gradle");
292
+
293
+ if (!(await fsx.exists(buildGradlePath))) {
294
+ throw new Error(`build.gradle 파일을 찾을 수 없습니다: ${buildGradlePath}`);
295
+ }
296
+
297
+ let content = await fsx.read(buildGradlePath);
298
+
299
+ // versionName, versionCode 설정
300
+ const cleanVersion = version.replace(/-.*$/, "");
301
+ const versionParts = cleanVersion.split(".");
302
+ const versionCode =
303
+ parseInt(versionParts[0] ?? "0") * 1000000 +
304
+ parseInt(versionParts[1] ?? "0") * 1000 +
305
+ parseInt(versionParts[2] ?? "0");
306
+
307
+ content = content.replace(/versionCode \d+/, `versionCode ${versionCode}`);
308
+ content = content.replace(/versionName "[^"]+"/, `versionName "${version}"`);
309
+
310
+ // SDK 버전 설정
311
+ if (sdkVersion != null) {
312
+ content = content.replace(/minSdkVersion .+/, `minSdkVersion ${sdkVersion}`);
313
+ content = content.replace(/targetSdkVersion .+/, `targetSdkVersion ${sdkVersion}`);
314
+ } else {
315
+ content = content.replace(/minSdkVersion .+/, `minSdkVersion rootProject.ext.minSdkVersion`);
316
+ content = content.replace(
317
+ /targetSdkVersion .+/,
318
+ `targetSdkVersion rootProject.ext.targetSdkVersion`,
319
+ );
320
+ }
321
+
322
+ await fsx.write(buildGradlePath, content);
323
+ }
324
+
325
+ /**
326
+ * styles.xml의 스플래시 테마 수정
327
+ *
328
+ * 1. Theme.SplashScreen parent → Theme.AppCompat.DayNight.NoActionBar
329
+ * Theme.SplashScreen은 android:windowBackground에 compat_splash_screen을 설정하여
330
+ * android:background(@drawable/splash)와 이중 표시를 발생시킨다.
331
+ * installSplashScreen()을 호출하지 않으므로 Theme.SplashScreen 기능이 불필요하다.
332
+ *
333
+ * 2. android:background → android:windowBackground
334
+ * android:background는 View 레벨 속성으로 AppCompat 뷰 계층의 여러 View에 상속되어
335
+ * 동일한 splash 로고가 다중 레이어에 중복 렌더링된다.
336
+ * android:windowBackground는 Window의 DecorView에만 적용되어 단일 렌더링을 보장한다.
337
+ */
338
+ async function _configureStyles(androidPath: string): Promise<void> {
339
+ const stylesPath = pathx.posixResolve(androidPath, "app/src/main/res/values/styles.xml");
340
+
341
+ if (!(await fsx.exists(stylesPath))) {
342
+ _logger.warn(`styles.xml 파일을 찾을 수 없습니다: ${stylesPath}`);
343
+ return;
344
+ }
345
+
346
+ let content = await fsx.read(stylesPath);
347
+ let changed = false;
348
+
349
+ if (content.includes('parent="Theme.SplashScreen"')) {
350
+ content = content.replace(
351
+ 'parent="Theme.SplashScreen"',
352
+ 'parent="Theme.AppCompat.DayNight.NoActionBar"',
353
+ );
354
+ changed = true;
355
+ }
356
+
357
+ if (content.includes('"android:background">@drawable/splash')) {
358
+ content = content.replace(
359
+ '"android:background">@drawable/splash',
360
+ '"android:windowBackground">@drawable/splash',
361
+ );
362
+ changed = true;
363
+ }
364
+
365
+ if (changed) {
366
+ await fsx.write(stylesPath, content);
367
+ }
368
+ }