@simplysm/sd-cli 14.0.98 → 14.0.99
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/commands/init/generators/client.d.ts.map +1 -1
- package/dist/commands/init/generators/client.js +2 -1
- package/dist/commands/init/generators/client.js.map +1 -1
- package/dist/commands/init/init-client.d.ts +5 -0
- package/dist/commands/init/init-client.d.ts.map +1 -0
- package/dist/commands/init/init-client.js +117 -0
- package/dist/commands/init/init-client.js.map +1 -0
- package/dist/commands/init/normalize.d.ts.map +1 -1
- package/dist/commands/init/normalize.js +3 -1
- package/dist/commands/init/normalize.js.map +1 -1
- package/dist/commands/init/patch.d.ts +16 -0
- package/dist/commands/init/patch.d.ts.map +1 -0
- package/dist/commands/init/patch.js +247 -0
- package/dist/commands/init/patch.js.map +1 -0
- package/dist/commands/init/prompts.d.ts +6 -1
- package/dist/commands/init/prompts.d.ts.map +1 -1
- package/dist/commands/init/prompts.js +46 -31
- package/dist/commands/init/prompts.js.map +1 -1
- package/dist/commands/init/recover.d.ts +12 -0
- package/dist/commands/init/recover.d.ts.map +1 -0
- package/dist/commands/init/recover.js +183 -0
- package/dist/commands/init/recover.js.map +1 -0
- package/dist/commands/init/ts-ast.d.ts +8 -0
- package/dist/commands/init/ts-ast.d.ts.map +1 -0
- package/dist/commands/init/ts-ast.js +39 -0
- package/dist/commands/init/ts-ast.js.map +1 -0
- package/dist/commands/init/types.d.ts +2 -0
- package/dist/commands/init/types.d.ts.map +1 -1
- package/dist/commands/init/validate.d.ts +3 -1
- package/dist/commands/init/validate.d.ts.map +1 -1
- package/dist/commands/init/validate.js +15 -0
- package/dist/commands/init/validate.js.map +1 -1
- package/dist/esbuild/esbuild-client-config.d.ts.map +1 -1
- package/dist/esbuild/esbuild-client-config.js +17 -0
- package/dist/esbuild/esbuild-client-config.js.map +1 -1
- package/dist/sd-cli-entry.d.ts.map +1 -1
- package/dist/sd-cli-entry.js +15 -2
- package/dist/sd-cli-entry.js.map +1 -1
- package/package.json +4 -4
- package/src/commands/init/generators/client.ts +6 -1
- package/src/commands/init/init-client.ts +155 -0
- package/src/commands/init/normalize.ts +3 -1
- package/src/commands/init/patch.ts +306 -0
- package/src/commands/init/prompts.ts +56 -34
- package/src/commands/init/recover.ts +236 -0
- package/src/commands/init/templates/client/public/robots.txt.hbs +2 -0
- package/src/commands/init/templates/client/src/app/home/master/role-permission/role.list.ts.hbs +12 -14
- package/src/commands/init/templates/client/src/app/home/master/user.list.ts.hbs +25 -27
- package/src/commands/init/templates/client/src/app/home/system/data-log/data-log.list.ts.hbs +29 -31
- package/src/commands/init/templates/client/src/app/home/system/system-log/system-log.list.ts.hbs +46 -48
- package/src/commands/init/templates/workspace-root/gitignore +9 -1
- package/src/commands/init/ts-ast.ts +48 -0
- package/src/commands/init/types.ts +2 -0
- package/src/commands/init/validate.ts +24 -1
- package/src/esbuild/esbuild-client-config.ts +17 -0
- package/src/sd-cli-entry.ts +18 -5
- package/tests/init/patch.spec.ts +222 -0
- package/tests/init/recover.spec.ts +236 -0
- package/tests/init/render.spec.ts +19 -0
- package/src/commands/init/templates/client/public/robots.txt +0 -1
package/src/sd-cli-entry.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { runWatch } from "./commands/watch";
|
|
|
10
10
|
import { runDev } from "./commands/dev";
|
|
11
11
|
import { runBuild } from "./commands/build";
|
|
12
12
|
import { runInit } from "./commands/init/init";
|
|
13
|
+
import { runInitClient } from "./commands/init/init-client";
|
|
13
14
|
import { runPublish } from "./commands/publish/publish-command";
|
|
14
15
|
import { runReplaceDeps } from "./commands/replace-deps";
|
|
15
16
|
import path from "path";
|
|
@@ -333,11 +334,23 @@ export function createCliParser(argv: string[]): Argv {
|
|
|
333
334
|
},
|
|
334
335
|
)
|
|
335
336
|
.command(
|
|
336
|
-
"init",
|
|
337
|
-
"Bootstrap a new SI workspace via interactive prompts",
|
|
338
|
-
(cmd) =>
|
|
339
|
-
|
|
340
|
-
|
|
337
|
+
"init [kind]",
|
|
338
|
+
"Bootstrap a new SI workspace via interactive prompts (kind=client: add a client package to an existing workspace)",
|
|
339
|
+
(cmd) =>
|
|
340
|
+
cmd
|
|
341
|
+
.version(false)
|
|
342
|
+
.hide("help")
|
|
343
|
+
.positional("kind", {
|
|
344
|
+
describe: "init 대상 (생략: 새 워크스페이스 부트스트랩)",
|
|
345
|
+
type: "string",
|
|
346
|
+
choices: ["client"] as const,
|
|
347
|
+
}),
|
|
348
|
+
async (args) => {
|
|
349
|
+
if (args.kind === "client") {
|
|
350
|
+
await runInitClient({ cwd: process.cwd() });
|
|
351
|
+
} else {
|
|
352
|
+
await runInit({ cwd: process.cwd() });
|
|
353
|
+
}
|
|
341
354
|
},
|
|
342
355
|
)
|
|
343
356
|
.demandCommand(1, "Please specify a command.")
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { normalize } from "../../src/commands/init/normalize";
|
|
5
|
+
import {
|
|
6
|
+
patchAppStructure,
|
|
7
|
+
patchDevService,
|
|
8
|
+
patchRootPackageJson,
|
|
9
|
+
patchSdConfig,
|
|
10
|
+
patchVitestConfig,
|
|
11
|
+
} from "../../src/commands/init/patch";
|
|
12
|
+
import { renderTemplate } from "../../src/commands/init/render";
|
|
13
|
+
import type { ClientInputSpec, InitInput, RenderData } from "../../src/commands/init/types";
|
|
14
|
+
|
|
15
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const TPL_ROOT = path.resolve(__dirname, "../../src/commands/init/templates");
|
|
17
|
+
|
|
18
|
+
function buildData(input: InitInput): RenderData {
|
|
19
|
+
return { ...normalize(input), jwtSecret: "test-jwt-secret" };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const baseInput: InitInput = {
|
|
23
|
+
workspaceName: "demo",
|
|
24
|
+
description: "Demo Workspace",
|
|
25
|
+
hasServer: true,
|
|
26
|
+
hasDb: true,
|
|
27
|
+
dbDialect: "mysql",
|
|
28
|
+
dbContextName: "main",
|
|
29
|
+
hasAuth: true,
|
|
30
|
+
userEntityName: "user",
|
|
31
|
+
userEntityLabel: "사용자",
|
|
32
|
+
clients: [{ name: "admin", type: "web", hasRouter: true }],
|
|
33
|
+
serverPort: 40080,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** before: 기존 클라이언트만 / after: 신규 클라이언트 포함 — 라운드트립 동등성 기준 데이터 쌍 */
|
|
37
|
+
function buildPair(
|
|
38
|
+
newClient: ClientInputSpec,
|
|
39
|
+
inputOverride?: Partial<InitInput>,
|
|
40
|
+
): { before: RenderData; after: RenderData; client: RenderData["clients"][number] } {
|
|
41
|
+
const input = { ...baseInput, ...inputOverride };
|
|
42
|
+
const before = buildData(input);
|
|
43
|
+
const after = buildData({ ...input, clients: [...input.clients, newClient] });
|
|
44
|
+
const client = after.clients[after.clients.length - 1];
|
|
45
|
+
return { before, after, client };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe("patchSdConfig", () => {
|
|
49
|
+
it("web 클라이언트 추가 — init 동등 결과", async () => {
|
|
50
|
+
const { before, after, client } = buildPair({ name: "portal", type: "web", hasRouter: true });
|
|
51
|
+
const tplPath = path.join(TPL_ROOT, "workspace-root/sd.config.ts.hbs");
|
|
52
|
+
const source = await renderTemplate(tplPath, before);
|
|
53
|
+
const expected = await renderTemplate(tplPath, after);
|
|
54
|
+
|
|
55
|
+
const r = patchSdConfig(source, after, client);
|
|
56
|
+
|
|
57
|
+
expect(r.patched).toBe(expected);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("mobile 클라이언트 추가 (appId 포함) — init 동등 결과", async () => {
|
|
61
|
+
const { before, after, client } = buildPair(
|
|
62
|
+
{ name: "pda", type: "mobile", hasRouter: false },
|
|
63
|
+
{ mobileAppId: "kr.co.demo.app" },
|
|
64
|
+
);
|
|
65
|
+
const tplPath = path.join(TPL_ROOT, "workspace-root/sd.config.ts.hbs");
|
|
66
|
+
const source = await renderTemplate(tplPath, before);
|
|
67
|
+
const expected = await renderTemplate(tplPath, after);
|
|
68
|
+
|
|
69
|
+
const r = patchSdConfig(source, after, client);
|
|
70
|
+
|
|
71
|
+
expect(r.patched).toBe(expected);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("SSG 클라이언트 추가 — prerender 포함", async () => {
|
|
75
|
+
const { before, after, client } = buildPair({
|
|
76
|
+
name: "www",
|
|
77
|
+
type: "web",
|
|
78
|
+
hasRouter: true,
|
|
79
|
+
useSsg: true,
|
|
80
|
+
});
|
|
81
|
+
const tplPath = path.join(TPL_ROOT, "workspace-root/sd.config.ts.hbs");
|
|
82
|
+
const source = await renderTemplate(tplPath, before);
|
|
83
|
+
const expected = await renderTemplate(tplPath, after);
|
|
84
|
+
|
|
85
|
+
const r = patchSdConfig(source, after, client);
|
|
86
|
+
|
|
87
|
+
expect(r.patched).toBe(expected);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("packages 정의를 찾을 수 없으면 패치 실패 + 스니펫 제공", () => {
|
|
91
|
+
const { after, client } = buildPair({ name: "portal", type: "web", hasRouter: true });
|
|
92
|
+
|
|
93
|
+
const r = patchSdConfig("export default {};\n", after, client);
|
|
94
|
+
|
|
95
|
+
expect(r.patched).toBeUndefined();
|
|
96
|
+
expect(r.snippet).toContain('"client-portal"');
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("patchVitestConfig", () => {
|
|
101
|
+
it("클라이언트 project 추가 — init 동등 결과", async () => {
|
|
102
|
+
const { before, after, client } = buildPair({ name: "portal", type: "web", hasRouter: true });
|
|
103
|
+
const tplPath = path.join(TPL_ROOT, "workspace-root/vitest.config.ts.hbs");
|
|
104
|
+
const source = await renderTemplate(tplPath, before);
|
|
105
|
+
const expected = await renderTemplate(tplPath, after);
|
|
106
|
+
|
|
107
|
+
const r = patchVitestConfig(source, client);
|
|
108
|
+
|
|
109
|
+
expect(r.patched).toBe(expected);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("projects 배열을 찾을 수 없으면 패치 실패 + 스니펫 제공", () => {
|
|
113
|
+
const { client } = buildPair({ name: "portal", type: "web", hasRouter: true });
|
|
114
|
+
|
|
115
|
+
const r = patchVitestConfig("export default {};\n", client);
|
|
116
|
+
|
|
117
|
+
expect(r.patched).toBeUndefined();
|
|
118
|
+
expect(r.snippet).toContain("client-portal");
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("patchRootPackageJson", () => {
|
|
123
|
+
it("첫 mobile 클라이언트 — run-device 스크립트 추가, init 동등 결과", async () => {
|
|
124
|
+
const { before, after, client } = buildPair(
|
|
125
|
+
{ name: "pda", type: "mobile", hasRouter: false },
|
|
126
|
+
{ mobileAppId: "kr.co.demo.app" },
|
|
127
|
+
);
|
|
128
|
+
const tplPath = path.join(TPL_ROOT, "workspace-root/package.json.hbs");
|
|
129
|
+
const source = await renderTemplate(tplPath, before);
|
|
130
|
+
const expected = await renderTemplate(tplPath, after);
|
|
131
|
+
|
|
132
|
+
const r = patchRootPackageJson(source, client);
|
|
133
|
+
|
|
134
|
+
expect(r.patched).toBe(expected);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("scripts 앵커를 찾을 수 없으면 패치 실패 + 스니펫 제공", () => {
|
|
138
|
+
const { client } = buildPair(
|
|
139
|
+
{ name: "pda", type: "mobile", hasRouter: false },
|
|
140
|
+
{ mobileAppId: "kr.co.demo.app" },
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const r = patchRootPackageJson("{}", client);
|
|
144
|
+
|
|
145
|
+
expect(r.patched).toBeUndefined();
|
|
146
|
+
expect(r.snippet).toContain("run-device");
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("patchAppStructure", () => {
|
|
151
|
+
it("라우팅 클라이언트 추가 — export 블록 추가, init 동등 결과", async () => {
|
|
152
|
+
const { before, after, client } = buildPair({ name: "portal", type: "web", hasRouter: true });
|
|
153
|
+
const tplPath = path.join(TPL_ROOT, "common/src/app-structure.ts.hbs");
|
|
154
|
+
const source = await renderTemplate(tplPath, before);
|
|
155
|
+
const expected = await renderTemplate(tplPath, after);
|
|
156
|
+
|
|
157
|
+
const r = patchAppStructure(source, after, client);
|
|
158
|
+
|
|
159
|
+
expect(r.patched).toBe(expected);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("patchDevService", () => {
|
|
164
|
+
const tplPath = path.join(TPL_ROOT, "server/src/services/dev.service.ts.hbs");
|
|
165
|
+
|
|
166
|
+
it("1개 → 2개 — import 추가 + 권한 평탄화 인자가 배열 결합으로 교체", async () => {
|
|
167
|
+
const { before, after, client } = buildPair({ name: "portal", type: "web", hasRouter: true });
|
|
168
|
+
const source = await renderTemplate(tplPath, before);
|
|
169
|
+
const expected = await renderTemplate(tplPath, after);
|
|
170
|
+
|
|
171
|
+
const r = patchDevService(source, before.appStructureNames, after, client);
|
|
172
|
+
|
|
173
|
+
expect(r.patched).toBe(expected);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("2개 → 3개 — 기존 배열 결합에 원소 추가", async () => {
|
|
177
|
+
const twoClients: InitInput = {
|
|
178
|
+
...baseInput,
|
|
179
|
+
clients: [
|
|
180
|
+
{ name: "admin", type: "web", hasRouter: true },
|
|
181
|
+
{ name: "portal", type: "web", hasRouter: true },
|
|
182
|
+
],
|
|
183
|
+
};
|
|
184
|
+
const before = buildData(twoClients);
|
|
185
|
+
const after = buildData({
|
|
186
|
+
...twoClients,
|
|
187
|
+
clients: [...twoClients.clients, { name: "shop", type: "web", hasRouter: true }],
|
|
188
|
+
});
|
|
189
|
+
const client = after.clients[after.clients.length - 1];
|
|
190
|
+
const source = await renderTemplate(tplPath, before);
|
|
191
|
+
const expected = await renderTemplate(tplPath, after);
|
|
192
|
+
|
|
193
|
+
const r = patchDevService(source, before.appStructureNames, after, client);
|
|
194
|
+
|
|
195
|
+
expect(r.patched).toBe(expected);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("0개 → 1개 (기본 export 워크스페이스) — import·권한 인자가 신규 단독으로 대체, init 동등 결과", async () => {
|
|
199
|
+
const serverOnly: InitInput = { ...baseInput, clients: [] };
|
|
200
|
+
const before = buildData(serverOnly); // appStructureNames = ["appStructureItems"]
|
|
201
|
+
const after = buildData({
|
|
202
|
+
...serverOnly,
|
|
203
|
+
clients: [{ name: "portal", type: "web", hasRouter: true }],
|
|
204
|
+
});
|
|
205
|
+
const client = after.clients[after.clients.length - 1];
|
|
206
|
+
const source = await renderTemplate(tplPath, before);
|
|
207
|
+
const expected = await renderTemplate(tplPath, after);
|
|
208
|
+
|
|
209
|
+
const r = patchDevService(source, [], after, client, "appStructureItems");
|
|
210
|
+
|
|
211
|
+
expect(r.patched).toBe(expected);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("import 앵커를 찾을 수 없으면 패치 실패 + 스니펫 제공", () => {
|
|
215
|
+
const { before, after, client } = buildPair({ name: "portal", type: "web", hasRouter: true });
|
|
216
|
+
|
|
217
|
+
const r = patchDevService("export const x = 1;\n", before.appStructureNames, after, client);
|
|
218
|
+
|
|
219
|
+
expect(r.patched).toBeUndefined();
|
|
220
|
+
expect(r.snippet).toContain(client.appStructureName);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
+
import { normalize } from "../../src/commands/init/normalize";
|
|
7
|
+
import { recoverWorkspace } from "../../src/commands/init/recover";
|
|
8
|
+
import { renderTemplate } from "../../src/commands/init/render";
|
|
9
|
+
import type { InitInput, RenderData } from "../../src/commands/init/types";
|
|
10
|
+
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const TPL_ROOT = path.resolve(__dirname, "../../src/commands/init/templates");
|
|
13
|
+
|
|
14
|
+
function buildData(input: InitInput): RenderData {
|
|
15
|
+
return { ...normalize(input), jwtSecret: "test-jwt-secret" };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const tmpDirs: string[] = [];
|
|
19
|
+
|
|
20
|
+
async function createTmpDir(): Promise<string> {
|
|
21
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "sd-init-recover-"));
|
|
22
|
+
tmpDirs.push(dir);
|
|
23
|
+
return dir;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
afterEach(async () => {
|
|
27
|
+
for (const dir of tmpDirs.splice(0)) {
|
|
28
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/** init 템플릿 렌더로 recover 가 읽는 파일들만 기존 워크스페이스 형태로 구성 */
|
|
33
|
+
async function buildWorkspace(input: InitInput): Promise<string> {
|
|
34
|
+
const cwd = await createTmpDir();
|
|
35
|
+
const data = buildData(input);
|
|
36
|
+
|
|
37
|
+
await fs.writeFile(
|
|
38
|
+
path.join(cwd, "package.json"),
|
|
39
|
+
await renderTemplate(path.join(TPL_ROOT, "workspace-root/package.json.hbs"), data),
|
|
40
|
+
);
|
|
41
|
+
await fs.writeFile(
|
|
42
|
+
path.join(cwd, "sd.config.ts"),
|
|
43
|
+
await renderTemplate(path.join(TPL_ROOT, "workspace-root/sd.config.ts.hbs"), data),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
if (data.hasAuth) {
|
|
47
|
+
const commonSrc = path.join(cwd, "packages/common/src");
|
|
48
|
+
await fs.mkdir(commonSrc, { recursive: true });
|
|
49
|
+
await fs.writeFile(
|
|
50
|
+
path.join(commonSrc, "app-structure.ts"),
|
|
51
|
+
await renderTemplate(path.join(TPL_ROOT, "common/src/app-structure.ts.hbs"), data),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (data.hasDb) {
|
|
56
|
+
const masterDir = path.join(cwd, "packages/common/src", data.dbFolderName, "tables/master");
|
|
57
|
+
await fs.mkdir(masterDir, { recursive: true });
|
|
58
|
+
if (data.hasAuth) {
|
|
59
|
+
await fs.writeFile(path.join(masterDir, `${data.userEntityKebab}.ts`), "");
|
|
60
|
+
await fs.writeFile(path.join(masterDir, `${data.userEntityKebab}-config.ts`), "");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (data.hasClientCommon) {
|
|
65
|
+
await fs.mkdir(path.join(cwd, "packages/client-common/src"), { recursive: true });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const client of data.clients) {
|
|
69
|
+
const srcDir = path.join(cwd, "packages", client.name, "src");
|
|
70
|
+
await fs.mkdir(srcDir, { recursive: true });
|
|
71
|
+
if (client.hasRouter) {
|
|
72
|
+
await fs.writeFile(path.join(srcDir, "routes.ts"), "");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return cwd;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
describe("recoverWorkspace", () => {
|
|
80
|
+
it("server+DB(mysql)+인증(user/사용자)+web/mobile 클라이언트 — 전체 라운드트립", async () => {
|
|
81
|
+
const cwd = await buildWorkspace({
|
|
82
|
+
workspaceName: "demo",
|
|
83
|
+
description: "Demo Workspace",
|
|
84
|
+
hasServer: true,
|
|
85
|
+
hasDb: true,
|
|
86
|
+
dbDialect: "mysql",
|
|
87
|
+
dbContextName: "main",
|
|
88
|
+
hasAuth: true,
|
|
89
|
+
userEntityName: "user",
|
|
90
|
+
userEntityLabel: "사용자",
|
|
91
|
+
mobileAppId: "kr.co.demo.app",
|
|
92
|
+
clients: [
|
|
93
|
+
{ name: "admin", type: "web", hasRouter: true },
|
|
94
|
+
{ name: "pda", type: "mobile", hasRouter: false },
|
|
95
|
+
],
|
|
96
|
+
serverPort: 40080,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const r = await recoverWorkspace(cwd);
|
|
100
|
+
|
|
101
|
+
expect(r.input.workspaceName).toBe("demo");
|
|
102
|
+
expect(r.input.description).toBe("Demo Workspace");
|
|
103
|
+
expect(r.input.hasServer).toBe(true);
|
|
104
|
+
expect(r.input.hasDb).toBe(true);
|
|
105
|
+
expect(r.input.dbDialect).toBe("mysql");
|
|
106
|
+
expect(r.input.dbContextName).toBe("main");
|
|
107
|
+
expect(r.input.hasAuth).toBe(true);
|
|
108
|
+
expect(r.input.userEntityName).toBe("user");
|
|
109
|
+
expect(r.input.userEntityLabel).toBe("사용자");
|
|
110
|
+
expect(r.input.mobileAppId).toBe("kr.co.demo.app");
|
|
111
|
+
expect(r.input.serverPort).toBe(40080);
|
|
112
|
+
expect(r.input.clients).toEqual([
|
|
113
|
+
{ name: "client-admin", type: "web", hasRouter: true, useSsg: false },
|
|
114
|
+
{ name: "client-pda", type: "mobile", hasRouter: false, useSsg: false },
|
|
115
|
+
]);
|
|
116
|
+
expect(r.hasClientCommonPkg).toBe(true);
|
|
117
|
+
expect(r.appStructureExportNames).toEqual(["adminAppStructureItems"]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("server=N + web 클라이언트 1개 — client-common 부재", async () => {
|
|
121
|
+
const cwd = await buildWorkspace({
|
|
122
|
+
workspaceName: "front-only",
|
|
123
|
+
description: "Front Only",
|
|
124
|
+
hasServer: false,
|
|
125
|
+
hasDb: false,
|
|
126
|
+
clients: [{ name: "admin", type: "web", hasRouter: true }],
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const r = await recoverWorkspace(cwd);
|
|
130
|
+
|
|
131
|
+
expect(r.input.hasServer).toBe(false);
|
|
132
|
+
expect(r.input.hasDb).toBe(false);
|
|
133
|
+
expect(r.input.hasAuth).toBe(false);
|
|
134
|
+
expect(r.input.clients).toEqual([
|
|
135
|
+
{ name: "client-admin", type: "web", hasRouter: true, useSsg: false },
|
|
136
|
+
]);
|
|
137
|
+
expect(r.hasClientCommonPkg).toBe(false);
|
|
138
|
+
expect(r.appStructureExportNames).toEqual([]);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("server=Y DB=N — DB 정보 없음", async () => {
|
|
142
|
+
const cwd = await buildWorkspace({
|
|
143
|
+
workspaceName: "no-db",
|
|
144
|
+
description: "No DB",
|
|
145
|
+
hasServer: true,
|
|
146
|
+
hasDb: false,
|
|
147
|
+
clients: [{ name: "admin", type: "web", hasRouter: true }],
|
|
148
|
+
serverPort: 41000,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const r = await recoverWorkspace(cwd);
|
|
152
|
+
|
|
153
|
+
expect(r.input.hasServer).toBe(true);
|
|
154
|
+
expect(r.input.hasDb).toBe(false);
|
|
155
|
+
expect(r.input.dbDialect).toBeUndefined();
|
|
156
|
+
expect(r.input.hasAuth).toBe(false);
|
|
157
|
+
expect(r.input.serverPort).toBe(41000);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("커스텀 사용자 엔티티(employee/직원) + 커스텀 DB context 복원", async () => {
|
|
161
|
+
const cwd = await buildWorkspace({
|
|
162
|
+
workspaceName: "custom",
|
|
163
|
+
description: "Custom",
|
|
164
|
+
hasServer: true,
|
|
165
|
+
hasDb: true,
|
|
166
|
+
dbDialect: "postgresql",
|
|
167
|
+
dbContextName: "erp",
|
|
168
|
+
hasAuth: true,
|
|
169
|
+
userEntityName: "employee",
|
|
170
|
+
userEntityLabel: "직원",
|
|
171
|
+
clients: [{ name: "admin", type: "web", hasRouter: true }],
|
|
172
|
+
serverPort: 40080,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const r = await recoverWorkspace(cwd);
|
|
176
|
+
|
|
177
|
+
expect(r.input.dbDialect).toBe("postgresql");
|
|
178
|
+
expect(r.input.dbContextName).toBe("erp");
|
|
179
|
+
expect(r.input.userEntityName).toBe("employee");
|
|
180
|
+
expect(r.input.userEntityLabel).toBe("직원");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("SSG 클라이언트 — prerender 로 useSsg 복원", async () => {
|
|
184
|
+
const cwd = await buildWorkspace({
|
|
185
|
+
workspaceName: "ssg",
|
|
186
|
+
description: "SSG",
|
|
187
|
+
hasServer: true,
|
|
188
|
+
hasDb: false,
|
|
189
|
+
clients: [{ name: "www", type: "web", hasRouter: true, useSsg: true }],
|
|
190
|
+
serverPort: 40080,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const r = await recoverWorkspace(cwd);
|
|
194
|
+
|
|
195
|
+
expect(r.input.clients).toEqual([
|
|
196
|
+
{ name: "client-www", type: "web", hasRouter: true, useSsg: true },
|
|
197
|
+
]);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("클라이언트 0개 init 워크스페이스 — appStructure fallback 식별자 복원", async () => {
|
|
201
|
+
const cwd = await buildWorkspace({
|
|
202
|
+
workspaceName: "server-only",
|
|
203
|
+
description: "Server Only",
|
|
204
|
+
hasServer: true,
|
|
205
|
+
hasDb: true,
|
|
206
|
+
dbDialect: "mysql",
|
|
207
|
+
dbContextName: "main",
|
|
208
|
+
hasAuth: true,
|
|
209
|
+
userEntityName: "user",
|
|
210
|
+
userEntityLabel: "사용자",
|
|
211
|
+
clients: [],
|
|
212
|
+
serverPort: 40080,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const r = await recoverWorkspace(cwd);
|
|
216
|
+
|
|
217
|
+
expect(r.input.clients).toEqual([]);
|
|
218
|
+
expect(r.appStructureExportNames).toEqual(["appStructureItems"]);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("sd.config.ts 없음 — 에러", async () => {
|
|
222
|
+
const cwd = await createTmpDir();
|
|
223
|
+
await fs.writeFile(
|
|
224
|
+
path.join(cwd, "package.json"),
|
|
225
|
+
JSON.stringify({ name: "broken", description: "" }),
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
await expect(recoverWorkspace(cwd)).rejects.toThrow();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("루트 package.json 없음 — 에러", async () => {
|
|
232
|
+
const cwd = await createTmpDir();
|
|
233
|
+
|
|
234
|
+
await expect(recoverWorkspace(cwd)).rejects.toThrow();
|
|
235
|
+
});
|
|
236
|
+
});
|
|
@@ -991,6 +991,25 @@ describe("SSG 클라이언트 스캐폴드", () => {
|
|
|
991
991
|
expect(out).not.toContain("provideClientHydration");
|
|
992
992
|
});
|
|
993
993
|
|
|
994
|
+
it("client/public/robots.txt: SSG ON → 전체 허용", async () => {
|
|
995
|
+
const out = await renderTemplate(
|
|
996
|
+
path.join(TPL_ROOT, "client/public/robots.txt.hbs"),
|
|
997
|
+
ssgCtx(true),
|
|
998
|
+
);
|
|
999
|
+
expect(out).toContain("User-agent: *");
|
|
1000
|
+
expect(out).toContain("Allow: /");
|
|
1001
|
+
expect(out).not.toContain("Disallow: /");
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
it("client/public/robots.txt: SSG OFF → 전체 차단", async () => {
|
|
1005
|
+
const out = await renderTemplate(
|
|
1006
|
+
path.join(TPL_ROOT, "client/public/robots.txt.hbs"),
|
|
1007
|
+
ssgCtx(false),
|
|
1008
|
+
);
|
|
1009
|
+
expect(out).toContain("User-agent: *");
|
|
1010
|
+
expect(out).toContain("Disallow: /");
|
|
1011
|
+
});
|
|
1012
|
+
|
|
994
1013
|
it("client/main.server.ts: 서버 부트스트랩 default export + 최소 provider", async () => {
|
|
995
1014
|
const out = await renderTemplate(path.join(TPL_ROOT, "client/src/main.server.ts.hbs"), ssgCtx(true));
|
|
996
1015
|
expect(out).toContain('import { provideServerRendering } from "@angular/platform-server";');
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
User-agent: * Disallow: /
|