@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.
- package/dist/angular/vite-angular-plugin.d.ts +2 -0
- package/dist/angular/vite-angular-plugin.d.ts.map +1 -1
- package/dist/angular/vite-angular-plugin.js +57 -28
- package/dist/angular/vite-angular-plugin.js.map +1 -1
- package/dist/angular/vite-postcss-inline-plugin.d.ts.map +1 -1
- package/dist/angular/vite-postcss-inline-plugin.js +4 -1
- package/dist/angular/vite-postcss-inline-plugin.js.map +1 -1
- package/dist/capacitor/capacitor-android.d.ts +16 -0
- package/dist/capacitor/capacitor-android.d.ts.map +1 -0
- package/dist/capacitor/capacitor-android.js +289 -0
- package/dist/capacitor/capacitor-android.js.map +1 -0
- package/dist/capacitor/capacitor.d.ts +0 -50
- package/dist/capacitor/capacitor.d.ts.map +1 -1
- package/dist/capacitor/capacitor.js +16 -281
- package/dist/capacitor/capacitor.js.map +1 -1
- package/dist/commands/check.js +2 -2
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/device.d.ts.map +1 -1
- package/dist/commands/device.js +3 -2
- package/dist/commands/device.js.map +1 -1
- package/dist/commands/lint.d.ts +1 -42
- package/dist/commands/lint.d.ts.map +1 -1
- package/dist/commands/lint.js +1 -151
- package/dist/commands/lint.js.map +1 -1
- package/dist/commands/publish.d.ts.map +1 -1
- package/dist/commands/publish.js +2 -1
- package/dist/commands/publish.js.map +1 -1
- package/dist/commands/typecheck.d.ts +3 -40
- package/dist/commands/typecheck.d.ts.map +1 -1
- package/dist/commands/typecheck.js +3 -232
- package/dist/commands/typecheck.js.map +1 -1
- package/dist/electron/electron.d.ts.map +1 -1
- package/dist/electron/electron.js +20 -8
- package/dist/electron/electron.js.map +1 -1
- package/dist/orchestrators/DevWatchOrchestrator.d.ts +1 -0
- package/dist/orchestrators/DevWatchOrchestrator.d.ts.map +1 -1
- package/dist/orchestrators/DevWatchOrchestrator.js +16 -0
- package/dist/orchestrators/DevWatchOrchestrator.js.map +1 -1
- package/dist/orchestrators/TypecheckOrchestrator.d.ts +74 -0
- package/dist/orchestrators/TypecheckOrchestrator.d.ts.map +1 -0
- package/dist/orchestrators/TypecheckOrchestrator.js +285 -0
- package/dist/orchestrators/TypecheckOrchestrator.js.map +1 -0
- package/dist/sd-cli.js +6 -1
- package/dist/sd-cli.js.map +1 -1
- package/dist/utils/lint-core.d.ts +43 -0
- package/dist/utils/lint-core.d.ts.map +1 -0
- package/dist/utils/lint-core.js +154 -0
- package/dist/utils/lint-core.js.map +1 -0
- package/dist/utils/lint-utils.d.ts +1 -1
- package/dist/utils/lint-utils.d.ts.map +1 -1
- package/dist/utils/server-production-files.d.ts +22 -0
- package/dist/utils/server-production-files.d.ts.map +1 -0
- package/dist/utils/server-production-files.js +162 -0
- package/dist/utils/server-production-files.js.map +1 -0
- package/dist/utils/vite-config.d.ts +1 -1
- package/dist/utils/vite-config.d.ts.map +1 -1
- package/dist/utils/vite-config.js +76 -26
- package/dist/utils/vite-config.js.map +1 -1
- package/dist/utils/vite-scope-watch-plugin.d.ts.map +1 -1
- package/dist/utils/vite-scope-watch-plugin.js +7 -1
- package/dist/utils/vite-scope-watch-plugin.js.map +1 -1
- package/dist/workers/lint.worker.d.ts +1 -1
- package/dist/workers/lint.worker.d.ts.map +1 -1
- package/dist/workers/lint.worker.js +1 -1
- package/dist/workers/lint.worker.js.map +1 -1
- package/dist/workers/server-build.worker.d.ts.map +1 -1
- package/dist/workers/server-build.worker.js +11 -161
- package/dist/workers/server-build.worker.js.map +1 -1
- package/dist/workers/server-runtime.worker.d.ts.map +1 -1
- package/dist/workers/server-runtime.worker.js +15 -0
- package/dist/workers/server-runtime.worker.js.map +1 -1
- package/package.json +9 -7
- package/src/angular/vite-angular-plugin.ts +88 -34
- package/src/angular/vite-postcss-inline-plugin.ts +5 -1
- package/src/capacitor/capacitor-android.ts +368 -0
- package/src/capacitor/capacitor.ts +18 -363
- package/src/commands/check.ts +2 -2
- package/src/commands/device.ts +3 -2
- package/src/commands/lint.ts +1 -201
- package/src/commands/publish.ts +2 -1
- package/src/commands/typecheck.ts +7 -292
- package/src/electron/electron.ts +15 -8
- package/src/orchestrators/DevWatchOrchestrator.ts +18 -0
- package/src/orchestrators/TypecheckOrchestrator.ts +364 -0
- package/src/sd-cli.ts +6 -1
- package/src/utils/lint-core.ts +205 -0
- package/src/utils/lint-utils.ts +1 -1
- package/src/utils/server-production-files.ts +186 -0
- package/src/utils/vite-config.ts +83 -27
- package/src/utils/vite-scope-watch-plugin.ts +6 -1
- package/src/workers/lint.worker.ts +1 -1
- package/src/workers/server-build.worker.ts +10 -185
- package/src/workers/server-runtime.worker.ts +15 -0
- package/tests/angular/linker-disk-cache.spec.ts +31 -25
- package/tests/angular/vite-angular-plugin-hmr-fallback.spec.ts +15 -15
- package/tests/angular/vite-angular-plugin-hmr.spec.ts +9 -9
- package/tests/angular/vite-angular-plugin-legacy-watch.spec.ts +108 -0
- package/tests/angular/vite-angular-plugin-lint.spec.ts +4 -4
- package/tests/angular/vite-angular-plugin-scss-hmr.spec.ts +10 -15
- package/tests/angular/vite-angular-plugin.spec.ts +80 -15
- package/tests/angular/vite-postcss-inline-plugin.spec.ts +10 -0
- package/tests/capacitor/capacitor-android-exports.verify.md +11 -0
- package/tests/capacitor/capacitor-android.spec.ts +219 -0
- package/tests/capacitor/capacitor-build.spec.ts +17 -21
- package/tests/capacitor/capacitor-icon.spec.ts +17 -19
- package/tests/capacitor/capacitor-init.spec.ts +18 -14
- package/tests/capacitor/capacitor-run.spec.ts +10 -24
- package/tests/capacitor/capacitor-workspace.spec.ts +30 -25
- package/tests/commands/check.spec.ts +2 -2
- package/tests/commands/device.spec.ts +12 -7
- package/tests/commands/lint.spec.ts +33 -194
- package/tests/commands/publish-set.verify.md +7 -0
- package/tests/electron/electron-symlink-cleanup.verify.md +8 -0
- package/tests/electron/electron.spec.ts +27 -2
- package/tests/orchestrators/dist-delete-watcher.verify.md +10 -0
- package/tests/orchestrators/typecheck-orchestrator.spec.ts +180 -0
- package/tests/sd-cli-catch-all.verify.md +7 -0
- package/tests/utils/lint-core-import-paths.verify.md +10 -0
- package/tests/utils/lint-core.spec.ts +188 -0
- package/tests/utils/server-production-files-import-paths.verify.md +14 -0
- package/tests/utils/vite-config.spec.ts +255 -133
- package/tests/utils/vite-scope-watch-plugin.spec.ts +22 -0
- package/tests/workers/server-build-context-dispose.verify.md +8 -0
- 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
|
+
}
|
package/src/utils/vite-config.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
308
|
+
rollupOptions: {
|
|
239
309
|
output: {
|
|
240
310
|
inlineDynamicImports: true,
|
|
241
311
|
},
|
|
242
312
|
},
|
|
243
313
|
};
|
|
244
314
|
|
|
245
|
-
//
|
|
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
|
-
|
|
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
|
|
|
@@ -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 {
|
|
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
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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
|
);
|