@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,186 @@
1
+ import type { ServerBuildInfo } from "../workers/server-build.worker";
2
+ import path from "path";
3
+ import fs from "fs";
4
+ import { cpx } from "@simplysm/core-node";
5
+ import { consola } from "consola";
6
+ import { collectAllDependencyExternals } from "./esbuild-config";
7
+
8
+ const logger = consola.withTag("sd:cli:server-production-files");
9
+
10
+ /**
11
+ * 세 가지 소스에서 외부 모듈을 수집하고 병합한다.
12
+ * collectAllDependencyExternals를 통한 단일 패스 의존성 트리 순회를 사용한다.
13
+ */
14
+ export function collectAllExternals(pkgDir: string, manualExternals?: string[]): string[] {
15
+ logger.debug("의존성 트리 스캔 중...");
16
+ const { optionalPeerDeps, nativeModules } = collectAllDependencyExternals(pkgDir);
17
+
18
+ const manual = manualExternals ?? [];
19
+ return [...new Set([...optionalPeerDeps, ...nativeModules, ...manual])];
20
+ }
21
+
22
+ /**
23
+ * pnpm-lock.yaml의 packages 섹션을 파싱하여 name→version 맵을 생성한다.
24
+ * Lockfile v9 형식: `packages:` 섹션의 `'name@version':` 키를 파싱한다.
25
+ * YAML 파서 의존성을 피하기 위해 단순 라인 기반 파싱을 사용한다.
26
+ */
27
+ export function parseLockfileVersions(cwd: string): Map<string, string> {
28
+ const lockfilePath = path.join(cwd, "pnpm-lock.yaml");
29
+ if (!fs.existsSync(lockfilePath)) {
30
+ throw new Error(`pnpm-lock.yaml not found in ${cwd}. Run "pnpm install" first.`);
31
+ }
32
+
33
+ const content = fs.readFileSync(lockfilePath, "utf-8");
34
+ const map = new Map<string, string>();
35
+
36
+ // "packages:" 섹션을 찾고 "'@scope/name@1.2.3':" 또는 "'name@1.2.3':" 형태의 항목을 파싱
37
+ const lines = content.split("\n");
38
+ let inPackages = false;
39
+ for (const line of lines) {
40
+ if (line === "packages:") {
41
+ inPackages = true;
42
+ continue;
43
+ }
44
+ if (inPackages && line.length > 0 && !line.startsWith(" ") && !line.startsWith("'")) {
45
+ break; // 다음 최상위 섹션
46
+ }
47
+ if (!inPackages) continue;
48
+
49
+ // "'@scope/name@version':" 또는 "'name@version':" 매칭
50
+ const match = /^\s{2}'(.+)@(\d[^']*)':\s*$/.exec(line);
51
+ if (match != null) {
52
+ const name = match[1];
53
+ const version = match[2];
54
+ // 첫 번째 항목 유지 (lockfile은 각 버전을 한 번만 기록)
55
+ if (!map.has(name)) {
56
+ map.set(name, version);
57
+ }
58
+ }
59
+ }
60
+
61
+ return map;
62
+ }
63
+
64
+ /**
65
+ * pnpm-lock.yaml에서 주어진 모든 패키지의 잠긴 버전을 확인한다.
66
+ * lockfile에서 패키지를 찾을 수 없으면 에러를 던진다.
67
+ */
68
+ export function resolveLockedVersions(cwd: string, pkgNames: string[]): Record<string, string> {
69
+ const versionMap = parseLockfileVersions(cwd);
70
+ const result: Record<string, string> = {};
71
+ for (const name of pkgNames) {
72
+ const version = versionMap.get(name);
73
+ if (version == null) {
74
+ throw new Error(
75
+ `External dependency "${name}" not found in pnpm-lock.yaml. ` +
76
+ `Run "pnpm install" and try again.`,
77
+ );
78
+ }
79
+ result[name] = version;
80
+ }
81
+ return result;
82
+ }
83
+
84
+ /**
85
+ * 프로덕션 배포용 파일을 생성한다
86
+ */
87
+ export function generateProductionFiles(
88
+ info: ServerBuildInfo,
89
+ externals: string[],
90
+ ): void {
91
+ const distDir = path.join(info.pkgDir, "dist");
92
+ const pkgJson = JSON.parse(fs.readFileSync(path.join(info.pkgDir, "package.json"), "utf-8"));
93
+
94
+ // dist/package.json
95
+ const distPkgJson: Record<string, unknown> = {
96
+ name: pkgJson.name,
97
+ version: pkgJson.version,
98
+ type: pkgJson.type,
99
+ };
100
+ if (externals.length > 0) {
101
+ distPkgJson["dependencies"] = resolveLockedVersions(info.cwd, externals);
102
+ }
103
+ if (info.packageManager === "volta") {
104
+ const nodeVersion = cpx.spawnSync("node", ["-v"]).stdout.trim();
105
+ distPkgJson["volta"] = { node: nodeVersion };
106
+ }
107
+ fs.writeFileSync(path.join(distDir, "package.json"), JSON.stringify(distPkgJson, undefined, 2));
108
+
109
+ // dist/mise.toml
110
+ if (info.packageManager === "mise") {
111
+ const rootMiseTomlPath = path.join(info.cwd, "mise.toml");
112
+ let nodeVersion = "20";
113
+ if (fs.existsSync(rootMiseTomlPath)) {
114
+ const miseContent = fs.readFileSync(rootMiseTomlPath, "utf-8");
115
+ const match = /node\s*=\s*"([^"]+)"/.exec(miseContent);
116
+ if (match != null) {
117
+ nodeVersion = match[1];
118
+ }
119
+ }
120
+ fs.writeFileSync(path.join(distDir, "mise.toml"), `[tools]\nnode = "${nodeVersion}"\n`);
121
+ }
122
+
123
+ // dist/openssl.cnf
124
+ fs.writeFileSync(
125
+ path.join(distDir, "openssl.cnf"),
126
+ [
127
+ "nodejs_conf = openssl_init",
128
+ "",
129
+ "[openssl_init]",
130
+ "providers = provider_sect",
131
+ "ssl_conf = ssl_sect",
132
+ "",
133
+ "[provider_sect]",
134
+ "default = default_sect",
135
+ "legacy = legacy_sect",
136
+ "",
137
+ "[default_sect]",
138
+ "activate = 1",
139
+ "",
140
+ "[legacy_sect]",
141
+ "activate = 1",
142
+ "",
143
+ "[ssl_sect]",
144
+ "system_default = system_default_sect",
145
+ "",
146
+ "[system_default_sect]",
147
+ "Options = UnsafeLegacyRenegotiation",
148
+ ].join("\n"),
149
+ );
150
+
151
+ // dist/pm2.config.cjs
152
+ if (info.pm2 != null) {
153
+ const pm2Name = info.pm2.name ?? pkgJson.name.replace(/@/g, "").replace(/[/\\]/g, "-");
154
+ const ignoreWatch = JSON.stringify([
155
+ "node_modules",
156
+ "www",
157
+ ...(info.pm2.ignoreWatchPaths ?? []),
158
+ ]);
159
+ const envObj: Record<string, string> = {
160
+ NODE_ENV: "production",
161
+ TZ: "Asia/Seoul",
162
+ ...(info.env ?? {}),
163
+ };
164
+ const envStr = JSON.stringify(envObj, undefined, 4);
165
+
166
+ const useInterpreter = info.packageManager !== "volta";
167
+
168
+ const pm2Config = [
169
+ ...(useInterpreter ? [`const cp = require("child_process");`, ``] : []),
170
+ `module.exports = {`,
171
+ ` name: ${JSON.stringify(pm2Name)},`,
172
+ ` script: "main.js",`,
173
+ ` watch: true,`,
174
+ ` watch_delay: 2000,`,
175
+ ` ignore_watch: ${ignoreWatch},`,
176
+ ...(useInterpreter ? [` interpreter: cp.execSync("mise which node").toString().trim(),`] : []),
177
+ ` interpreter_args: "--openssl-config=openssl.cnf",`,
178
+ ` env: ${envStr.replace(/\n/g, "\n ")},`,
179
+ ` arrayProcess: "concat",`,
180
+ ` useDelTargetNull: true,`,
181
+ `};`,
182
+ ].join("\n");
183
+
184
+ fs.writeFileSync(path.join(distDir, "pm2.config.cjs"), pm2Config);
185
+ }
186
+ }
@@ -1,6 +1,9 @@
1
1
  import type { InlineConfig, PluginOption } from "vite";
2
2
  import path from "path";
3
+ import fs from "fs";
4
+ import tsconfigPaths from "vite-tsconfig-paths";
3
5
  import browserslistToEsbuild from "browserslist-to-esbuild";
6
+ import { pathx } from "@simplysm/core-node";
4
7
  import { sdAngularPlugin } from "../angular/vite-angular-plugin.js";
5
8
  import solidPlugin from "vite-plugin-solid";
6
9
  import {
@@ -9,7 +12,8 @@ import {
9
12
  } from "./vite-scope-watch-plugin.js";
10
13
  import { sdPostCssInlinePlugin } from "../angular/vite-postcss-inline-plugin.js";
11
14
  import type { SdPwaConfig } from "../sd-config.types.js";
12
- import { sdPwaPlugin } from "./vite-pwa-plugin.js";
15
+ import { VitePWA } from "vite-plugin-pwa";
16
+ import { generatePwaIcons } from "./generate-pwa-icons.js";
13
17
 
14
18
  /** createClientViteConfig 옵션 */
15
19
  export interface CreateClientViteConfigOptions {
@@ -68,9 +72,9 @@ export interface CreateClientViteConfigOptions {
68
72
  * Angular AOT 플러그인, tsconfigPaths, env define, server/build 기본 설정,
69
73
  * browserslist, PostCSS, polyfills, legacyModule (inlineDynamicImports) 등을 통합 구성한다.
70
74
  */
71
- export function createClientViteConfig(
75
+ export async function createClientViteConfig(
72
76
  options: CreateClientViteConfigOptions,
73
- ): InlineConfig {
77
+ ): Promise<InlineConfig> {
74
78
  const name = options.pkgName.replace(/^@[^/]+\//, "");
75
79
 
76
80
  // browserslist → esbuild target
@@ -98,8 +102,29 @@ export function createClientViteConfig(
98
102
  }
99
103
  }
100
104
 
105
+ // replaceDeps dist 경로 (symlink → realpath 해결)
106
+ let replaceDepDistPaths: string[] | undefined;
107
+ if (options.replaceDeps != null && options.replaceDeps.length > 0) {
108
+ replaceDepDistPaths = [];
109
+ for (const dep of options.replaceDeps) {
110
+ const distDir = path.join(
111
+ options.pkgDir,
112
+ "node_modules",
113
+ ...dep.packageName.split("/"),
114
+ "dist",
115
+ );
116
+ try {
117
+ replaceDepDistPaths.push(pathx.posix(fs.realpathSync(distDir)));
118
+ } catch {
119
+ replaceDepDistPaths.push(pathx.posix(distDir));
120
+ }
121
+ }
122
+ }
123
+
101
124
  // plugins
102
- const plugins: PluginOption[] = [];
125
+ const plugins: PluginOption[] = [
126
+ tsconfigPaths({ projects: [options.tsconfigPath] }),
127
+ ];
103
128
 
104
129
  if (options.framework === "solid") {
105
130
  plugins.push(solidPlugin());
@@ -115,6 +140,7 @@ export function createClientViteConfig(
115
140
  enableLint: options.enableLint,
116
141
  browserslist: normalizedBrowserslist,
117
142
  postCssPlugins: options.postCssPlugins,
143
+ replaceDepDistPaths,
118
144
  }),
119
145
  );
120
146
 
@@ -157,32 +183,70 @@ export function createClientViteConfig(
157
183
  ? { postcss: { plugins: options.postCssPlugins as import("postcss").AcceptedPlugin[] } }
158
184
  : undefined;
159
185
 
160
- // optimizeDeps.exclude (사용자 지정 exclude)
186
+ // optimizeDeps.exclude (사용자 지정 exclude + replaceDeps 패키지)
187
+ const excludeList = [
188
+ ...(options.exclude ?? []),
189
+ ...(options.replaceDeps?.map((dep) => dep.packageName) ?? []),
190
+ ];
161
191
  const optimizeDepsConfig =
162
- options.exclude != null && options.exclude.length > 0
163
- ? { exclude: options.exclude }
164
- : undefined;
192
+ excludeList.length > 0 ? { exclude: excludeList } : undefined;
165
193
 
166
194
  const config: InlineConfig = {
167
195
  root: options.pkgDir,
168
196
  base: options.base ?? `/${name}/`,
169
- resolve: { tsconfigPaths: true },
170
197
  define: Object.keys(define).length > 0 ? define : undefined,
171
198
  plugins,
172
199
  server: serverConfig,
173
200
  css: cssConfig,
201
+ esbuild: {
202
+ target: esbuildTarget,
203
+ },
174
204
  build: {
175
205
  target: esbuildTarget,
176
206
  },
177
207
  optimizeDeps: {
178
208
  ...optimizeDepsConfig,
209
+ esbuildOptions: {
210
+ target: esbuildTarget as string[],
211
+ },
179
212
  },
180
213
  };
181
214
 
182
215
  // PWA (build 모드 + pwa !== false)
183
216
  if (options.mode === "build" && options.pwa !== false) {
217
+ const pwaConfig = typeof options.pwa === "object" ? options.pwa : {};
218
+
219
+ // 아이콘 자동 생성 (커스텀 icons 미설정 시)
220
+ let iconsConfig: Record<string, unknown> = {};
221
+ if (pwaConfig.manifest?.icons != null) {
222
+ iconsConfig = { icons: pwaConfig.manifest.icons };
223
+ } else {
224
+ const generatedIcons = await generatePwaIcons(options.pkgDir);
225
+ if (generatedIcons.length > 0) {
226
+ iconsConfig = { icons: generatedIcons };
227
+ }
228
+ }
229
+
230
+ const pwaManifest = {
231
+ name: pwaConfig.manifest?.name ?? name,
232
+ short_name: pwaConfig.manifest?.short_name ?? name,
233
+ display: pwaConfig.manifest?.display ?? "standalone",
234
+ theme_color: pwaConfig.manifest?.theme_color ?? "#ffffff",
235
+ background_color: pwaConfig.manifest?.background_color ?? "#ffffff",
236
+ ...iconsConfig,
237
+ };
238
+ const pwaWorkbox = {
239
+ globPatterns: pwaConfig.workbox?.globPatterns ?? [
240
+ "**/*.{js,css,html,ico,png,svg,woff2}",
241
+ ],
242
+ };
184
243
  (config.plugins as PluginOption[]).push(
185
- sdPwaPlugin({ pkgDir: options.pkgDir, pkgName: name, pwa: options.pwa }),
244
+ VitePWA({
245
+ registerType: "prompt",
246
+ injectRegister: "script",
247
+ manifest: pwaManifest,
248
+ workbox: pwaWorkbox,
249
+ }),
186
250
  );
187
251
  }
188
252
 
@@ -231,18 +295,24 @@ export function createClientViteConfig(
231
295
  }
232
296
  }
233
297
 
234
- // legacyModule: true → 코드 스플리팅 비활성화 + import.meta/import() 치환 (Chrome 61 호환)
298
+ // legacyModule: true → 코드 스플리팅 비활성화 + esbuild import.meta 변환 + 잔여 import() 제거
235
299
  if (options.legacyModule === true) {
300
+ config.esbuild = {
301
+ ...config.esbuild,
302
+ supported: {
303
+ "import-meta": false,
304
+ },
305
+ };
236
306
  config.build = {
237
307
  ...config.build,
238
- rolldownOptions: {
308
+ rollupOptions: {
239
309
  output: {
240
310
  inlineDynamicImports: true,
241
311
  },
242
312
  },
243
313
  };
244
314
 
245
- // Rolldown이 인라인하지 못한 잔여 dynamic import()를 제거한다.
315
+ // Rollup이 인라인하지 못한 잔여 dynamic import()를 제거한다.
246
316
  // inlineDynamicImports가 정적 경로를 모두 인라인한 후에도,
247
317
  // @vite-ignore나 런타임 계산 경로의 import()가 남을 수 있다.
248
318
  // Chrome 61은 import() 구문을 파싱하지 못하므로 no-op 함수로 치환한다.
@@ -260,20 +330,6 @@ export function createClientViteConfig(
260
330
  };
261
331
  },
262
332
  });
263
-
264
- // import.meta 구문을 치환한다. Chrome 61은 import.meta를 파싱하지 못한다 (Chrome 64+).
265
- // Vite/Rolldown이 빌드 시 대부분의 import.meta를 resolve하지만, 잔여분에 대한 안전망이다.
266
- (config.plugins as PluginOption[]).push({
267
- name: "sd-legacy-strip-import-meta",
268
- enforce: "post",
269
- renderChunk(code) {
270
- if (!code.includes("import.meta")) return null;
271
- return {
272
- code: code.replace(/\bimport\.meta\b/g, "(void 0)"),
273
- map: null,
274
- };
275
- },
276
- });
277
333
  }
278
334
 
279
335
  // build 모드 설정 (프로덕션 빌드 또는 legacyModule dev)
@@ -51,7 +51,12 @@ export function sdScopeWatchPlugin(options: SdScopeWatchPluginOptions): Plugin {
51
51
  "dist",
52
52
  );
53
53
  if (fs.existsSync(distDir)) {
54
- watchPaths.push(distDir);
54
+ // symlink → realpath 해결 (Vite 모듈 그래프가 realpath를 키로 사용)
55
+ try {
56
+ watchPaths.push(fs.realpathSync(distDir));
57
+ } catch {
58
+ watchPaths.push(distDir);
59
+ }
55
60
  }
56
61
  }
57
62
 
@@ -1,5 +1,5 @@
1
1
  import { createWorker } from "@simplysm/core-node";
2
- import { executeLint, type LintOptions, type LintResult } from "../commands/lint";
2
+ import { executeLint, type LintOptions, type LintResult } from "../utils/lint-core";
3
3
 
4
4
  //#region Worker
5
5
 
@@ -2,7 +2,7 @@ import type ts from "typescript";
2
2
  import path from "path";
3
3
  import fs from "fs";
4
4
  import esbuild from "esbuild";
5
- import { cpx, createWorker, FsWatcher, pathx } from "@simplysm/core-node";
5
+ import { createWorker, FsWatcher, pathx } from "@simplysm/core-node";
6
6
  import { err as errNs } from "@simplysm/core-common";
7
7
  import { consola } from "consola";
8
8
  import type { BuildOutput } from "../engines/types";
@@ -14,9 +14,9 @@ import {
14
14
  } from "../utils/tsconfig";
15
15
  import {
16
16
  createServerEsbuildOptions,
17
- collectAllDependencyExternals,
18
17
  writeChangedOutputFiles,
19
18
  } from "../utils/esbuild-config";
19
+ import { collectAllExternals, generateProductionFiles } from "../utils/server-production-files";
20
20
  import { runTscPackageBuild } from "../utils/tsc-build";
21
21
  import { LintWithProgramRunner } from "../utils/lint-with-program";
22
22
  import { registerCleanupHandlers, createOnceGuard, setupWorkerConsola } from "../utils/worker-utils";
@@ -136,184 +136,6 @@ async function cleanup(): Promise<void> {
136
136
  }
137
137
  }
138
138
 
139
- /**
140
- * 세 가지 소스에서 외부 모듈을 수집하고 병합한다.
141
- * collectAllDependencyExternals를 통한 단일 패스 의존성 트리 순회를 사용한다.
142
- */
143
- function collectAllExternals(pkgDir: string, manualExternals?: string[]): string[] {
144
- logger.debug("의존성 트리 스캔 중...");
145
- const { optionalPeerDeps, nativeModules } = collectAllDependencyExternals(pkgDir);
146
-
147
- const manual = manualExternals ?? [];
148
- return [...new Set([...optionalPeerDeps, ...nativeModules, ...manual])];
149
- }
150
-
151
- /**
152
- * pnpm-lock.yaml의 packages 섹션을 파싱하여 name→version 맵을 생성한다.
153
- * Lockfile v9 형식: `packages:` 섹션의 `'name@version':` 키를 파싱한다.
154
- * YAML 파서 의존성을 피하기 위해 단순 라인 기반 파싱을 사용한다.
155
- */
156
- function parseLockfileVersions(cwd: string): Map<string, string> {
157
- const lockfilePath = path.join(cwd, "pnpm-lock.yaml");
158
- if (!fs.existsSync(lockfilePath)) {
159
- throw new Error(`pnpm-lock.yaml not found in ${cwd}. Run "pnpm install" first.`);
160
- }
161
-
162
- const content = fs.readFileSync(lockfilePath, "utf-8");
163
- const map = new Map<string, string>();
164
-
165
- // "packages:" 섹션을 찾고 "'@scope/name@1.2.3':" 또는 "'name@1.2.3':" 형태의 항목을 파싱
166
- const lines = content.split("\n");
167
- let inPackages = false;
168
- for (const line of lines) {
169
- if (line === "packages:") {
170
- inPackages = true;
171
- continue;
172
- }
173
- if (inPackages && line.length > 0 && !line.startsWith(" ") && !line.startsWith("'")) {
174
- break; // 다음 최상위 섹션
175
- }
176
- if (!inPackages) continue;
177
-
178
- // "'@scope/name@version':" 또는 "'name@version':" 매칭
179
- const match = /^\s{2}'(.+)@(\d[^']*)':\s*$/.exec(line);
180
- if (match != null) {
181
- const name = match[1];
182
- const version = match[2];
183
- // 첫 번째 항목 유지 (lockfile은 각 버전을 한 번만 기록)
184
- if (!map.has(name)) {
185
- map.set(name, version);
186
- }
187
- }
188
- }
189
-
190
- return map;
191
- }
192
-
193
- /**
194
- * pnpm-lock.yaml에서 주어진 모든 패키지의 잠긴 버전을 확인한다.
195
- * lockfile에서 패키지를 찾을 수 없으면 에러를 던진다.
196
- */
197
- function resolveLockedVersions(cwd: string, pkgNames: string[]): Record<string, string> {
198
- const versionMap = parseLockfileVersions(cwd);
199
- const result: Record<string, string> = {};
200
- for (const name of pkgNames) {
201
- const version = versionMap.get(name);
202
- if (version == null) {
203
- throw new Error(
204
- `External dependency "${name}" not found in pnpm-lock.yaml. ` +
205
- `Run "pnpm install" and try again.`,
206
- );
207
- }
208
- result[name] = version;
209
- }
210
- return result;
211
- }
212
-
213
- /**
214
- * 프로덕션 배포용 파일을 생성한다
215
- */
216
- function generateProductionFiles(
217
- info: ServerBuildInfo,
218
- externals: string[],
219
- ): void {
220
- const distDir = path.join(info.pkgDir, "dist");
221
- const pkgJson = JSON.parse(fs.readFileSync(path.join(info.pkgDir, "package.json"), "utf-8"));
222
-
223
- // dist/package.json
224
- const distPkgJson: Record<string, unknown> = {
225
- name: pkgJson.name,
226
- version: pkgJson.version,
227
- type: pkgJson.type,
228
- };
229
- if (externals.length > 0) {
230
- distPkgJson["dependencies"] = resolveLockedVersions(info.cwd, externals);
231
- }
232
- if (info.packageManager === "volta") {
233
- const nodeVersion = cpx.spawnSync("node", ["-v"]).stdout.trim();
234
- distPkgJson["volta"] = { node: nodeVersion };
235
- }
236
- fs.writeFileSync(path.join(distDir, "package.json"), JSON.stringify(distPkgJson, undefined, 2));
237
-
238
- // dist/mise.toml
239
- if (info.packageManager === "mise") {
240
- const rootMiseTomlPath = path.join(info.cwd, "mise.toml");
241
- let nodeVersion = "20";
242
- if (fs.existsSync(rootMiseTomlPath)) {
243
- const miseContent = fs.readFileSync(rootMiseTomlPath, "utf-8");
244
- const match = /node\s*=\s*"([^"]+)"/.exec(miseContent);
245
- if (match != null) {
246
- nodeVersion = match[1];
247
- }
248
- }
249
- fs.writeFileSync(path.join(distDir, "mise.toml"), `[tools]\nnode = "${nodeVersion}"\n`);
250
- }
251
-
252
- // dist/openssl.cnf
253
- fs.writeFileSync(
254
- path.join(distDir, "openssl.cnf"),
255
- [
256
- "nodejs_conf = openssl_init",
257
- "",
258
- "[openssl_init]",
259
- "providers = provider_sect",
260
- "ssl_conf = ssl_sect",
261
- "",
262
- "[provider_sect]",
263
- "default = default_sect",
264
- "legacy = legacy_sect",
265
- "",
266
- "[default_sect]",
267
- "activate = 1",
268
- "",
269
- "[legacy_sect]",
270
- "activate = 1",
271
- "",
272
- "[ssl_sect]",
273
- "system_default = system_default_sect",
274
- "",
275
- "[system_default_sect]",
276
- "Options = UnsafeLegacyRenegotiation",
277
- ].join("\n"),
278
- );
279
-
280
- // dist/pm2.config.cjs
281
- if (info.pm2 != null) {
282
- const pm2Name = info.pm2.name ?? pkgJson.name.replace(/@/g, "").replace(/[/\\]/g, "-");
283
- const ignoreWatch = JSON.stringify([
284
- "node_modules",
285
- "www",
286
- ...(info.pm2.ignoreWatchPaths ?? []),
287
- ]);
288
- const envObj: Record<string, string> = {
289
- NODE_ENV: "production",
290
- TZ: "Asia/Seoul",
291
- ...(info.env ?? {}),
292
- };
293
- const envStr = JSON.stringify(envObj, undefined, 4);
294
-
295
- const useInterpreter = info.packageManager !== "volta";
296
-
297
- const pm2Config = [
298
- ...(useInterpreter ? [`const cp = require("child_process");`, ``] : []),
299
- `module.exports = {`,
300
- ` name: ${JSON.stringify(pm2Name)},`,
301
- ` script: "main.js",`,
302
- ` watch: true,`,
303
- ` watch_delay: 2000,`,
304
- ` ignore_watch: ${ignoreWatch},`,
305
- ...(useInterpreter ? [` interpreter: cp.execSync("mise which node").toString().trim(),`] : []),
306
- ` interpreter_args: "--openssl-config=openssl.cnf",`,
307
- ` env: ${envStr.replace(/\n/g, "\n ")},`,
308
- ` arrayProcess: "concat",`,
309
- ` useDelTargetNull: true,`,
310
- `};`,
311
- ].join("\n");
312
-
313
- fs.writeFileSync(path.join(distDir, "pm2.config.cjs"), pm2Config);
314
- }
315
- }
316
-
317
139
  registerCleanupHandlers(cleanup, logger);
318
140
 
319
141
  //#endregion
@@ -610,11 +432,14 @@ async function startWatch(info: ServerWatchInfo): Promise<void> {
610
432
  const newExternal = cachedExternal;
611
433
 
612
434
  const oldContext = esbuildContext;
613
- if (info.output.js) {
614
- esbuildContext = await createEsbuildWatchContext(info, newEntryPoints, newExternal);
615
- }
616
- if (oldContext != null) {
617
- await oldContext.dispose();
435
+ try {
436
+ if (info.output.js) {
437
+ esbuildContext = await createEsbuildWatchContext(info, newEntryPoints, newExternal);
438
+ }
439
+ } finally {
440
+ if (oldContext != null) {
441
+ await oldContext.dispose();
442
+ }
618
443
  }
619
444
 
620
445
  const result = await rebuildAll();
@@ -1,3 +1,5 @@
1
+ import fs from "node:fs";
2
+ import path from "path";
1
3
  import { createWorker } from "@simplysm/core-node";
2
4
  import { env, err as errNs } from "@simplysm/core-common";
3
5
  import { consola } from "consola";
@@ -49,10 +51,18 @@ const logger = consola.withTag("sd:cli:server-runtime:worker");
49
51
  /** 서버 인스턴스 (정리 대상) */
50
52
  let serverInstance: { close: () => Promise<void> } | undefined;
51
53
 
54
+ /** .dev-port 기록 경로 (cleanup에서 삭제용) */
55
+ let mainJsDir: string | undefined;
56
+
52
57
  /**
53
58
  * 리소스 정리
54
59
  */
55
60
  async function cleanup(): Promise<void> {
61
+ if (mainJsDir != null) {
62
+ try { fs.unlinkSync(path.join(mainJsDir, ".dev-port")); } catch { /* 파일 없으면 무시 */ }
63
+ mainJsDir = undefined;
64
+ }
65
+
56
66
  const server = serverInstance;
57
67
  serverInstance = undefined;
58
68
  if (server != null) {
@@ -172,6 +182,11 @@ async function start(info: ServerRuntimeStartInfo): Promise<void> {
172
182
  await server.listen();
173
183
  logger.debug(`서버 리슨 완료 (${Math.round(performance.now() - stepStart)}ms)`);
174
184
 
185
+ // .dev-port 기록 (device 명령어에서 자동 탐지용)
186
+ mainJsDir = path.dirname(info.mainJsPath);
187
+ fs.mkdirSync(mainJsDir, { recursive: true });
188
+ fs.writeFileSync(path.join(mainJsDir, ".dev-port"), String(server.options.port));
189
+
175
190
  logger.debug(
176
191
  `런타임 총 시작 시간: ${Math.round(performance.now() - startTime)}ms`,
177
192
  );