@simplysm/sd-cli 14.0.66 → 14.0.69

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 (100) hide show
  1. package/dist/commands/init/generators/client-common.d.ts +3 -0
  2. package/dist/commands/init/generators/client-common.d.ts.map +1 -0
  3. package/dist/commands/init/generators/client-common.js +17 -0
  4. package/dist/commands/init/generators/client-common.js.map +1 -0
  5. package/dist/commands/init/generators/client.d.ts +3 -0
  6. package/dist/commands/init/generators/client.d.ts.map +1 -0
  7. package/dist/commands/init/generators/client.js +20 -0
  8. package/dist/commands/init/generators/client.js.map +1 -0
  9. package/dist/commands/init/generators/common.d.ts +3 -0
  10. package/dist/commands/init/generators/common.d.ts.map +1 -0
  11. package/dist/commands/init/generators/common.js +14 -0
  12. package/dist/commands/init/generators/common.js.map +1 -0
  13. package/dist/commands/init/generators/root.d.ts +3 -0
  14. package/dist/commands/init/generators/root.d.ts.map +1 -0
  15. package/dist/commands/init/generators/root.js +28 -0
  16. package/dist/commands/init/generators/root.js.map +1 -0
  17. package/dist/commands/init/generators/server.d.ts +3 -0
  18. package/dist/commands/init/generators/server.d.ts.map +1 -0
  19. package/dist/commands/init/generators/server.js +12 -0
  20. package/dist/commands/init/generators/server.js.map +1 -0
  21. package/dist/commands/init/init.d.ts +5 -0
  22. package/dist/commands/init/init.d.ts.map +1 -0
  23. package/dist/commands/init/init.js +44 -0
  24. package/dist/commands/init/init.js.map +1 -0
  25. package/dist/commands/init/normalize.d.ts +3 -0
  26. package/dist/commands/init/normalize.d.ts.map +1 -0
  27. package/dist/commands/init/normalize.js +42 -0
  28. package/dist/commands/init/normalize.js.map +1 -0
  29. package/dist/commands/init/prompts.d.ts +3 -0
  30. package/dist/commands/init/prompts.d.ts.map +1 -0
  31. package/dist/commands/init/prompts.js +89 -0
  32. package/dist/commands/init/prompts.js.map +1 -0
  33. package/dist/commands/init/render.d.ts +4 -0
  34. package/dist/commands/init/render.d.ts.map +1 -0
  35. package/dist/commands/init/render.js +20 -0
  36. package/dist/commands/init/render.js.map +1 -0
  37. package/dist/commands/init/template-paths.d.ts +2 -0
  38. package/dist/commands/init/template-paths.d.ts.map +1 -0
  39. package/dist/commands/init/template-paths.js +7 -0
  40. package/dist/commands/init/template-paths.js.map +1 -0
  41. package/dist/commands/init/types.d.ts +47 -0
  42. package/dist/commands/init/types.d.ts.map +1 -0
  43. package/dist/commands/init/types.js +2 -0
  44. package/dist/commands/init/types.js.map +1 -0
  45. package/dist/commands/init/validate.d.ts +4 -0
  46. package/dist/commands/init/validate.d.ts.map +1 -0
  47. package/dist/commands/init/validate.js +31 -0
  48. package/dist/commands/init/validate.js.map +1 -0
  49. package/dist/sd-cli-entry.d.ts.map +1 -1
  50. package/dist/sd-cli-entry.js +14 -1
  51. package/dist/sd-cli-entry.js.map +1 -1
  52. package/package.json +5 -4
  53. package/src/commands/init/generators/client-common.ts +29 -0
  54. package/src/commands/init/generators/client.ts +29 -0
  55. package/src/commands/init/generators/common.ts +22 -0
  56. package/src/commands/init/generators/root.ts +32 -0
  57. package/src/commands/init/generators/server.ts +16 -0
  58. package/src/commands/init/init.ts +54 -0
  59. package/src/commands/init/normalize.ts +46 -0
  60. package/src/commands/init/prompts.ts +101 -0
  61. package/src/commands/init/render.ts +25 -0
  62. package/src/commands/init/template-paths.ts +8 -0
  63. package/src/commands/init/templates/client/ngsw-config.json +27 -0
  64. package/src/commands/init/templates/client/package.json.hbs +29 -0
  65. package/src/commands/init/templates/client/src/AppPage.ts.hbs +18 -0
  66. package/src/commands/init/templates/client/src/index.html.hbs +12 -0
  67. package/src/commands/init/templates/client/src/main.ts.hbs +28 -0
  68. package/src/commands/init/templates/client/src/polyfills.ts +2 -0
  69. package/src/commands/init/templates/client/src/routes.ts +3 -0
  70. package/src/commands/init/templates/client/src/styles.scss +1 -0
  71. package/src/commands/init/templates/client/tsconfig.json +20 -0
  72. package/src/commands/init/templates/client-common/package.json.hbs +20 -0
  73. package/src/commands/init/templates/client-common/src/index.ts.hbs +11 -0
  74. package/src/commands/init/templates/client-common/src/providers/AppOrmProvider.ts.hbs +36 -0
  75. package/src/commands/init/templates/client-common/src/providers/AppServiceProvider.ts +27 -0
  76. package/src/commands/init/templates/client-common/tsconfig.json +20 -0
  77. package/src/commands/init/templates/common/package.json.hbs +10 -0
  78. package/src/commands/init/templates/common/src/MainDbContext.ts +4 -0
  79. package/src/commands/init/templates/common/src/index.ts.hbs +5 -0
  80. package/src/commands/init/templates/common/tsconfig.json +8 -0
  81. package/src/commands/init/templates/server/package.json.hbs +23 -0
  82. package/src/commands/init/templates/server/src/index.ts +1 -0
  83. package/src/commands/init/templates/server/src/main.ts.hbs +25 -0
  84. package/src/commands/init/templates/server/tsconfig.json +8 -0
  85. package/src/commands/init/templates/workspace-root/.prettierignore +1 -0
  86. package/src/commands/init/templates/workspace-root/.prettierrc.yaml +12 -0
  87. package/src/commands/init/templates/workspace-root/eslint.config.ts +4 -0
  88. package/src/commands/init/templates/workspace-root/mise.toml.hbs +7 -0
  89. package/src/commands/init/templates/workspace-root/package.json.hbs +43 -0
  90. package/src/commands/init/templates/workspace-root/pnpm-workspace.yaml +17 -0
  91. package/src/commands/init/templates/workspace-root/sd.config.ts.hbs +63 -0
  92. package/src/commands/init/templates/workspace-root/tsconfig.json.hbs +30 -0
  93. package/src/commands/init/templates/workspace-root/vitest.config.ts.hbs +72 -0
  94. package/src/commands/init/types.ts +51 -0
  95. package/src/commands/init/validate.ts +41 -0
  96. package/src/sd-cli-entry.ts +19 -1
  97. package/tests/init/__snapshots__/render.spec.ts.snap +213 -0
  98. package/tests/init/normalize.spec.ts +114 -0
  99. package/tests/init/render.spec.ts +216 -0
  100. package/tests/init/validate.spec.ts +80 -0
@@ -0,0 +1,213 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`client/src/main.ts.hbs > 라우팅 N (mobile) 1`] = `
4
+ "import { enableProdMode } from "@angular/core";
5
+ import { bootstrapApplication } from "@angular/platform-browser";
6
+ import { provideHttpClient, withFetch } from "@angular/common/http";
7
+ import { provideSdAngular } from "@simplysm/angular";
8
+ import { AppPage } from "./AppPage";
9
+ import "./styles.scss";
10
+
11
+ if (typeof ngDevMode !== "undefined" && !ngDevMode) {
12
+ enableProdMode();
13
+ }
14
+
15
+ bootstrapApplication(AppPage, {
16
+ providers: [
17
+ provideHttpClient(withFetch()),
18
+ provideSdAngular({ clientName: "client-pda" }),
19
+ ],
20
+ }).catch((err: unknown) => {
21
+ console.error(err);
22
+ });
23
+ "
24
+ `;
25
+
26
+ exports[`client/src/main.ts.hbs > 라우팅 Y (web) 1`] = `
27
+ "import { enableProdMode } from "@angular/core";
28
+ import { bootstrapApplication } from "@angular/platform-browser";
29
+ import { provideHttpClient, withFetch } from "@angular/common/http";
30
+ import { provideRouter, withHashLocation } from "@angular/router";
31
+ import { provideSdAngular } from "@simplysm/angular";
32
+ import { AppPage } from "./AppPage";
33
+ import { routes } from "./routes";
34
+ import "./styles.scss";
35
+
36
+ if (typeof ngDevMode !== "undefined" && !ngDevMode) {
37
+ enableProdMode();
38
+ }
39
+
40
+ bootstrapApplication(AppPage, {
41
+ providers: [
42
+ provideHttpClient(withFetch()),
43
+ provideRouter(routes, withHashLocation()),
44
+ provideSdAngular({ clientName: "client-admin" }),
45
+ ],
46
+ }).catch((err: unknown) => {
47
+ console.error(err);
48
+ });
49
+ "
50
+ `;
51
+
52
+ exports[`sd.config.ts.hbs > case-001: server+admin, DB=N 1`] = `
53
+ "import type { SdConfigFn } from "@simplysm/sd-cli";
54
+
55
+ const config: SdConfigFn = () => ({
56
+ packages: {
57
+ "server": {
58
+ target: "server",
59
+ env: {
60
+ SERVER_PORT: "40080",
61
+ },
62
+ configs: {
63
+ auth: {
64
+ jwtSecret: "test-jwt-secret",
65
+ },
66
+ },
67
+ },
68
+ "common": {
69
+ target: "neutral",
70
+ },
71
+ "client-common": {
72
+ target: "browser",
73
+ },
74
+ "client-admin": {
75
+ target: "client",
76
+ server: "server",
77
+ },
78
+ },
79
+ });
80
+
81
+ export default config;
82
+ "
83
+ `;
84
+
85
+ exports[`sd.config.ts.hbs > case-002: server+admin+pda(mobile), DB=mysql 1`] = `
86
+ "import type { SdConfigFn } from "@simplysm/sd-cli";
87
+
88
+ const config: SdConfigFn = () => ({
89
+ packages: {
90
+ "server": {
91
+ target: "server",
92
+ env: {
93
+ SERVER_PORT: "40080",
94
+ },
95
+ configs: {
96
+ orm: {
97
+ MAIN: {
98
+ dialect: "mysql",
99
+ host: "localhost",
100
+ port: 3306,
101
+ username: "root",
102
+ password: "1234",
103
+ database: "DEMO2",
104
+ defaultIsolationLevel: "READ_UNCOMMITTED",
105
+ },
106
+ },
107
+ auth: {
108
+ jwtSecret: "test-jwt-secret",
109
+ },
110
+ },
111
+ },
112
+ "common": {
113
+ target: "neutral",
114
+ },
115
+ "client-common": {
116
+ target: "browser",
117
+ },
118
+ "client-admin": {
119
+ target: "client",
120
+ server: "server",
121
+ },
122
+ "client-pda": {
123
+ target: "client",
124
+ server: "server",
125
+ capacitor: {
126
+ appId: "kr.co.demo2.app",
127
+ appName: "Demo2 Workspace",
128
+ plugins: {},
129
+ icon: "res/icon.png",
130
+ platform: {
131
+ android: {},
132
+ },
133
+ },
134
+ },
135
+ },
136
+ });
137
+
138
+ export default config;
139
+ "
140
+ `;
141
+
142
+ exports[`sd.config.ts.hbs > case-003: server=N, admin only 1`] = `
143
+ "import type { SdConfigFn } from "@simplysm/sd-cli";
144
+
145
+ const config: SdConfigFn = () => ({
146
+ packages: {
147
+ "client-admin": {
148
+ target: "client",
149
+ },
150
+ },
151
+ });
152
+
153
+ export default config;
154
+ "
155
+ `;
156
+
157
+ exports[`server/src/main.ts.hbs > DB=N — OrmService 없음 1`] = `
158
+ "import path from "node:path";
159
+ import { env, num, parseBoolEnv } from "@simplysm/core-common";
160
+ import { setupConsola } from "@simplysm/core-node";
161
+ import { createServiceServer, getConfig } from "@simplysm/service-server";
162
+
163
+ setupConsola();
164
+
165
+ const rootConfig = await getConfig<Record<string, unknown>>(
166
+ path.resolve(import.meta.dirname, ".config.json"),
167
+ );
168
+ const authConfig = rootConfig?.["auth"] as { jwtSecret: string } | undefined;
169
+ if (authConfig?.jwtSecret == null) {
170
+ throw new Error("auth.jwtSecret 설정을 찾을 수 없습니다. .config.json을 확인하세요.");
171
+ }
172
+
173
+ export const server = createServiceServer({
174
+ rootPath: import.meta.dirname,
175
+ services: [],
176
+ port: num.parseInt(env("SERVER_PORT"))!,
177
+ auth: { jwtSecret: authConfig.jwtSecret },
178
+ });
179
+
180
+ if (!parseBoolEnv(env("DEV"))) {
181
+ await server.listen();
182
+ }
183
+ "
184
+ `;
185
+
186
+ exports[`server/src/main.ts.hbs > DB=Y — OrmService 포함 1`] = `
187
+ "import path from "node:path";
188
+ import { env, num, parseBoolEnv } from "@simplysm/core-common";
189
+ import { setupConsola } from "@simplysm/core-node";
190
+ import { createServiceServer, getConfig, OrmService } from "@simplysm/service-server";
191
+
192
+ setupConsola();
193
+
194
+ const rootConfig = await getConfig<Record<string, unknown>>(
195
+ path.resolve(import.meta.dirname, ".config.json"),
196
+ );
197
+ const authConfig = rootConfig?.["auth"] as { jwtSecret: string } | undefined;
198
+ if (authConfig?.jwtSecret == null) {
199
+ throw new Error("auth.jwtSecret 설정을 찾을 수 없습니다. .config.json을 확인하세요.");
200
+ }
201
+
202
+ export const server = createServiceServer({
203
+ rootPath: import.meta.dirname,
204
+ services: [OrmService],
205
+ port: num.parseInt(env("SERVER_PORT"))!,
206
+ auth: { jwtSecret: authConfig.jwtSecret },
207
+ });
208
+
209
+ if (!parseBoolEnv(env("DEV"))) {
210
+ await server.listen();
211
+ }
212
+ "
213
+ `;
@@ -0,0 +1,114 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { normalize } from "../../src/commands/init/normalize";
3
+ import type { InitInput } from "../../src/commands/init/types";
4
+
5
+ const base: InitInput = {
6
+ workspaceName: "demo",
7
+ description: "Demo",
8
+ hasServer: false,
9
+ clients: [],
10
+ hasDb: false,
11
+ };
12
+
13
+ describe("normalize", () => {
14
+ it("client- prefix 없는 이름에 자동 prefix", () => {
15
+ const r = normalize({
16
+ ...base,
17
+ clients: [{ name: "admin", type: "web", hasRouter: true }],
18
+ });
19
+ expect(r.clients[0].name).toBe("client-admin");
20
+ });
21
+
22
+ it("client- prefix 이미 있는 이름은 보존", () => {
23
+ const r = normalize({
24
+ ...base,
25
+ clients: [{ name: "client-admin", type: "web", hasRouter: true }],
26
+ });
27
+ expect(r.clients[0].name).toBe("client-admin");
28
+ });
29
+
30
+ it("server=Y → hasCommon=true", () => {
31
+ const r = normalize({ ...base, hasServer: true, serverPort: 40080 });
32
+ expect(r.hasCommon).toBe(true);
33
+ });
34
+
35
+ it("server=N → hasCommon=false", () => {
36
+ expect(normalize(base).hasCommon).toBe(false);
37
+ });
38
+
39
+ it("hasClientCommon: server=Y 또는 client>=2", () => {
40
+ expect(normalize({ ...base, hasServer: true }).hasClientCommon).toBe(true);
41
+ expect(
42
+ normalize({
43
+ ...base,
44
+ clients: [
45
+ { name: "a", type: "web", hasRouter: true },
46
+ { name: "b", type: "web", hasRouter: true },
47
+ ],
48
+ }).hasClientCommon,
49
+ ).toBe(true);
50
+ expect(
51
+ normalize({
52
+ ...base,
53
+ clients: [{ name: "a", type: "web", hasRouter: true }],
54
+ }).hasClientCommon,
55
+ ).toBe(false);
56
+ });
57
+
58
+ it("mobile client → hasRouter 자동 false, isMobile=true, hasMobile=true", () => {
59
+ const r = normalize({
60
+ ...base,
61
+ clients: [{ name: "pda", type: "mobile", hasRouter: true }],
62
+ });
63
+ expect(r.clients[0].hasRouter).toBe(false);
64
+ expect(r.clients[0].isMobile).toBe(true);
65
+ expect(r.hasMobile).toBe(true);
66
+ });
67
+
68
+ it("workspaceNameUpper: 대문자 + 하이픈→언더스코어", () => {
69
+ expect(normalize({ ...base, workspaceName: "my-cool-app" }).workspaceNameUpper).toBe(
70
+ "MY_COOL_APP",
71
+ );
72
+ });
73
+
74
+ it("DB dialect 별 dbPort 자동 도출", () => {
75
+ expect(
76
+ normalize({ ...base, hasServer: true, hasDb: true, dbDialect: "mysql" }).dbPort,
77
+ ).toBe(3306);
78
+ expect(
79
+ normalize({ ...base, hasServer: true, hasDb: true, dbDialect: "postgres" }).dbPort,
80
+ ).toBe(5432);
81
+ expect(
82
+ normalize({ ...base, hasServer: true, hasDb: true, dbDialect: "mssql" }).dbPort,
83
+ ).toBe(1433);
84
+ });
85
+
86
+ it("dbDialect 별 boolean 플래그", () => {
87
+ const m = normalize({ ...base, hasServer: true, hasDb: true, dbDialect: "mysql" });
88
+ expect(m.isMysql).toBe(true);
89
+ expect(m.isPostgres).toBe(false);
90
+ expect(m.isMssql).toBe(false);
91
+ });
92
+
93
+ it("server=N 인데 DB=Y 입력 시 hasDb 강제 false", () => {
94
+ const r = normalize({ ...base, hasServer: false, hasDb: true, dbDialect: "mysql" });
95
+ expect(r.hasDb).toBe(false);
96
+ expect(r.dbDialect).toBeUndefined();
97
+ });
98
+
99
+ it("mobile client 0개면 mobileAppId 무시", () => {
100
+ const r = normalize({ ...base, mobileAppId: "kr.co.x.y" });
101
+ expect(r.mobileAppId).toBeUndefined();
102
+ });
103
+
104
+ it("firstMobileClientName 도출", () => {
105
+ const r = normalize({
106
+ ...base,
107
+ clients: [
108
+ { name: "admin", type: "web", hasRouter: true },
109
+ { name: "pda", type: "mobile", hasRouter: false },
110
+ ],
111
+ });
112
+ expect(r.firstMobileClientName).toBe("client-pda");
113
+ });
114
+ });
@@ -0,0 +1,216 @@
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 { renderTemplate } from "../../src/commands/init/render";
6
+ import type { InitInput, RenderData } from "../../src/commands/init/types";
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+ const TPL_ROOT = path.resolve(__dirname, "../../src/commands/init/templates");
10
+
11
+ function buildData(input: InitInput): RenderData {
12
+ return { ...normalize(input), jwtSecret: "test-jwt-secret" };
13
+ }
14
+
15
+ describe("sd.config.ts.hbs", () => {
16
+ it("case-001: server+admin, DB=N", async () => {
17
+ const data = buildData({
18
+ workspaceName: "demo",
19
+ description: "Demo Workspace",
20
+ hasServer: true,
21
+ hasDb: false,
22
+ clients: [{ name: "admin", type: "web", hasRouter: true }],
23
+ serverPort: 40080,
24
+ });
25
+ const out = await renderTemplate(
26
+ path.join(TPL_ROOT, "workspace-root/sd.config.ts.hbs"),
27
+ data,
28
+ );
29
+ expect(out).toMatchSnapshot();
30
+ });
31
+
32
+ it("case-002: server+admin+pda(mobile), DB=mysql", async () => {
33
+ const data = buildData({
34
+ workspaceName: "demo2",
35
+ description: "Demo2 Workspace",
36
+ hasServer: true,
37
+ hasDb: true,
38
+ dbDialect: "mysql",
39
+ mobileAppId: "kr.co.demo2.app",
40
+ clients: [
41
+ { name: "admin", type: "web", hasRouter: true },
42
+ { name: "pda", type: "mobile", hasRouter: false },
43
+ ],
44
+ serverPort: 40080,
45
+ });
46
+ const out = await renderTemplate(
47
+ path.join(TPL_ROOT, "workspace-root/sd.config.ts.hbs"),
48
+ data,
49
+ );
50
+ expect(out).toMatchSnapshot();
51
+ });
52
+
53
+ it("case-003: server=N, admin only", async () => {
54
+ const data = buildData({
55
+ workspaceName: "demo3",
56
+ description: "Demo3 Workspace",
57
+ hasServer: false,
58
+ hasDb: false,
59
+ clients: [{ name: "admin", type: "web", hasRouter: true }],
60
+ });
61
+ const out = await renderTemplate(
62
+ path.join(TPL_ROOT, "workspace-root/sd.config.ts.hbs"),
63
+ data,
64
+ );
65
+ expect(out).toMatchSnapshot();
66
+ });
67
+ });
68
+
69
+ describe("server/src/main.ts.hbs", () => {
70
+ it("DB=N — OrmService 없음", async () => {
71
+ const data = buildData({
72
+ workspaceName: "demo",
73
+ description: "Demo",
74
+ hasServer: true,
75
+ hasDb: false,
76
+ clients: [],
77
+ serverPort: 40080,
78
+ });
79
+ const out = await renderTemplate(path.join(TPL_ROOT, "server/src/main.ts.hbs"), data);
80
+ expect(out).toMatchSnapshot();
81
+ expect(out).not.toContain("OrmService");
82
+ });
83
+
84
+ it("DB=Y — OrmService 포함", async () => {
85
+ const data = buildData({
86
+ workspaceName: "demo",
87
+ description: "Demo",
88
+ hasServer: true,
89
+ hasDb: true,
90
+ dbDialect: "mysql",
91
+ clients: [],
92
+ serverPort: 40080,
93
+ });
94
+ const out = await renderTemplate(path.join(TPL_ROOT, "server/src/main.ts.hbs"), data);
95
+ expect(out).toMatchSnapshot();
96
+ expect(out).toContain("OrmService");
97
+ });
98
+ });
99
+
100
+ describe("client/src/main.ts.hbs", () => {
101
+ it("라우팅 Y (web)", async () => {
102
+ const data = buildData({
103
+ workspaceName: "demo",
104
+ description: "Demo",
105
+ hasServer: true,
106
+ hasDb: false,
107
+ clients: [{ name: "admin", type: "web", hasRouter: true }],
108
+ serverPort: 40080,
109
+ });
110
+ const ctx = { ...data, client: data.clients[0] };
111
+ const out = await renderTemplate(path.join(TPL_ROOT, "client/src/main.ts.hbs"), ctx);
112
+ expect(out).toMatchSnapshot();
113
+ expect(out).toContain("provideRouter");
114
+ });
115
+
116
+ it("라우팅 N (mobile)", async () => {
117
+ const data = buildData({
118
+ workspaceName: "demo",
119
+ description: "Demo",
120
+ hasServer: true,
121
+ hasDb: false,
122
+ mobileAppId: "kr.co.demo.app",
123
+ clients: [{ name: "pda", type: "mobile", hasRouter: false }],
124
+ serverPort: 40080,
125
+ });
126
+ const ctx = { ...data, client: data.clients[0] };
127
+ const out = await renderTemplate(path.join(TPL_ROOT, "client/src/main.ts.hbs"), ctx);
128
+ expect(out).toMatchSnapshot();
129
+ expect(out).not.toContain("provideRouter");
130
+ });
131
+ });
132
+
133
+ describe("client/src/AppPage.ts.hbs", () => {
134
+ it("라우팅 Y → router-outlet", async () => {
135
+ const data = buildData({
136
+ workspaceName: "demo",
137
+ description: "Demo",
138
+ hasServer: false,
139
+ hasDb: false,
140
+ clients: [{ name: "admin", type: "web", hasRouter: true }],
141
+ });
142
+ const ctx = { ...data, client: data.clients[0] };
143
+ const out = await renderTemplate(path.join(TPL_ROOT, "client/src/AppPage.ts.hbs"), ctx);
144
+ expect(out).toContain("router-outlet");
145
+ });
146
+
147
+ it("라우팅 N → div", async () => {
148
+ const data = buildData({
149
+ workspaceName: "demo",
150
+ description: "Demo",
151
+ hasServer: false,
152
+ hasDb: false,
153
+ clients: [{ name: "x", type: "web", hasRouter: false }],
154
+ });
155
+ const ctx = { ...data, client: data.clients[0] };
156
+ const out = await renderTemplate(path.join(TPL_ROOT, "client/src/AppPage.ts.hbs"), ctx);
157
+ expect(out).not.toContain("router-outlet");
158
+ expect(out).toContain("<div></div>");
159
+ });
160
+ });
161
+
162
+ describe("client-common/src/providers/AppOrmProvider.ts.hbs", () => {
163
+ it("workspaceNameUpper 가 database 명에 들어감", async () => {
164
+ const data = buildData({
165
+ workspaceName: "demo2",
166
+ description: "Demo2",
167
+ hasServer: true,
168
+ hasDb: true,
169
+ dbDialect: "mysql",
170
+ clients: [],
171
+ serverPort: 40080,
172
+ });
173
+ const out = await renderTemplate(
174
+ path.join(TPL_ROOT, "client-common/src/providers/AppOrmProvider.ts.hbs"),
175
+ data,
176
+ );
177
+ expect(out).toContain('database: "DEMO2"');
178
+ expect(out).toContain('from "@demo2/common"');
179
+ });
180
+ });
181
+
182
+ describe("root/package.json.hbs", () => {
183
+ it("hasMobile=true 시 run-device 스크립트 포함", async () => {
184
+ const data = buildData({
185
+ workspaceName: "demo",
186
+ description: "Demo",
187
+ hasServer: true,
188
+ hasDb: false,
189
+ mobileAppId: "kr.co.demo.app",
190
+ clients: [{ name: "pda", type: "mobile", hasRouter: false }],
191
+ serverPort: 40080,
192
+ });
193
+ const out = await renderTemplate(
194
+ path.join(TPL_ROOT, "workspace-root/package.json.hbs"),
195
+ data,
196
+ );
197
+ expect(out).toContain("run-device");
198
+ expect(out).toContain("client-pda");
199
+ });
200
+
201
+ it("hasMobile=false 시 run-device 스크립트 없음", async () => {
202
+ const data = buildData({
203
+ workspaceName: "demo",
204
+ description: "Demo",
205
+ hasServer: true,
206
+ hasDb: false,
207
+ clients: [{ name: "admin", type: "web", hasRouter: true }],
208
+ serverPort: 40080,
209
+ });
210
+ const out = await renderTemplate(
211
+ path.join(TPL_ROOT, "workspace-root/package.json.hbs"),
212
+ data,
213
+ );
214
+ expect(out).not.toContain("run-device");
215
+ });
216
+ });
@@ -0,0 +1,80 @@
1
+ import { mkdtemp, rm } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { fsx } from "@simplysm/core-node";
5
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
6
+ import { validateBeforePrompt, validateInput } from "../../src/commands/init/validate";
7
+ import type { InitInput } from "../../src/commands/init/types";
8
+
9
+ const baseInput: InitInput = {
10
+ workspaceName: "demo",
11
+ description: "Demo",
12
+ hasServer: true,
13
+ clients: [{ name: "admin", type: "web", hasRouter: true }],
14
+ hasDb: false,
15
+ };
16
+
17
+ describe("validateBeforePrompt", () => {
18
+ let dir: string;
19
+
20
+ beforeEach(async () => {
21
+ dir = await mkdtemp(join(tmpdir(), "sd-init-test-"));
22
+ });
23
+
24
+ afterEach(async () => {
25
+ await rm(dir, { recursive: true, force: true });
26
+ });
27
+
28
+ it("빈 디렉토리는 통과", async () => {
29
+ await expect(validateBeforePrompt(dir)).resolves.toBeUndefined();
30
+ });
31
+
32
+ it("파일이 있으면 거부", async () => {
33
+ await fsx.write(join(dir, "x.txt"), "content");
34
+ await expect(validateBeforePrompt(dir)).rejects.toThrow(/비어있지 않습니다/);
35
+ });
36
+
37
+ it(".git 만 있는 디렉토리는 통과", async () => {
38
+ await fsx.mkdir(join(dir, ".git"));
39
+ await expect(validateBeforePrompt(dir)).resolves.toBeUndefined();
40
+ });
41
+ });
42
+
43
+ describe("validateInput", () => {
44
+ it("server=Y + client>=1 통과", () => {
45
+ expect(() => validateInput(baseInput)).not.toThrow();
46
+ });
47
+
48
+ it("server=N + client=0 거부", () => {
49
+ expect(() => validateInput({ ...baseInput, hasServer: false, clients: [] })).toThrow(
50
+ /server 도 client 도 없는/,
51
+ );
52
+ });
53
+
54
+ it("workspaceName 이 kebab-case 아니면 거부", () => {
55
+ expect(() => validateInput({ ...baseInput, workspaceName: "Demo" })).toThrow();
56
+ expect(() => validateInput({ ...baseInput, workspaceName: "demo_app" })).toThrow();
57
+ expect(() => validateInput({ ...baseInput, workspaceName: "1demo" })).toThrow();
58
+ });
59
+
60
+ it("client 이름 kebab-case 아니면 거부", () => {
61
+ expect(() =>
62
+ validateInput({
63
+ ...baseInput,
64
+ clients: [{ name: "Admin", type: "web", hasRouter: true }],
65
+ }),
66
+ ).toThrow();
67
+ });
68
+
69
+ it("client 이름 중복 (prefix 정규화 후) 거부", () => {
70
+ expect(() =>
71
+ validateInput({
72
+ ...baseInput,
73
+ clients: [
74
+ { name: "admin", type: "web", hasRouter: true },
75
+ { name: "client-admin", type: "web", hasRouter: true },
76
+ ],
77
+ }),
78
+ ).toThrow(/중복/);
79
+ });
80
+ });