@simplysm/sd-cli 13.0.0-beta.46 → 13.0.0-beta.47
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/README.md +3 -3
- package/dist/builders/BaseBuilder.js.map +0 -1
- package/dist/builders/DtsBuilder.js.map +0 -1
- package/dist/builders/LibraryBuilder.js.map +0 -1
- package/dist/builders/index.js.map +0 -1
- package/dist/builders/types.js.map +0 -1
- package/dist/capacitor/capacitor.js.map +0 -1
- package/dist/commands/add-client.js.map +0 -1
- package/dist/commands/add-server.js.map +0 -1
- package/dist/commands/build.js.map +0 -1
- package/dist/commands/dev.js.map +0 -1
- package/dist/commands/device.js.map +0 -1
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/lint.js.map +0 -1
- package/dist/commands/publish.js.map +0 -1
- package/dist/commands/typecheck.js.map +0 -1
- package/dist/commands/watch.js.map +0 -1
- package/dist/electron/electron.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/infra/ResultCollector.js.map +0 -1
- package/dist/infra/SignalHandler.js.map +0 -1
- package/dist/infra/WorkerManager.js.map +0 -1
- package/dist/infra/index.js.map +0 -1
- package/dist/orchestrators/WatchOrchestrator.js.map +0 -1
- package/dist/orchestrators/index.js.map +0 -1
- package/dist/sd-cli.js.map +0 -1
- package/dist/sd-config.types.js.map +0 -1
- package/dist/utils/build-env.js.map +0 -1
- package/dist/utils/config-editor.js.map +0 -1
- package/dist/utils/copy-src.js.map +0 -1
- package/dist/utils/esbuild-config.d.ts +1 -0
- package/dist/utils/esbuild-config.d.ts.map +1 -1
- package/dist/utils/esbuild-config.js +2 -1
- package/dist/utils/esbuild-config.js.map +1 -2
- package/dist/utils/listr-manager.js.map +0 -1
- package/dist/utils/output-utils.js.map +0 -1
- package/dist/utils/package-utils.js.map +0 -1
- package/dist/utils/replace-deps.js.map +0 -1
- package/dist/utils/sd-config.js.map +0 -1
- package/dist/utils/spawn.js.map +0 -1
- package/dist/utils/tailwind-config-deps.js.map +0 -1
- package/dist/utils/template.js.map +0 -1
- package/dist/utils/tsconfig.js.map +0 -1
- package/dist/utils/typecheck-serialization.js.map +0 -1
- package/dist/utils/vite-config.js.map +0 -1
- package/dist/utils/worker-events.js.map +0 -1
- package/dist/workers/client.worker.js.map +0 -1
- package/dist/workers/dts.worker.js.map +0 -1
- package/dist/workers/library.worker.js.map +0 -1
- package/dist/workers/server-runtime.worker.js.map +0 -1
- package/dist/workers/server.worker.js.map +0 -1
- package/package.json +5 -4
- package/src/builders/BaseBuilder.ts +141 -0
- package/src/builders/DtsBuilder.ts +138 -0
- package/src/builders/LibraryBuilder.ts +161 -0
- package/src/builders/index.ts +4 -0
- package/src/builders/types.ts +55 -0
- package/src/capacitor/capacitor.ts +827 -0
- package/src/commands/add-client.ts +135 -0
- package/src/commands/add-server.ts +150 -0
- package/src/commands/build.ts +475 -0
- package/src/commands/dev.ts +602 -0
- package/src/commands/device.ts +151 -0
- package/src/commands/init.ts +104 -0
- package/src/commands/lint.ts +216 -0
- package/src/commands/publish.ts +836 -0
- package/src/commands/typecheck.ts +329 -0
- package/src/commands/watch.ts +38 -0
- package/src/electron/electron.ts +329 -0
- package/src/index.ts +1 -0
- package/src/infra/ResultCollector.ts +81 -0
- package/src/infra/SignalHandler.ts +52 -0
- package/src/infra/WorkerManager.ts +65 -0
- package/src/infra/index.ts +3 -0
- package/src/orchestrators/WatchOrchestrator.ts +211 -0
- package/src/orchestrators/index.ts +1 -0
- package/src/sd-cli.ts +307 -0
- package/src/sd-config.types.ts +271 -0
- package/src/utils/build-env.ts +12 -0
- package/src/utils/config-editor.ts +131 -0
- package/src/utils/copy-src.ts +60 -0
- package/src/utils/esbuild-config.ts +263 -0
- package/src/utils/listr-manager.ts +89 -0
- package/src/utils/output-utils.ts +61 -0
- package/src/utils/package-utils.ts +63 -0
- package/src/utils/replace-deps.ts +163 -0
- package/src/utils/sd-config.ts +44 -0
- package/src/utils/spawn.ts +79 -0
- package/src/utils/tailwind-config-deps.ts +95 -0
- package/src/utils/template.ts +51 -0
- package/src/utils/tsconfig.ts +111 -0
- package/src/utils/typecheck-serialization.ts +82 -0
- package/src/utils/vite-config.ts +184 -0
- package/src/utils/worker-events.ts +102 -0
- package/src/workers/client.worker.ts +236 -0
- package/src/workers/dts.worker.ts +416 -0
- package/src/workers/library.worker.ts +245 -0
- package/src/workers/server-runtime.worker.ts +154 -0
- package/src/workers/server.worker.ts +435 -0
- package/templates/add-client/__CLIENT__/package.json.hbs +1 -1
- package/templates/add-server/__SERVER__/package.json.hbs +2 -2
- package/templates/init/package.json.hbs +3 -3
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { Project, SyntaxKind, type ObjectLiteralExpression } from "ts-morph";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* sd.config.ts에서 packages 객체 리터럴을 찾는다.
|
|
5
|
+
*
|
|
6
|
+
* 구조: const config: SdConfigFn = () => ({ packages: { ... } });
|
|
7
|
+
* -> ArrowFunction -> ParenthesizedExpression -> ObjectLiteral -> "packages" property -> ObjectLiteral
|
|
8
|
+
*/
|
|
9
|
+
function findPackagesObject(configPath: string): {
|
|
10
|
+
project: Project;
|
|
11
|
+
packagesObj: ObjectLiteralExpression;
|
|
12
|
+
} {
|
|
13
|
+
const project = new Project();
|
|
14
|
+
const sourceFile = project.addSourceFileAtPath(configPath);
|
|
15
|
+
|
|
16
|
+
// "config" 변수 선언 찾기
|
|
17
|
+
const configVar = sourceFile.getVariableDeclarationOrThrow("config");
|
|
18
|
+
const arrowFn = configVar.getInitializerIfKindOrThrow(SyntaxKind.ArrowFunction);
|
|
19
|
+
|
|
20
|
+
// 화살표 함수 본문에서 반환 객체 찾기
|
|
21
|
+
const body = arrowFn.getBody();
|
|
22
|
+
let returnObj: ObjectLiteralExpression;
|
|
23
|
+
|
|
24
|
+
if (body.isKind(SyntaxKind.ParenthesizedExpression)) {
|
|
25
|
+
returnObj = body.getExpressionIfKindOrThrow(SyntaxKind.ObjectLiteralExpression);
|
|
26
|
+
} else if (body.isKind(SyntaxKind.Block)) {
|
|
27
|
+
const returnStmt = body.getFirstDescendantByKindOrThrow(SyntaxKind.ReturnStatement);
|
|
28
|
+
returnObj = returnStmt.getExpressionIfKindOrThrow(SyntaxKind.ObjectLiteralExpression);
|
|
29
|
+
} else {
|
|
30
|
+
throw new Error("sd.config.ts의 구조를 인식할 수 없습니다.");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// "packages" 프로퍼티 찾기
|
|
34
|
+
const packagesProp = returnObj.getPropertyOrThrow("packages").asKindOrThrow(SyntaxKind.PropertyAssignment);
|
|
35
|
+
const packagesObj = packagesProp.getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression);
|
|
36
|
+
|
|
37
|
+
return { project, packagesObj };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* sd.config.ts의 packages 객체에 새 패키지 항목을 추가한다.
|
|
42
|
+
*
|
|
43
|
+
* @returns true: 성공, false: 이미 존재
|
|
44
|
+
*/
|
|
45
|
+
export function addPackageToSdConfig(
|
|
46
|
+
configPath: string,
|
|
47
|
+
packageName: string,
|
|
48
|
+
config: Record<string, unknown>,
|
|
49
|
+
): boolean {
|
|
50
|
+
const { project, packagesObj } = findPackagesObject(configPath);
|
|
51
|
+
|
|
52
|
+
// 이미 존재하는지 확인 (따옴표 있는 형태와 없는 형태 모두 체크)
|
|
53
|
+
const existing = packagesObj.getProperty(`"${packageName}"`) ?? packagesObj.getProperty(packageName);
|
|
54
|
+
if (existing) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 새 프로퍼티 추가 -- config 객체를 ts-morph initializer 문자열로 변환
|
|
59
|
+
const configStr = JSON.stringify(config)
|
|
60
|
+
.replace(/"([^"]+)":/g, "$1: ")
|
|
61
|
+
.replace(/"/g, '"');
|
|
62
|
+
|
|
63
|
+
packagesObj.addPropertyAssignment({
|
|
64
|
+
name: `"${packageName}"`,
|
|
65
|
+
initializer: configStr,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
project.saveSync();
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* sd.config.ts에서 특정 클라이언트의 server 필드를 설정한다.
|
|
74
|
+
*/
|
|
75
|
+
export function setClientServerInSdConfig(configPath: string, clientName: string, serverName: string): void {
|
|
76
|
+
const { project, packagesObj } = findPackagesObject(configPath);
|
|
77
|
+
|
|
78
|
+
const clientPropNode = packagesObj.getProperty(`"${clientName}"`) ?? packagesObj.getProperty(clientName);
|
|
79
|
+
if (clientPropNode == null) {
|
|
80
|
+
throw new Error(`클라이언트 "${clientName}"을(를) sd.config.ts에서 찾을 수 없습니다.`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const clientProp = clientPropNode.asKindOrThrow(SyntaxKind.PropertyAssignment);
|
|
84
|
+
const clientObj = clientProp.getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression);
|
|
85
|
+
|
|
86
|
+
// 기존 server 프로퍼티가 있으면 제거
|
|
87
|
+
const serverProp = clientObj.getProperty("server");
|
|
88
|
+
if (serverProp) {
|
|
89
|
+
serverProp.remove();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// server 프로퍼티 추가
|
|
93
|
+
clientObj.addPropertyAssignment({
|
|
94
|
+
name: "server",
|
|
95
|
+
initializer: `"${serverName}"`,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
project.saveSync();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* eslint.config.ts에 tailwindcss 설정 블록을 추가한다.
|
|
103
|
+
*
|
|
104
|
+
* @returns true: 추가됨, false: 이미 존재
|
|
105
|
+
*/
|
|
106
|
+
export function addTailwindToEslintConfig(configPath: string, clientName: string): boolean {
|
|
107
|
+
const project = new Project();
|
|
108
|
+
const sourceFile = project.addSourceFileAtPath(configPath);
|
|
109
|
+
|
|
110
|
+
// default export 배열 찾기
|
|
111
|
+
const defaultExport = sourceFile.getFirstDescendantByKindOrThrow(SyntaxKind.ArrayLiteralExpression);
|
|
112
|
+
|
|
113
|
+
// tailwindcss 설정이 이미 있는지 확인
|
|
114
|
+
const text = defaultExport.getText();
|
|
115
|
+
if (text.includes("tailwindcss")) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 새 설정 객체 추가
|
|
120
|
+
defaultExport.addElement(`{
|
|
121
|
+
files: ["**/*.{ts,tsx}"],
|
|
122
|
+
settings: {
|
|
123
|
+
tailwindcss: {
|
|
124
|
+
config: "packages/${clientName}/tailwind.config.ts",
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
}`);
|
|
128
|
+
|
|
129
|
+
project.saveSync();
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { fsGlob, fsCopy, fsMkdir, fsRm, FsWatcher } from "@simplysm/core-node";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* src/에서 glob 패턴에 매칭되는 파일을 dist/로 복사한다.
|
|
6
|
+
* 상대 경로가 유지된다: src/a/b.css → dist/a/b.css
|
|
7
|
+
*
|
|
8
|
+
* @param pkgDir 패키지 루트 디렉토리
|
|
9
|
+
* @param patterns glob 패턴 배열 (src/ 기준 상대 경로)
|
|
10
|
+
*/
|
|
11
|
+
export async function copySrcFiles(pkgDir: string, patterns: string[]): Promise<void> {
|
|
12
|
+
const srcDir = path.join(pkgDir, "src");
|
|
13
|
+
const distDir = path.join(pkgDir, "dist");
|
|
14
|
+
|
|
15
|
+
for (const pattern of patterns) {
|
|
16
|
+
const files = await fsGlob(pattern, { cwd: srcDir, absolute: true });
|
|
17
|
+
for (const file of files) {
|
|
18
|
+
const relativePath = path.relative(srcDir, file);
|
|
19
|
+
const distPath = path.join(distDir, relativePath);
|
|
20
|
+
await fsMkdir(path.dirname(distPath));
|
|
21
|
+
await fsCopy(file, distPath);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* src/에서 glob 패턴에 매칭되는 파일을 감시하여 dist/로 복사한다.
|
|
28
|
+
* 초기 복사 후 변경/추가/삭제를 자동 반영한다.
|
|
29
|
+
*
|
|
30
|
+
* @param pkgDir 패키지 루트 디렉토리
|
|
31
|
+
* @param patterns glob 패턴 배열 (src/ 기준 상대 경로)
|
|
32
|
+
* @returns FsWatcher 인스턴스 (shutdown 시 close() 호출 필요)
|
|
33
|
+
*/
|
|
34
|
+
export async function watchCopySrcFiles(pkgDir: string, patterns: string[]): Promise<FsWatcher> {
|
|
35
|
+
const srcDir = path.join(pkgDir, "src");
|
|
36
|
+
const distDir = path.join(pkgDir, "dist");
|
|
37
|
+
|
|
38
|
+
// 초기 복사
|
|
39
|
+
await copySrcFiles(pkgDir, patterns);
|
|
40
|
+
|
|
41
|
+
// watch 시작
|
|
42
|
+
const watchPaths = patterns.map((p) => path.join(srcDir, p));
|
|
43
|
+
const watcher = await FsWatcher.watch(watchPaths);
|
|
44
|
+
|
|
45
|
+
watcher.onChange({ delay: 300 }, async (changes) => {
|
|
46
|
+
for (const { event, path: filePath } of changes) {
|
|
47
|
+
const relPath = path.relative(srcDir, filePath);
|
|
48
|
+
const distPath = path.join(distDir, relPath);
|
|
49
|
+
|
|
50
|
+
if (event === "unlink") {
|
|
51
|
+
await fsRm(distPath);
|
|
52
|
+
} else if (event === "add" || event === "change") {
|
|
53
|
+
await fsMkdir(path.dirname(distPath));
|
|
54
|
+
await fsCopy(filePath, distPath);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return watcher;
|
|
60
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { readFileSync, existsSync } from "fs";
|
|
3
|
+
import fs from "fs/promises";
|
|
4
|
+
import { createRequire } from "module";
|
|
5
|
+
import { glob } from "glob";
|
|
6
|
+
import type esbuild from "esbuild";
|
|
7
|
+
import { solidPlugin } from "esbuild-plugin-solid";
|
|
8
|
+
import type { TypecheckEnv } from "./tsconfig";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* ESM 상대 import 경로에 .js 확장자를 추가하는 esbuild 플러그인.
|
|
12
|
+
*
|
|
13
|
+
* bundle: false 모드에서 esbuild는 import 경로를 그대로 유지하므로,
|
|
14
|
+
* Node.js ESM에서 직접 실행 시 확장자 누락으로 모듈을 찾지 못하는 문제를 해결한다.
|
|
15
|
+
*/
|
|
16
|
+
function esmRelativeImportPlugin(outdir: string): esbuild.Plugin {
|
|
17
|
+
return {
|
|
18
|
+
name: "esm-relative-import",
|
|
19
|
+
setup(build) {
|
|
20
|
+
build.onEnd(async () => {
|
|
21
|
+
const files = await glob("**/*.js", { cwd: outdir });
|
|
22
|
+
|
|
23
|
+
await Promise.all(
|
|
24
|
+
files.map(async (file) => {
|
|
25
|
+
const filePath = path.join(outdir, file);
|
|
26
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
27
|
+
|
|
28
|
+
const rewritten = content.replace(
|
|
29
|
+
/((?:from|import)\s*["'])(\.\.?\/[^"']*?)(["'])/g,
|
|
30
|
+
(_match, prefix: string, importPath: string, suffix: string) => {
|
|
31
|
+
if (/\.(js|mjs|cjs|json|css|wasm|node)$/i.test(importPath)) return _match;
|
|
32
|
+
return `${prefix}${importPath}.js${suffix}`;
|
|
33
|
+
},
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
if (rewritten !== content) {
|
|
37
|
+
await fs.writeFile(filePath, rewritten);
|
|
38
|
+
}
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Library 빌드용 esbuild 옵션
|
|
48
|
+
* - bundle: false (개별 파일 트랜스파일)
|
|
49
|
+
* - platform: target에 따라 node 또는 browser
|
|
50
|
+
*/
|
|
51
|
+
export interface LibraryEsbuildOptions {
|
|
52
|
+
pkgDir: string;
|
|
53
|
+
entryPoints: string[];
|
|
54
|
+
target: "node" | "browser" | "neutral";
|
|
55
|
+
compilerOptions: Record<string, unknown>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Server 빌드용 esbuild 옵션
|
|
60
|
+
* - bundle: true (모든 의존성 포함한 단일 번들)
|
|
61
|
+
*/
|
|
62
|
+
export interface ServerEsbuildOptions {
|
|
63
|
+
pkgDir: string;
|
|
64
|
+
entryPoints: string[];
|
|
65
|
+
compilerOptions: Record<string, unknown>;
|
|
66
|
+
env?: Record<string, string>;
|
|
67
|
+
/** 번들에서 제외할 외부 모듈 */
|
|
68
|
+
external?: string[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* package.json에서 solid-js 의존성 감지
|
|
73
|
+
*/
|
|
74
|
+
function hasSolidDependency(pkgDir: string): boolean {
|
|
75
|
+
const pkgJson = JSON.parse(readFileSync(path.join(pkgDir, "package.json"), "utf-8")) as PkgJson;
|
|
76
|
+
const allDeps = { ...pkgJson.dependencies, ...pkgJson.peerDependencies };
|
|
77
|
+
return "solid-js" in allDeps;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Library용 esbuild 설정 생성
|
|
82
|
+
*
|
|
83
|
+
* node/browser/neutral 타겟의 라이브러리 패키지 빌드에 사용합니다.
|
|
84
|
+
* - bundle: false (개별 파일을 각각 트랜스파일)
|
|
85
|
+
* - platform: target이 node면 node, 그 외는 browser
|
|
86
|
+
* - target: node면 node20, 그 외는 chrome84
|
|
87
|
+
*/
|
|
88
|
+
export function createLibraryEsbuildOptions(options: LibraryEsbuildOptions): esbuild.BuildOptions {
|
|
89
|
+
const plugins: esbuild.Plugin[] = [esmRelativeImportPlugin(path.join(options.pkgDir, "dist"))];
|
|
90
|
+
|
|
91
|
+
if (hasSolidDependency(options.pkgDir)) {
|
|
92
|
+
plugins.unshift(solidPlugin());
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
entryPoints: options.entryPoints,
|
|
97
|
+
outdir: path.join(options.pkgDir, "dist"),
|
|
98
|
+
format: "esm",
|
|
99
|
+
sourcemap: true,
|
|
100
|
+
sourcesContent: false,
|
|
101
|
+
platform: options.target === "node" ? "node" : "browser",
|
|
102
|
+
target: options.target === "node" ? "node20" : "chrome84",
|
|
103
|
+
bundle: false,
|
|
104
|
+
tsconfigRaw: { compilerOptions: options.compilerOptions as esbuild.TsconfigRaw["compilerOptions"] },
|
|
105
|
+
plugins,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Server용 esbuild 설정 생성
|
|
111
|
+
*
|
|
112
|
+
* 서버 패키지 빌드에 사용합니다.
|
|
113
|
+
* - bundle: true (모든 의존성 포함한 단일 번들)
|
|
114
|
+
* - minify: true (코드 보호를 위한 압축)
|
|
115
|
+
* - banner: CJS 패키지의 require() 지원을 위한 createRequire shim
|
|
116
|
+
* - env를 define 옵션으로 치환 (process.env["KEY"] 형태)
|
|
117
|
+
*/
|
|
118
|
+
export function createServerEsbuildOptions(options: ServerEsbuildOptions): esbuild.BuildOptions {
|
|
119
|
+
const define: Record<string, string> = {};
|
|
120
|
+
if (options.env != null) {
|
|
121
|
+
for (const [key, value] of Object.entries(options.env)) {
|
|
122
|
+
define[`process.env["${key}"]`] = JSON.stringify(value);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
entryPoints: options.entryPoints,
|
|
128
|
+
outdir: path.join(options.pkgDir, "dist"),
|
|
129
|
+
format: "esm",
|
|
130
|
+
minify: true,
|
|
131
|
+
platform: "node",
|
|
132
|
+
target: "node20",
|
|
133
|
+
bundle: true,
|
|
134
|
+
banner: {
|
|
135
|
+
js: "import { createRequire } from 'module'; const require = createRequire(import.meta.url);",
|
|
136
|
+
},
|
|
137
|
+
external: options.external,
|
|
138
|
+
define,
|
|
139
|
+
tsconfigRaw: { compilerOptions: options.compilerOptions as esbuild.TsconfigRaw["compilerOptions"] },
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 빌드 타겟에서 TypecheckEnv 추출
|
|
145
|
+
*
|
|
146
|
+
* 빌드용이므로 neutral은 browser로 처리합니다.
|
|
147
|
+
* (neutral 패키지는 Node/브라우저 공용이지만, 빌드 시에는 browser 환경 기준으로 처리)
|
|
148
|
+
*/
|
|
149
|
+
export function getTypecheckEnvFromTarget(target: "node" | "browser" | "neutral"): TypecheckEnv {
|
|
150
|
+
return target === "node" ? "node" : "browser";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
//#region Optional Peer Deps
|
|
154
|
+
|
|
155
|
+
interface PkgJson {
|
|
156
|
+
dependencies?: Record<string, string>;
|
|
157
|
+
peerDependencies?: Record<string, string>;
|
|
158
|
+
peerDependenciesMeta?: Record<string, { optional?: boolean }>;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 의존성 트리에서 미설치 optional peer dep 수집
|
|
163
|
+
*
|
|
164
|
+
* 서버 빌드(bundle: true) 시 설치되지 않은 optional peer dependency를
|
|
165
|
+
* esbuild external로 지정하여 빌드 실패를 방지한다.
|
|
166
|
+
*/
|
|
167
|
+
export function collectUninstalledOptionalPeerDeps(pkgDir: string): string[] {
|
|
168
|
+
const external = new Set<string>();
|
|
169
|
+
const visited = new Set<string>();
|
|
170
|
+
|
|
171
|
+
const pkgJson = JSON.parse(readFileSync(path.join(pkgDir, "package.json"), "utf-8")) as PkgJson;
|
|
172
|
+
for (const dep of Object.keys(pkgJson.dependencies ?? {})) {
|
|
173
|
+
scanOptionalPeerDeps(dep, pkgDir, external, visited);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return [...external];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function scanOptionalPeerDeps(pkgName: string, resolveDir: string, external: Set<string>, visited: Set<string>): void {
|
|
180
|
+
if (visited.has(pkgName)) return;
|
|
181
|
+
visited.add(pkgName);
|
|
182
|
+
|
|
183
|
+
const req = createRequire(path.join(resolveDir, "noop.js"));
|
|
184
|
+
|
|
185
|
+
let pkgJsonPath: string;
|
|
186
|
+
try {
|
|
187
|
+
pkgJsonPath = req.resolve(`${pkgName}/package.json`);
|
|
188
|
+
} catch {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const depDir = path.dirname(pkgJsonPath);
|
|
193
|
+
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf-8")) as PkgJson;
|
|
194
|
+
|
|
195
|
+
if (pkgJson.peerDependenciesMeta != null) {
|
|
196
|
+
const peerDeps = pkgJson.peerDependencies ?? {};
|
|
197
|
+
const depReq = createRequire(path.join(depDir, "noop.js"));
|
|
198
|
+
for (const [name, meta] of Object.entries(pkgJson.peerDependenciesMeta)) {
|
|
199
|
+
if (meta.optional === true && name in peerDeps) {
|
|
200
|
+
try {
|
|
201
|
+
depReq.resolve(name);
|
|
202
|
+
} catch {
|
|
203
|
+
external.add(name);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
for (const dep of Object.keys(pkgJson.dependencies ?? {})) {
|
|
210
|
+
scanOptionalPeerDeps(dep, depDir, external, visited);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
//#endregion
|
|
215
|
+
|
|
216
|
+
//#region Native Module Externals
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* 의존성 중 binding.gyp가 있는 네이티브 모듈 수집
|
|
220
|
+
*
|
|
221
|
+
* node-gyp로 빌드되는 네이티브 모듈은 esbuild가 번들링할 수 없으므로
|
|
222
|
+
* external로 지정해야 한다.
|
|
223
|
+
*/
|
|
224
|
+
export function collectNativeModuleExternals(pkgDir: string): string[] {
|
|
225
|
+
const external = new Set<string>();
|
|
226
|
+
const visited = new Set<string>();
|
|
227
|
+
|
|
228
|
+
const pkgJson = JSON.parse(readFileSync(path.join(pkgDir, "package.json"), "utf-8")) as PkgJson;
|
|
229
|
+
for (const dep of Object.keys(pkgJson.dependencies ?? {})) {
|
|
230
|
+
scanNativeModules(dep, pkgDir, external, visited);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return [...external];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function scanNativeModules(pkgName: string, resolveDir: string, external: Set<string>, visited: Set<string>): void {
|
|
237
|
+
if (visited.has(pkgName)) return;
|
|
238
|
+
visited.add(pkgName);
|
|
239
|
+
|
|
240
|
+
const req = createRequire(path.join(resolveDir, "noop.js"));
|
|
241
|
+
|
|
242
|
+
let pkgJsonPath: string;
|
|
243
|
+
try {
|
|
244
|
+
pkgJsonPath = req.resolve(`${pkgName}/package.json`);
|
|
245
|
+
} catch {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const depDir = path.dirname(pkgJsonPath);
|
|
250
|
+
|
|
251
|
+
// binding.gyp 존재 여부로 네이티브 모듈 감지
|
|
252
|
+
if (existsSync(path.join(depDir, "binding.gyp"))) {
|
|
253
|
+
external.add(pkgName);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// 하위 dependencies도 재귀 탐색
|
|
257
|
+
const depPkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf-8")) as PkgJson;
|
|
258
|
+
for (const dep of Object.keys(depPkgJson.dependencies ?? {})) {
|
|
259
|
+
scanNativeModules(dep, depDir, external, visited);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
//#endregion
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { Listr } from "listr2";
|
|
3
|
+
import type { consola } from "consola";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* RebuildListrManager 이벤트 타입
|
|
7
|
+
*/
|
|
8
|
+
interface RebuildListrManagerEvents {
|
|
9
|
+
batchComplete: [];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 리빌드 시 Listr 실행을 관리하는 클래스
|
|
14
|
+
*
|
|
15
|
+
* 여러 Worker가 동시에 buildStart를 발생시킬 때, 한 번에 하나의 Listr만 실행되도록 보장합니다.
|
|
16
|
+
* 실행 중에 들어온 빌드 요청은 pending에 모아두었다가 현재 배치가 완료되면 다음 배치로 실행합니다.
|
|
17
|
+
*
|
|
18
|
+
* EventEmitter를 확장하여 배치 완료 시 `batchComplete` 이벤트를 발생시킵니다.
|
|
19
|
+
*/
|
|
20
|
+
export class RebuildListrManager extends EventEmitter<RebuildListrManagerEvents> {
|
|
21
|
+
private _isRunning = false;
|
|
22
|
+
private readonly _pendingBuilds = new Map<string, { title: string; promise: Promise<void>; resolver: () => void }>();
|
|
23
|
+
|
|
24
|
+
constructor(private readonly _logger: ReturnType<typeof consola.withTag>) {
|
|
25
|
+
super();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 빌드를 등록하고 resolver 함수를 반환합니다.
|
|
30
|
+
*
|
|
31
|
+
* @param key - 빌드를 식별하는 고유 키 (예: "core-common:build")
|
|
32
|
+
* @param title - Listr에 표시할 제목 (예: "core-common (node)")
|
|
33
|
+
* @returns 워커가 빌드 완료 시 호출할 resolver 함수
|
|
34
|
+
*/
|
|
35
|
+
registerBuild(key: string, title: string): () => void {
|
|
36
|
+
let resolver!: () => void;
|
|
37
|
+
const promise = new Promise<void>((resolve) => {
|
|
38
|
+
resolver = resolve;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
this._pendingBuilds.set(key, { title, promise, resolver });
|
|
42
|
+
|
|
43
|
+
// Listr가 실행 중이 아니면 다음 tick에 배치 실행
|
|
44
|
+
if (!this._isRunning) {
|
|
45
|
+
void Promise.resolve().then(() => void this._runBatch());
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return resolver;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* pending에 있는 빌드들을 모아서 하나의 Listr로 실행합니다.
|
|
53
|
+
* 실행 중에 들어온 새 빌드는 다음 배치로 넘어갑니다.
|
|
54
|
+
*/
|
|
55
|
+
private async _runBatch(): Promise<void> {
|
|
56
|
+
if (this._isRunning || this._pendingBuilds.size === 0) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
this._isRunning = true;
|
|
61
|
+
|
|
62
|
+
// 현재 pending을 스냅샷으로 가져옴
|
|
63
|
+
const batchBuilds = new Map(this._pendingBuilds);
|
|
64
|
+
this._pendingBuilds.clear();
|
|
65
|
+
|
|
66
|
+
// Listr 태스크 생성
|
|
67
|
+
const tasks = Array.from(batchBuilds.entries()).map(([, { title, promise }]) => ({
|
|
68
|
+
title,
|
|
69
|
+
task: () => promise,
|
|
70
|
+
}));
|
|
71
|
+
|
|
72
|
+
const listr = new Listr(tasks, { concurrent: true });
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
await listr.run();
|
|
76
|
+
// 배치 완료 이벤트 발생
|
|
77
|
+
this.emit("batchComplete");
|
|
78
|
+
} catch (err) {
|
|
79
|
+
this._logger.error("listr 실행 중 오류 발생", { error: String(err) });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
this._isRunning = false;
|
|
83
|
+
|
|
84
|
+
// 실행 중 새로 들어온 pending이 있으면 다음 배치 실행
|
|
85
|
+
if (this._pendingBuilds.size > 0) {
|
|
86
|
+
void this._runBatch();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { consola } from "consola";
|
|
2
|
+
import type { BuildResult } from "../infra/ResultCollector";
|
|
3
|
+
import type { PackageResult } from "./package-utils";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* printErrors에서 사용되는 결과 타입
|
|
7
|
+
* PackageResult와 BuildResult 모두 지원
|
|
8
|
+
*/
|
|
9
|
+
type ErrorResult = PackageResult | BuildResult;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 에러만 출력한다.
|
|
13
|
+
* 성공한 빌드는 listr의 체크마크로 이미 표시되므로 별도 출력하지 않음.
|
|
14
|
+
* @param results 패키지별 빌드 결과 상태
|
|
15
|
+
*/
|
|
16
|
+
export function printErrors(results: Map<string, ErrorResult>): void {
|
|
17
|
+
for (const result of results.values()) {
|
|
18
|
+
if (result.status === "error") {
|
|
19
|
+
const typeLabel = result.type === "dts" ? "dts" : result.target;
|
|
20
|
+
const errorLines: string[] = [`${result.name} (${typeLabel})`];
|
|
21
|
+
if (result.message != null && result.message !== "") {
|
|
22
|
+
for (const line of result.message.split("\n")) {
|
|
23
|
+
errorLines.push(` → ${line}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
consola.error(errorLines.join("\n"));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 서버 URL만 출력한다.
|
|
33
|
+
* @param results 패키지별 빌드 결과 상태
|
|
34
|
+
* @param serverClientsMap 서버별 연결된 클라이언트 목록
|
|
35
|
+
*/
|
|
36
|
+
export function printServers(results: Map<string, PackageResult>, serverClientsMap?: Map<string, string[]>): void {
|
|
37
|
+
// 서버 정보 수집
|
|
38
|
+
const servers = [...results.values()].filter((r) => r.status === "server" && r.port != null);
|
|
39
|
+
|
|
40
|
+
// 서버 정보 출력 (있으면 앞에 빈 줄 추가)
|
|
41
|
+
if (servers.length > 0) {
|
|
42
|
+
process.stdout.write("\n");
|
|
43
|
+
for (const server of servers) {
|
|
44
|
+
if (server.target === "server") {
|
|
45
|
+
// 서버에 연결된 클라이언트가 있으면 클라이언트 URL만 출력
|
|
46
|
+
const clients = serverClientsMap?.get(server.name) ?? [];
|
|
47
|
+
if (clients.length > 0) {
|
|
48
|
+
for (const clientName of clients) {
|
|
49
|
+
consola.info(`[server] http://localhost:${server.port}/${clientName}/`);
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
// 연결된 클라이언트가 없으면 서버 루트 URL 출력
|
|
53
|
+
consola.info(`[server] http://localhost:${server.port}/`);
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
// standalone client는 이름 포함해서 출력
|
|
57
|
+
consola.info(`[server] http://localhost:${server.port}/${server.name}/`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { SdPackageConfig } from "../sd-config.types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 패키지명에서 watch scope 목록을 생성한다.
|
|
5
|
+
* - 패키지명의 scope (예: "@myapp/root" → "@myapp")
|
|
6
|
+
* - @simplysm (항상 포함)
|
|
7
|
+
* @param packageName 루트 package.json의 name 필드
|
|
8
|
+
* @returns scope 배열 (중복 제거)
|
|
9
|
+
*/
|
|
10
|
+
export function getWatchScopes(packageName: string): string[] {
|
|
11
|
+
const scopes = new Set(["@simplysm"]);
|
|
12
|
+
const match = packageName.match(/^(@[^/]+)\//);
|
|
13
|
+
if (match != null) {
|
|
14
|
+
scopes.add(match[1]);
|
|
15
|
+
}
|
|
16
|
+
return [...scopes];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 패키지 결과 상태
|
|
21
|
+
*/
|
|
22
|
+
export interface PackageResult {
|
|
23
|
+
name: string;
|
|
24
|
+
target: string;
|
|
25
|
+
type: "build" | "dts" | "server" | "capacitor";
|
|
26
|
+
status: "success" | "error" | "server";
|
|
27
|
+
message?: string;
|
|
28
|
+
port?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 패키지 설정에서 targets 필터링 (scripts 타겟 제외)
|
|
33
|
+
* @param packages 패키지 설정 맵
|
|
34
|
+
* @param targets 필터링할 패키지 이름 목록. 빈 배열이면 scripts를 제외한 모든 패키지 반환
|
|
35
|
+
* @returns 필터링된 패키지 설정 맵
|
|
36
|
+
* @internal 테스트용으로 export
|
|
37
|
+
*/
|
|
38
|
+
export function filterPackagesByTargets(
|
|
39
|
+
packages: Record<string, SdPackageConfig | undefined>,
|
|
40
|
+
targets: string[],
|
|
41
|
+
): Record<string, SdPackageConfig> {
|
|
42
|
+
const result: Record<string, SdPackageConfig> = {};
|
|
43
|
+
|
|
44
|
+
for (const [name, config] of Object.entries(packages)) {
|
|
45
|
+
if (config == null) continue;
|
|
46
|
+
|
|
47
|
+
// scripts 타겟은 watch/dev 대상에서 제외
|
|
48
|
+
if (config.target === "scripts") continue;
|
|
49
|
+
|
|
50
|
+
// targets가 비어있으면 모든 패키지 포함
|
|
51
|
+
if (targets.length === 0) {
|
|
52
|
+
result[name] = config;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// targets에 포함된 패키지만 필터링
|
|
57
|
+
if (targets.includes(name)) {
|
|
58
|
+
result[name] = config;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return result;
|
|
63
|
+
}
|