@simplysm/sd-cli 14.0.98 → 14.0.100

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 (60) hide show
  1. package/dist/commands/init/generators/client.d.ts.map +1 -1
  2. package/dist/commands/init/generators/client.js +2 -1
  3. package/dist/commands/init/generators/client.js.map +1 -1
  4. package/dist/commands/init/init-client.d.ts +5 -0
  5. package/dist/commands/init/init-client.d.ts.map +1 -0
  6. package/dist/commands/init/init-client.js +117 -0
  7. package/dist/commands/init/init-client.js.map +1 -0
  8. package/dist/commands/init/normalize.d.ts.map +1 -1
  9. package/dist/commands/init/normalize.js +3 -1
  10. package/dist/commands/init/normalize.js.map +1 -1
  11. package/dist/commands/init/patch.d.ts +16 -0
  12. package/dist/commands/init/patch.d.ts.map +1 -0
  13. package/dist/commands/init/patch.js +247 -0
  14. package/dist/commands/init/patch.js.map +1 -0
  15. package/dist/commands/init/prompts.d.ts +6 -1
  16. package/dist/commands/init/prompts.d.ts.map +1 -1
  17. package/dist/commands/init/prompts.js +46 -31
  18. package/dist/commands/init/prompts.js.map +1 -1
  19. package/dist/commands/init/recover.d.ts +12 -0
  20. package/dist/commands/init/recover.d.ts.map +1 -0
  21. package/dist/commands/init/recover.js +183 -0
  22. package/dist/commands/init/recover.js.map +1 -0
  23. package/dist/commands/init/ts-ast.d.ts +8 -0
  24. package/dist/commands/init/ts-ast.d.ts.map +1 -0
  25. package/dist/commands/init/ts-ast.js +39 -0
  26. package/dist/commands/init/ts-ast.js.map +1 -0
  27. package/dist/commands/init/types.d.ts +2 -0
  28. package/dist/commands/init/types.d.ts.map +1 -1
  29. package/dist/commands/init/validate.d.ts +3 -1
  30. package/dist/commands/init/validate.d.ts.map +1 -1
  31. package/dist/commands/init/validate.js +15 -0
  32. package/dist/commands/init/validate.js.map +1 -1
  33. package/dist/esbuild/esbuild-client-config.d.ts.map +1 -1
  34. package/dist/esbuild/esbuild-client-config.js +17 -0
  35. package/dist/esbuild/esbuild-client-config.js.map +1 -1
  36. package/dist/sd-cli-entry.d.ts.map +1 -1
  37. package/dist/sd-cli-entry.js +15 -2
  38. package/dist/sd-cli-entry.js.map +1 -1
  39. package/package.json +8 -8
  40. package/src/commands/init/generators/client.ts +6 -1
  41. package/src/commands/init/init-client.ts +155 -0
  42. package/src/commands/init/normalize.ts +3 -1
  43. package/src/commands/init/patch.ts +306 -0
  44. package/src/commands/init/prompts.ts +56 -34
  45. package/src/commands/init/recover.ts +236 -0
  46. package/src/commands/init/templates/client/public/robots.txt.hbs +2 -0
  47. package/src/commands/init/templates/client/src/app/home/master/role-permission/role.list.ts.hbs +12 -14
  48. package/src/commands/init/templates/client/src/app/home/master/user.list.ts.hbs +25 -27
  49. package/src/commands/init/templates/client/src/app/home/system/data-log/data-log.list.ts.hbs +29 -31
  50. package/src/commands/init/templates/client/src/app/home/system/system-log/system-log.list.ts.hbs +46 -48
  51. package/src/commands/init/templates/workspace-root/gitignore +9 -1
  52. package/src/commands/init/ts-ast.ts +48 -0
  53. package/src/commands/init/types.ts +2 -0
  54. package/src/commands/init/validate.ts +24 -1
  55. package/src/esbuild/esbuild-client-config.ts +17 -0
  56. package/src/sd-cli-entry.ts +18 -5
  57. package/tests/init/patch.spec.ts +222 -0
  58. package/tests/init/recover.spec.ts +236 -0
  59. package/tests/init/render.spec.ts +19 -0
  60. package/src/commands/init/templates/client/public/robots.txt +0 -1
@@ -0,0 +1,236 @@
1
+ import path from "path";
2
+ import ts from "typescript";
3
+ import { str } from "@simplysm/core-common";
4
+ import { fsx } from "@simplysm/core-node";
5
+ import { findPackagesObject, getObjectProp, getProp, getPropertyName, getStringProp } from "./ts-ast";
6
+ import type { ClientInputSpec, DbDialect, InitInput } from "./types";
7
+
8
+ /** 기존 워크스페이스 파일에서 복원한 init 입력 + 부가 정보 */
9
+ export interface RecoveredWorkspace {
10
+ /** 기존 워크스페이스를 표현하는 init 입력 (clients = 기존 클라이언트 목록) */
11
+ input: InitInput;
12
+ /** packages/client-common 패키지 실재 여부 */
13
+ hasClientCommonPkg: boolean;
14
+ /** common/src/app-structure.ts 의 실제 export 식별자 목록 (선언 순서) */
15
+ appStructureExportNames: string[];
16
+ }
17
+
18
+ export async function recoverWorkspace(cwd: string): Promise<RecoveredWorkspace> {
19
+ const rootPkg = await readRootPackageJson(cwd);
20
+ const sdConfig = await parseSdConfig(cwd);
21
+
22
+ const clients: ClientInputSpec[] = [];
23
+ for (const c of sdConfig.clients) {
24
+ clients.push({
25
+ name: c.name,
26
+ type: c.isMobile ? "mobile" : "web",
27
+ hasRouter: await fsx.exists(path.resolve(cwd, "packages", c.name, "src/routes.ts")),
28
+ useSsg: c.useSsg,
29
+ });
30
+ }
31
+
32
+ const commonSrc = path.resolve(cwd, "packages/common/src");
33
+ const appStructurePath = path.join(commonSrc, "app-structure.ts");
34
+ const hasAuth = await fsx.exists(appStructurePath);
35
+
36
+ let dbContextName: string | undefined;
37
+ let userEntityName: string | undefined;
38
+ let userEntityLabel: string | undefined;
39
+ let appStructureExportNames: string[] = [];
40
+
41
+ if (sdConfig.hasDb) {
42
+ const dbFolderName = await findDbFolderName(commonSrc);
43
+ dbContextName = str.toCamelCase(dbFolderName.slice("db-".length));
44
+
45
+ if (hasAuth) {
46
+ userEntityName = await findUserEntityName(path.join(commonSrc, dbFolderName));
47
+ }
48
+ }
49
+
50
+ if (hasAuth) {
51
+ const appStructureSource = await fsx.read(appStructurePath);
52
+ const sf = ts.createSourceFile(
53
+ "app-structure.ts",
54
+ appStructureSource,
55
+ ts.ScriptTarget.Latest,
56
+ true,
57
+ );
58
+ appStructureExportNames = findExportedVariableNames(sf);
59
+ if (userEntityName != null) {
60
+ userEntityLabel = findMenuTitleByCode(sf, userEntityName);
61
+ if (userEntityLabel == null) {
62
+ throw new Error(
63
+ `common/src/app-structure.ts 에서 사용자 엔티티("${userEntityName}") 메뉴 정의를 찾을 수 없습니다.`,
64
+ );
65
+ }
66
+ }
67
+ }
68
+
69
+ return {
70
+ input: {
71
+ workspaceName: rootPkg.workspaceName,
72
+ description: rootPkg.description,
73
+ hasServer: sdConfig.hasServer,
74
+ clients,
75
+ hasDb: sdConfig.hasDb,
76
+ dbDialect: sdConfig.dbDialect,
77
+ dbContextName,
78
+ hasAuth,
79
+ userEntityName,
80
+ userEntityLabel,
81
+ mobileAppId: sdConfig.mobileAppId,
82
+ serverPort: sdConfig.serverPort,
83
+ },
84
+ hasClientCommonPkg: await fsx.exists(path.resolve(cwd, "packages/client-common")),
85
+ appStructureExportNames,
86
+ };
87
+ }
88
+
89
+ //-- 루트 package.json
90
+
91
+ async function readRootPackageJson(
92
+ cwd: string,
93
+ ): Promise<{ workspaceName: string; description: string }> {
94
+ const pkgPath = path.resolve(cwd, "package.json");
95
+ if (!(await fsx.exists(pkgPath))) {
96
+ throw new Error(`루트 package.json 을 찾을 수 없습니다: ${pkgPath}`);
97
+ }
98
+ const pkg = JSON.parse(await fsx.read(pkgPath)) as { name?: string; description?: string };
99
+ if (pkg.name == null) {
100
+ throw new Error("루트 package.json 에 name 이 없습니다.");
101
+ }
102
+ return { workspaceName: pkg.name, description: pkg.description ?? "" };
103
+ }
104
+
105
+ //-- sd.config.ts
106
+
107
+ interface SdConfigInfo {
108
+ hasServer: boolean;
109
+ serverPort?: number;
110
+ hasDb: boolean;
111
+ dbDialect?: DbDialect;
112
+ mobileAppId?: string;
113
+ clients: { name: string; isMobile: boolean; useSsg: boolean }[];
114
+ }
115
+
116
+ async function parseSdConfig(cwd: string): Promise<SdConfigInfo> {
117
+ const configPath = path.resolve(cwd, "sd.config.ts");
118
+ if (!(await fsx.exists(configPath))) {
119
+ throw new Error(`sd.config.ts 를 찾을 수 없습니다: ${configPath}`);
120
+ }
121
+ const source = await fsx.read(configPath);
122
+ const sf = ts.createSourceFile("sd.config.ts", source, ts.ScriptTarget.Latest, true);
123
+
124
+ const packagesObj = findPackagesObject(sf);
125
+ if (packagesObj == null) {
126
+ throw new Error("sd.config.ts 에서 packages 정의를 찾을 수 없습니다.");
127
+ }
128
+
129
+ const info: SdConfigInfo = { hasServer: false, hasDb: false, clients: [] };
130
+
131
+ for (const prop of packagesObj.properties) {
132
+ if (!ts.isPropertyAssignment(prop) || !ts.isObjectLiteralExpression(prop.initializer)) {
133
+ continue;
134
+ }
135
+ const pkgName = getPropertyName(prop.name);
136
+ if (pkgName == null) continue;
137
+ const pkgObj = prop.initializer;
138
+ const target = getStringProp(pkgObj, "target");
139
+
140
+ if (target === "server") {
141
+ info.hasServer = true;
142
+
143
+ const envObj = getObjectProp(pkgObj, "env");
144
+ const portStr = envObj != null ? getStringProp(envObj, "SERVER_PORT") : undefined;
145
+ if (portStr != null) info.serverPort = Number(portStr);
146
+
147
+ const configsObj = getObjectProp(pkgObj, "configs");
148
+ const ormObj = configsObj != null ? getObjectProp(configsObj, "orm") : undefined;
149
+ if (ormObj != null) {
150
+ info.hasDb = true;
151
+ const firstConn = ormObj.properties.find(
152
+ (p): p is ts.PropertyAssignment =>
153
+ ts.isPropertyAssignment(p) && ts.isObjectLiteralExpression(p.initializer),
154
+ );
155
+ if (firstConn != null) {
156
+ info.dbDialect = getStringProp(
157
+ firstConn.initializer as ts.ObjectLiteralExpression,
158
+ "dialect",
159
+ ) as DbDialect | undefined;
160
+ }
161
+ }
162
+ } else if (target === "client") {
163
+ const capacitorObj = getObjectProp(pkgObj, "capacitor");
164
+ if (capacitorObj != null && info.mobileAppId == null) {
165
+ info.mobileAppId = getStringProp(capacitorObj, "appId");
166
+ }
167
+ info.clients.push({
168
+ name: pkgName,
169
+ isMobile: capacitorObj != null,
170
+ useSsg: getProp(pkgObj, "prerender") != null,
171
+ });
172
+ }
173
+ }
174
+
175
+ return info;
176
+ }
177
+
178
+ //-- common 패키지 구조
179
+
180
+ async function findDbFolderName(commonSrc: string): Promise<string> {
181
+ if (!(await fsx.exists(commonSrc))) {
182
+ throw new Error(`packages/common/src 를 찾을 수 없습니다: ${commonSrc}`);
183
+ }
184
+ const children = await fsx.readdir(commonSrc);
185
+ const dbFolderName = children.find((c) => c.startsWith("db-"));
186
+ if (dbFolderName == null) {
187
+ throw new Error("packages/common/src 에서 DB context 폴더(db-*)를 찾을 수 없습니다.");
188
+ }
189
+ return dbFolderName;
190
+ }
191
+
192
+ async function findUserEntityName(dbFolder: string): Promise<string> {
193
+ const masterDir = path.join(dbFolder, "tables/master");
194
+ if (!(await fsx.exists(masterDir))) {
195
+ throw new Error(`사용자 엔티티 테이블 폴더를 찾을 수 없습니다: ${masterDir}`);
196
+ }
197
+ const fileNames = await fsx.readdir(masterDir);
198
+ for (const fileName of fileNames) {
199
+ if (!fileName.endsWith(".ts") || fileName.endsWith("-config.ts")) continue;
200
+ const base = fileName.slice(0, -".ts".length);
201
+ if (fileNames.includes(`${base}-config.ts`)) return base;
202
+ }
203
+ throw new Error(
204
+ `사용자 엔티티 테이블 쌍(<엔티티>.ts + <엔티티>-config.ts)을 찾을 수 없습니다: ${masterDir}`,
205
+ );
206
+ }
207
+
208
+ //-- app-structure.ts
209
+
210
+ function findExportedVariableNames(sf: ts.SourceFile): string[] {
211
+ const exportNames: string[] = [];
212
+ for (const stmt of sf.statements) {
213
+ if (!ts.isVariableStatement(stmt)) continue;
214
+ const isExported = stmt.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
215
+ if (isExported !== true) continue;
216
+ for (const decl of stmt.declarationList.declarations) {
217
+ if (ts.isIdentifier(decl.name)) exportNames.push(decl.name.text);
218
+ }
219
+ }
220
+ return exportNames;
221
+ }
222
+
223
+ function findMenuTitleByCode(sf: ts.SourceFile, code: string): string | undefined {
224
+ let title: string | undefined;
225
+ const visit = (node: ts.Node): void => {
226
+ if (title != null) return;
227
+ if (ts.isObjectLiteralExpression(node) && getStringProp(node, "code") === code) {
228
+ title = getStringProp(node, "title");
229
+ if (title != null) return;
230
+ }
231
+ ts.forEachChild(node, visit);
232
+ };
233
+ visit(sf);
234
+ return title;
235
+ }
236
+
@@ -0,0 +1,2 @@
1
+ User-agent: *
2
+ {{client.robotsDirective}}
@@ -86,20 +86,18 @@ const ITEMS_PER_PAGE = 50;
86
86
  (restore)="onRestore($event)"
87
87
  >
88
88
  <ng-template #filterTpl>
89
- <div class="form-box-inline">
90
- <div>
91
- <label>검색어</label>
92
- <sd-textfield
93
- [type]="'text'"
94
- [(value)]="filter().searchText"
95
- (valueChange)="mark(filter)"
96
- />
97
- </div>
98
- <div class="form-box-item">
99
- <sd-checkbox [(value)]="filter().isIncludeDeleted" (valueChange)="mark(filter)">
100
- 삭제항목 포함
101
- </sd-checkbox>
102
- </div>
89
+ <div>
90
+ <label>검색어</label>
91
+ <sd-textfield
92
+ [type]="'text'"
93
+ [(value)]="filter().searchText"
94
+ (valueChange)="mark(filter)"
95
+ />
96
+ </div>
97
+ <div>
98
+ <sd-checkbox [(value)]="filter().isIncludeDeleted" (valueChange)="mark(filter)">
99
+ 삭제항목 포함
100
+ </sd-checkbox>
103
101
  </div>
104
102
  </ng-template>
105
103
 
@@ -87,33 +87,31 @@ const ITEMS_PER_PAGE = 50;
87
87
  (restore)="onRestore($event)"
88
88
  >
89
89
  <ng-template #filterTpl>
90
- <div class="form-box-inline">
91
- <div>
92
- <label>검색어</label>
93
- <sd-textfield
94
- [type]="'text'"
95
- [(value)]="filter().searchText"
96
- (valueChange)="mark(filter)"
97
- />
98
- </div>
99
- <div>
100
- <label>역할</label>
101
- <sd-shared-data-select
102
- [selectMode]="'multi'"
103
- [items]="sharedRoles.items()"
104
- [(value)]="filter().roleIds"
105
- (valueChange)="mark(filter)"
106
- >
107
- <ng-template [itemOf]="sharedRoles.items()" let-item="item">
108
- \{{ item.name }}
109
- </ng-template>
110
- </sd-shared-data-select>
111
- </div>
112
- <div class="form-box-item">
113
- <sd-checkbox [(value)]="filter().isIncludeDeleted" (valueChange)="mark(filter)">
114
- 삭제항목 포함
115
- </sd-checkbox>
116
- </div>
90
+ <div>
91
+ <label>검색어</label>
92
+ <sd-textfield
93
+ [type]="'text'"
94
+ [(value)]="filter().searchText"
95
+ (valueChange)="mark(filter)"
96
+ />
97
+ </div>
98
+ <div>
99
+ <label>역할</label>
100
+ <sd-shared-data-select
101
+ [selectMode]="'multi'"
102
+ [items]="sharedRoles.items()"
103
+ [(value)]="filter().roleIds"
104
+ (valueChange)="mark(filter)"
105
+ >
106
+ <ng-template [itemOf]="sharedRoles.items()" let-item="item">
107
+ \{{ item.name }}
108
+ </ng-template>
109
+ </sd-shared-data-select>
110
+ </div>
111
+ <div>
112
+ <sd-checkbox [(value)]="filter().isIncludeDeleted" (valueChange)="mark(filter)">
113
+ 삭제항목 포함
114
+ </sd-checkbox>
117
115
  </div>
118
116
  </ng-template>
119
117
 
@@ -74,37 +74,35 @@ const ITEMS_PER_PAGE = 50;
74
74
  (filterSubmit)="onFilterSubmit()"
75
75
  >
76
76
  <ng-template #filterTpl>
77
- <div class="form-box-inline">
78
- <div>
79
- <label>기간</label>
80
- <sd-date-range-picker
81
- [(from)]="filter().fromDate"
82
- (fromChange)="mark(filter)"
83
- [(to)]="filter().toDate"
84
- (toChange)="mark(filter)"
85
- />
86
- </div>
87
- <div>
88
- <label>검색어</label>
89
- <sd-textfield
90
- [type]="'text'"
91
- [(value)]="filter().searchText"
92
- (valueChange)="mark(filter)"
93
- />
94
- </div>
95
- <div>
96
- <label>수행자</label>
97
- <sd-shared-data-select
98
- [selectMode]="'multi'"
99
- [items]="shared{{userEntityPascal}}s.items()"
100
- [(value)]="filter().{{userEntityCamel}}Ids"
101
- (valueChange)="mark(filter)"
102
- >
103
- <ng-template [itemOf]="shared{{userEntityPascal}}s.items()" let-item="item">
104
- \{{ item.name }}
105
- </ng-template>
106
- </sd-shared-data-select>
107
- </div>
77
+ <div>
78
+ <label>기간</label>
79
+ <sd-date-range-picker
80
+ [(from)]="filter().fromDate"
81
+ (fromChange)="mark(filter)"
82
+ [(to)]="filter().toDate"
83
+ (toChange)="mark(filter)"
84
+ />
85
+ </div>
86
+ <div>
87
+ <label>검색어</label>
88
+ <sd-textfield
89
+ [type]="'text'"
90
+ [(value)]="filter().searchText"
91
+ (valueChange)="mark(filter)"
92
+ />
93
+ </div>
94
+ <div>
95
+ <label>수행자</label>
96
+ <sd-shared-data-select
97
+ [selectMode]="'multi'"
98
+ [items]="shared{{userEntityPascal}}s.items()"
99
+ [(value)]="filter().{{userEntityCamel}}Ids"
100
+ (valueChange)="mark(filter)"
101
+ >
102
+ <ng-template [itemOf]="shared{{userEntityPascal}}s.items()" let-item="item">
103
+ \{{ item.name }}
104
+ </ng-template>
105
+ </sd-shared-data-select>
108
106
  </div>
109
107
  </ng-template>
110
108
 
@@ -82,54 +82,52 @@ type TSeverity = "error" | "warn" | "log";
82
82
  (filterSubmit)="onFilterSubmit()"
83
83
  >
84
84
  <ng-template #filterTpl>
85
- <div class="form-box-inline">
86
- <div>
87
- <label>기간</label>
88
- <sd-date-range-picker
89
- [(from)]="filter().fromDate"
90
- (fromChange)="mark(filter)"
91
- [(to)]="filter().toDate"
92
- (toChange)="mark(filter)"
93
- />
94
- </div>
95
- <div>
96
- <label>심각도</label>
97
- <sd-select [(value)]="filter().severity" (valueChange)="mark(filter)">
98
- <sd-select-item [value]="undefined">
99
- <span class="tx-theme-gray-default">전체</span>
100
- </sd-select-item>
101
- <sd-select-item [value]="'error'">
102
- <sd-label [theme]="'danger'">error</sd-label>
103
- </sd-select-item>
104
- <sd-select-item [value]="'warn'">
105
- <sd-label [theme]="'warning'">warn</sd-label>
106
- </sd-select-item>
107
- <sd-select-item [value]="'log'">
108
- <sd-label [theme]="'gray'">log</sd-label>
109
- </sd-select-item>
110
- </sd-select>
111
- </div>
112
- <div>
113
- <label>검색어</label>
114
- <sd-textfield
115
- [type]="'text'"
116
- [(value)]="filter().searchText"
117
- (valueChange)="mark(filter)"
118
- />
119
- </div>
120
- <div>
121
- <label>사용자</label>
122
- <sd-shared-data-select
123
- [selectMode]="'multi'"
124
- [items]="shared{{userEntityPascal}}s.items()"
125
- [(value)]="filter().{{userEntityCamel}}Ids"
126
- (valueChange)="mark(filter)"
127
- >
128
- <ng-template [itemOf]="shared{{userEntityPascal}}s.items()" let-item="item">
129
- \{{ item.name }}
130
- </ng-template>
131
- </sd-shared-data-select>
132
- </div>
85
+ <div>
86
+ <label>기간</label>
87
+ <sd-date-range-picker
88
+ [(from)]="filter().fromDate"
89
+ (fromChange)="mark(filter)"
90
+ [(to)]="filter().toDate"
91
+ (toChange)="mark(filter)"
92
+ />
93
+ </div>
94
+ <div>
95
+ <label>심각도</label>
96
+ <sd-select [(value)]="filter().severity" (valueChange)="mark(filter)">
97
+ <sd-select-item [value]="undefined">
98
+ <span class="tx-theme-gray-default">전체</span>
99
+ </sd-select-item>
100
+ <sd-select-item [value]="'error'">
101
+ <sd-label [theme]="'danger'">error</sd-label>
102
+ </sd-select-item>
103
+ <sd-select-item [value]="'warn'">
104
+ <sd-label [theme]="'warning'">warn</sd-label>
105
+ </sd-select-item>
106
+ <sd-select-item [value]="'log'">
107
+ <sd-label [theme]="'gray'">log</sd-label>
108
+ </sd-select-item>
109
+ </sd-select>
110
+ </div>
111
+ <div>
112
+ <label>검색어</label>
113
+ <sd-textfield
114
+ [type]="'text'"
115
+ [(value)]="filter().searchText"
116
+ (valueChange)="mark(filter)"
117
+ />
118
+ </div>
119
+ <div>
120
+ <label>사용자</label>
121
+ <sd-shared-data-select
122
+ [selectMode]="'multi'"
123
+ [items]="shared{{userEntityPascal}}s.items()"
124
+ [(value)]="filter().{{userEntityCamel}}Ids"
125
+ (valueChange)="mark(filter)"
126
+ >
127
+ <ng-template [itemOf]="shared{{userEntityPascal}}s.items()" let-item="item">
128
+ \{{ item.name }}
129
+ </ng-template>
130
+ </sd-shared-data-select>
133
131
  </div>
134
132
  </ng-template>
135
133
 
@@ -1,4 +1,12 @@
1
- .claude
1
+ .claude/sd-*
2
+ .claude/references/sd-*
3
+ .claude/rules/sd-*
4
+ .claude/scripts/sd_*
5
+ .claude/skills/sd-*
6
+ .claude/skills/playwright-cli
7
+ .claude/skills/angular-developer
8
+ .claude/settings.json
9
+ .claude/simplysm.json
2
10
 
3
11
  .playwright-cli
4
12
  __pycache__
@@ -0,0 +1,48 @@
1
+ import ts from "typescript";
2
+
3
+ /** sd.config.ts 의 `packages` 객체 리터럴 탐색 */
4
+ export function findPackagesObject(sf: ts.SourceFile): ts.ObjectLiteralExpression | undefined {
5
+ let found: ts.ObjectLiteralExpression | undefined;
6
+ const visit = (node: ts.Node): void => {
7
+ if (found != null) return;
8
+ if (
9
+ ts.isPropertyAssignment(node) &&
10
+ getPropertyName(node.name) === "packages" &&
11
+ ts.isObjectLiteralExpression(node.initializer)
12
+ ) {
13
+ found = node.initializer;
14
+ return;
15
+ }
16
+ ts.forEachChild(node, visit);
17
+ };
18
+ visit(sf);
19
+ return found;
20
+ }
21
+
22
+ export function getPropertyName(propName: ts.PropertyName): string | undefined {
23
+ if (ts.isIdentifier(propName) || ts.isStringLiteral(propName)) return propName.text;
24
+ return undefined;
25
+ }
26
+
27
+ export function getProp(
28
+ obj: ts.ObjectLiteralExpression,
29
+ key: string,
30
+ ): ts.Expression | undefined {
31
+ for (const p of obj.properties) {
32
+ if (ts.isPropertyAssignment(p) && getPropertyName(p.name) === key) return p.initializer;
33
+ }
34
+ return undefined;
35
+ }
36
+
37
+ export function getObjectProp(
38
+ obj: ts.ObjectLiteralExpression,
39
+ key: string,
40
+ ): ts.ObjectLiteralExpression | undefined {
41
+ const expr = getProp(obj, key);
42
+ return expr != null && ts.isObjectLiteralExpression(expr) ? expr : undefined;
43
+ }
44
+
45
+ export function getStringProp(obj: ts.ObjectLiteralExpression, key: string): string | undefined {
46
+ const expr = getProp(obj, key);
47
+ return expr != null && ts.isStringLiteral(expr) ? expr.text : undefined;
48
+ }
@@ -32,6 +32,8 @@ export interface ClientSpec {
32
32
  appStructureName: string;
33
33
  needsNgIcons: boolean;
34
34
  useSsg: boolean;
35
+ /** robots.txt 지시문 — SSG(SEO) 클라이언트는 전체 허용, 그 외는 전체 차단 */
36
+ robotsDirective: string;
35
37
  }
36
38
 
37
39
  export interface NormalizedInput {
@@ -1,5 +1,6 @@
1
+ import path from "path";
1
2
  import { fsx } from "@simplysm/core-node";
2
- import type { InitInput } from "./types";
3
+ import type { ClientInputSpec, InitInput } from "./types";
3
4
 
4
5
  const KEBAB_CASE_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
5
6
 
@@ -46,3 +47,25 @@ export function validateInput(input: InitInput): void {
46
47
  clientNames.add(normalized);
47
48
  }
48
49
  }
50
+
51
+ /** init client — 신규 클라이언트가 기존 워크스페이스와 충돌하지 않는지 검증 */
52
+ export async function validateInitClientInput(
53
+ cwd: string,
54
+ newClient: ClientInputSpec,
55
+ existingClients: ClientInputSpec[],
56
+ ): Promise<void> {
57
+ if (!KEBAB_CASE_RE.test(newClient.name)) {
58
+ throw new Error(`client 이름은 영문 kebab-case 여야 합니다. 입력값: "${newClient.name}"`);
59
+ }
60
+
61
+ const toNormalized = (clientName: string): string =>
62
+ clientName.startsWith("client-") ? clientName : `client-${clientName}`;
63
+ const normalized = toNormalized(newClient.name);
64
+ if (existingClients.some((c) => toNormalized(c.name) === normalized)) {
65
+ throw new Error(`이미 존재하는 client 입니다: "${normalized}"`);
66
+ }
67
+
68
+ if (await fsx.exists(path.resolve(cwd, "packages", normalized))) {
69
+ throw new Error(`이미 존재하는 패키지 디렉토리입니다: packages/${normalized}`);
70
+ }
71
+ }
@@ -175,6 +175,23 @@ export async function createClientEsbuildContext(
175
175
  entryNames: isDev ? "[name]" : "[name]-[hash]",
176
176
  chunkNames: "[name]-[hash]",
177
177
  assetNames: isDev ? "[name]" : "[name]-[hash]",
178
+ // CSS/SCSS url() 로 참조하는 폰트·이미지 자산을 outdir로 복사하고 url을 출력 경로로 재작성.
179
+ // esbuild 기본 loader 맵에는 js/ts/css/json/text만 있어, 미설정 시 url(*.woff) 등에서
180
+ // "No loader is configured" 에러가 발생한다.
181
+ loader: {
182
+ ".woff": "file",
183
+ ".woff2": "file",
184
+ ".ttf": "file",
185
+ ".eot": "file",
186
+ ".otf": "file",
187
+ ".svg": "file",
188
+ ".png": "file",
189
+ ".jpg": "file",
190
+ ".jpeg": "file",
191
+ ".gif": "file",
192
+ ".webp": "file",
193
+ ".ico": "file",
194
+ },
178
195
  bundle: true,
179
196
  splitting: options.legacyModule !== true,
180
197
  format: "esm",