@simplysm/sd-cli 13.0.98 → 13.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @simplysm/sd-cli
2
2
 
3
- CLI tool -- monorepo build, dev server, publish, and code quality tooling for Simplysm projects.
3
+ Simplysm package - CLI tool. Provides monorepo build, dev server, publish, and code quality tooling for Simplysm projects. Also exports configuration types and a Vite config factory for client packages.
4
4
 
5
5
  ## Installation
6
6
 
@@ -10,128 +10,258 @@ npm install @simplysm/sd-cli
10
10
 
11
11
  ## API Overview
12
12
 
13
- ### Configuration Types
13
+ ### Config Types
14
14
 
15
15
  | API | Type | Description |
16
16
  |-----|------|-------------|
17
- | `BuildTarget` | type | Build target: `"node"`, `"browser"`, `"neutral"` |
18
- | `SdNpmPublishConfig` | interface | npm registry publish configuration |
19
- | `SdPublishConfig` | type | Union of all publish configs (npm, local-directory, storage) |
20
- | `SdLocalDirectoryPublishConfig` | interface | Copy to local directory publish configuration |
21
- | `SdStoragePublishConfig` | interface | FTP/FTPS/SFTP publish configuration |
22
- | `SdPostPublishScriptConfig` | interface | Post-publish script configuration |
17
+ | `SdConfig` | interface | Top-level `sd.config.ts` configuration |
18
+ | `SdConfigFn` | type | Function signature for `sd.config.ts` default export |
19
+ | `SdConfigParams` | interface | Parameters passed to `SdConfigFn` |
20
+ | `SdPackageConfig` | type | Union of all package config types |
23
21
  | `SdBuildPackageConfig` | interface | Package config for node/browser/neutral targets |
24
- | `SdCapacitorSignConfig` | interface | Capacitor Android APK/AAB signing configuration |
25
- | `SdCapacitorPermission` | interface | Capacitor Android permission configuration |
26
- | `SdCapacitorIntentFilter` | interface | Capacitor Android Intent Filter configuration |
27
- | `SdCapacitorAndroidConfig` | interface | Capacitor Android platform configuration |
28
- | `SdCapacitorConfig` | interface | Capacitor configuration (appId, plugins, icon, platform) |
29
- | `SdElectronConfig` | interface | Electron configuration (appId, portable, installer) |
30
- | `SdClientPackageConfig` | interface | Client package configuration (Vite dev server) |
31
- | `SdServerPackageConfig` | interface | Server package configuration (Fastify server) |
32
- | `SdWatchHookConfig` | interface | Watch hook configuration for scripts packages |
33
- | `SdScriptsPackageConfig` | interface | Scripts-only package configuration |
34
- | `SdPackageConfig` | type | Union of all package configs |
35
- | `SdConfig` | interface | Root `sd.config.ts` configuration |
36
- | `SdConfigParams` | interface | Parameters passed to sd.config.ts function |
37
- | `SdConfigFn` | type | Type of the default export function in sd.config.ts |
22
+ | `SdClientPackageConfig` | interface | Client package config (Vite dev server) |
23
+ | `SdServerPackageConfig` | interface | Server package config (Fastify server) |
24
+ | `SdScriptsPackageConfig` | interface | Scripts-only package config |
25
+ | `BuildTarget` | type | Build target type (`"node" \| "browser" \| "neutral"`) |
26
+ | `SdPublishConfig` | type | Union of all publish config types |
27
+ | `SdNpmPublishConfig` | interface | npm registry publish config |
28
+ | `SdLocalDirectoryPublishConfig` | interface | Local directory publish config |
29
+ | `SdStoragePublishConfig` | interface | FTP/FTPS/SFTP publish config |
30
+ | `SdPostPublishScriptConfig` | interface | Post-publish script config |
31
+ | `SdCapacitorConfig` | interface | Capacitor configuration |
32
+ | `SdCapacitorAndroidConfig` | interface | Capacitor Android platform config |
33
+ | `SdCapacitorSignConfig` | interface | Capacitor Android signing config |
34
+ | `SdCapacitorPermission` | interface | Capacitor Android permission config |
35
+ | `SdCapacitorIntentFilter` | interface | Capacitor Android Intent Filter config |
36
+ | `SdElectronConfig` | interface | Electron configuration |
37
+ | `SdWatchHookConfig` | interface | Watch hook config for scripts packages |
38
38
 
39
39
  ### Vite Utilities
40
40
 
41
41
  | API | Type | Description |
42
42
  |-----|------|-------------|
43
- | `ViteConfigOptions` | interface | Options for creating Vite config |
44
- | `createViteConfig` | function | Create Vite config for SolidJS + TailwindCSS client packages |
43
+ | `createViteConfig` | function | Create Vite config for SolidJS + Tailwind client packages |
44
+ | `ViteConfigOptions` | interface | Options for `createViteConfig` |
45
45
 
46
- ## `BuildTarget`
46
+ ---
47
47
 
48
- ```typescript
49
- type BuildTarget = "node" | "browser" | "neutral";
50
- ```
48
+ ### `SdConfig`
51
49
 
52
- ## `SdPublishConfig`
53
-
54
- ```typescript
55
- type SdPublishConfig =
56
- | SdNpmPublishConfig
57
- | SdLocalDirectoryPublishConfig
58
- | SdStoragePublishConfig;
59
- ```
50
+ | Field | Type | Description |
51
+ |-------|------|-------------|
52
+ | `packages` | `Record<string, SdPackageConfig \| undefined>` | Per-package configuration (key: subdirectory name under `packages/`) |
53
+ | `replaceDeps` | `Record<string, string>?` | Dependency replacement config (symlink local sources) |
54
+ | `postPublish` | `SdPostPublishScriptConfig[]?` | Scripts to execute after deployment |
60
55
 
61
- ## `SdPackageConfig`
56
+ ### `SdConfigFn`
62
57
 
63
58
  ```typescript
64
- type SdPackageConfig =
65
- | SdBuildPackageConfig
66
- | SdClientPackageConfig
67
- | SdServerPackageConfig
68
- | SdScriptsPackageConfig;
59
+ type SdConfigFn = (params: SdConfigParams) => SdConfig | Promise<SdConfig>
69
60
  ```
70
61
 
71
- ## `SdConfig`
72
-
73
- ```typescript
74
- interface SdConfig {
75
- packages: Record<string, SdPackageConfig | undefined>;
76
- replaceDeps?: Record<string, string>;
77
- postPublish?: SdPostPublishScriptConfig[];
78
- }
79
- ```
62
+ ### `SdConfigParams`
80
63
 
81
- Root configuration type for `sd.config.ts`. The `packages` field maps package subdirectory names (e.g., `"core-common"`) to their build configuration. The `replaceDeps` field enables local symlink replacement for development.
64
+ | Field | Type | Description |
65
+ |-------|------|-------------|
66
+ | `cwd` | `string` | Current working directory |
67
+ | `dev` | `boolean` | Development mode flag |
68
+ | `options` | `string[]` | Additional options (from CLI `-o` flag) |
82
69
 
83
- ## `SdConfigFn`
70
+ ### `BuildTarget`
84
71
 
85
72
  ```typescript
86
- type SdConfigFn = (params: SdConfigParams) => SdConfig | Promise<SdConfig>;
73
+ type BuildTarget = "node" | "browser" | "neutral"
87
74
  ```
88
75
 
89
- The `sd.config.ts` file must default-export a function of this type.
90
-
91
- ## `SdConfigParams`
76
+ ### `SdPackageConfig`
92
77
 
93
78
  ```typescript
94
- interface SdConfigParams {
95
- cwd: string;
96
- dev: boolean;
97
- options: string[];
98
- }
79
+ type SdPackageConfig =
80
+ | SdBuildPackageConfig
81
+ | SdClientPackageConfig
82
+ | SdServerPackageConfig
83
+ | SdScriptsPackageConfig
99
84
  ```
100
85
 
101
- ## `ViteConfigOptions`
86
+ ### `SdBuildPackageConfig`
87
+
88
+ | Field | Type | Description |
89
+ |-------|------|-------------|
90
+ | `target` | `BuildTarget` | Build target |
91
+ | `publish` | `SdPublishConfig?` | Publish configuration |
92
+ | `copySrc` | `string[]?` | Glob patterns for files to copy from `src/` to `dist/` |
93
+
94
+ ### `SdClientPackageConfig`
95
+
96
+ | Field | Type | Description |
97
+ |-------|------|-------------|
98
+ | `target` | `"client"` | Build target |
99
+ | `server` | `string \| number` | Server package name or Vite port number |
100
+ | `env` | `Record<string, string>?` | Environment variables for build |
101
+ | `publish` | `SdPublishConfig?` | Publish configuration |
102
+ | `capacitor` | `SdCapacitorConfig?` | Capacitor configuration |
103
+ | `electron` | `SdElectronConfig?` | Electron configuration |
104
+ | `configs` | `Record<string, unknown>?` | Runtime config (written to `dist/.config.json`) |
105
+ | `exclude` | `string[]?` | Packages to exclude from Vite optimizeDeps |
106
+
107
+ ### `SdServerPackageConfig`
108
+
109
+ | Field | Type | Description |
110
+ |-------|------|-------------|
111
+ | `target` | `"server"` | Build target |
112
+ | `env` | `Record<string, string>?` | Environment variables for build |
113
+ | `publish` | `SdPublishConfig?` | Publish configuration |
114
+ | `configs` | `Record<string, unknown>?` | Runtime config (written to `dist/.config.json`) |
115
+ | `externals` | `string[]?` | External modules for esbuild |
116
+ | `pm2` | `{ name?: string; ignoreWatchPaths?: string[] }?` | PM2 configuration |
117
+ | `packageManager` | `"volta" \| "mise"?` | Package manager setting |
118
+
119
+ ### `SdScriptsPackageConfig`
120
+
121
+ | Field | Type | Description |
122
+ |-------|------|-------------|
123
+ | `target` | `"scripts"` | Build target |
124
+ | `publish` | `SdPublishConfig?` | Publish configuration |
125
+ | `watch` | `SdWatchHookConfig?` | Watch hook configuration |
126
+
127
+ ### `SdWatchHookConfig`
128
+
129
+ | Field | Type | Description |
130
+ |-------|------|-------------|
131
+ | `target` | `string[]` | Glob patterns to watch (relative to package directory) |
132
+ | `cmd` | `string` | Command to execute on change |
133
+ | `args` | `string[]?` | Command arguments |
134
+
135
+ ### `SdPublishConfig`
102
136
 
103
137
  ```typescript
104
- interface ViteConfigOptions {
105
- pkgDir: string;
106
- name: string;
107
- tsconfigPath: string;
108
- compilerOptions: Record<string, unknown>;
109
- env?: Record<string, string>;
110
- mode: "build" | "dev";
111
- serverPort?: number;
112
- replaceDeps?: string[];
113
- onScopeRebuild?: () => void;
114
- outDir?: string;
115
- base?: string;
116
- }
138
+ type SdPublishConfig = SdNpmPublishConfig | SdLocalDirectoryPublishConfig | SdStoragePublishConfig
117
139
  ```
118
140
 
119
- ## `createViteConfig`
141
+ ### `SdNpmPublishConfig`
142
+
143
+ | Field | Type | Description |
144
+ |-------|------|-------------|
145
+ | `type` | `"npm"` | Publish type |
146
+
147
+ ### `SdLocalDirectoryPublishConfig`
148
+
149
+ | Field | Type | Description |
150
+ |-------|------|-------------|
151
+ | `type` | `"local-directory"` | Publish type |
152
+ | `path` | `string` | Target path (supports `%VER%`, `%PROJECT%` substitution) |
153
+
154
+ ### `SdStoragePublishConfig`
155
+
156
+ | Field | Type | Description |
157
+ |-------|------|-------------|
158
+ | `type` | `"ftp" \| "ftps" \| "sftp"` | Protocol type |
159
+ | `host` | `string` | Server hostname |
160
+ | `port` | `number?` | Server port |
161
+ | `path` | `string?` | Remote path |
162
+ | `user` | `string?` | Username |
163
+ | `password` | `string?` | Password |
164
+
165
+ ### `SdPostPublishScriptConfig`
166
+
167
+ | Field | Type | Description |
168
+ |-------|------|-------------|
169
+ | `type` | `"script"` | Config type |
170
+ | `cmd` | `string` | Command to execute |
171
+ | `args` | `string[]` | Script arguments (supports `%VER%`, `%PROJECT%` substitution) |
172
+
173
+ ### `SdCapacitorConfig`
174
+
175
+ | Field | Type | Description |
176
+ |-------|------|-------------|
177
+ | `appId` | `string` | App ID (e.g., `"com.example.app"`) |
178
+ | `appName` | `string` | App name |
179
+ | `plugins` | `Record<string, Record<string, unknown> \| true>?` | Capacitor plugin configuration |
180
+ | `icon` | `string?` | App icon path (relative to package directory) |
181
+ | `debug` | `boolean?` | Debug build flag |
182
+ | `platform` | `{ android?: SdCapacitorAndroidConfig }?` | Per-platform configuration |
183
+
184
+ ### `SdCapacitorAndroidConfig`
185
+
186
+ | Field | Type | Description |
187
+ |-------|------|-------------|
188
+ | `config` | `Record<string, string>?` | AndroidManifest.xml application tag attributes |
189
+ | `bundle` | `boolean?` | AAB bundle build flag (false for APK) |
190
+ | `intentFilters` | `SdCapacitorIntentFilter[]?` | Intent Filter configuration |
191
+ | `sign` | `SdCapacitorSignConfig?` | APK/AAB signing configuration |
192
+ | `sdkVersion` | `number?` | Android SDK version (minSdk, targetSdk) |
193
+ | `permissions` | `SdCapacitorPermission[]?` | Additional permission configuration |
194
+
195
+ ### `SdCapacitorSignConfig`
196
+
197
+ | Field | Type | Description |
198
+ |-------|------|-------------|
199
+ | `keystore` | `string` | Keystore file path (relative to package directory) |
200
+ | `storePassword` | `string` | Keystore password |
201
+ | `alias` | `string` | Key alias |
202
+ | `password` | `string` | Key password |
203
+ | `keystoreType` | `string?` | Keystore type (default: `"jks"`) |
204
+
205
+ ### `SdCapacitorPermission`
206
+
207
+ | Field | Type | Description |
208
+ |-------|------|-------------|
209
+ | `name` | `string` | Permission name (e.g., `"CAMERA"`) |
210
+ | `maxSdkVersion` | `number?` | Maximum SDK version |
211
+ | `ignore` | `string?` | `tools:ignore` attribute value |
212
+
213
+ ### `SdCapacitorIntentFilter`
214
+
215
+ | Field | Type | Description |
216
+ |-------|------|-------------|
217
+ | `action` | `string?` | Intent action (e.g., `"android.intent.action.VIEW"`) |
218
+ | `category` | `string?` | Intent category (e.g., `"android.intent.category.DEFAULT"`) |
219
+
220
+ ### `SdElectronConfig`
221
+
222
+ | Field | Type | Description |
223
+ |-------|------|-------------|
224
+ | `appId` | `string` | Electron app ID |
225
+ | `portable` | `boolean?` | Portable `.exe` (true) or NSIS installer (false) |
226
+ | `installerIcon` | `string?` | Installer icon path (`.ico`, relative to package directory) |
227
+ | `reinstallDependencies` | `string[]?` | npm packages to include in Electron |
228
+ | `postInstallScript` | `string?` | npm postinstall script |
229
+ | `nsisOptions` | `Record<string, unknown>?` | NSIS options |
230
+ | `env` | `Record<string, string>?` | Environment variables |
231
+
232
+ ### `ViteConfigOptions`
233
+
234
+ | Field | Type | Description |
235
+ |-------|------|-------------|
236
+ | `pkgDir` | `string` | Package directory path |
237
+ | `name` | `string` | Package name |
238
+ | `tsconfigPath` | `string` | tsconfig.json path |
239
+ | `compilerOptions` | `Record<string, unknown>` | TypeScript compiler options |
240
+ | `env` | `Record<string, string>?` | Environment variables |
241
+ | `mode` | `"build" \| "dev"` | Build or dev mode |
242
+ | `serverPort` | `number?` | Server port in dev mode (0 for auto-assign) |
243
+ | `replaceDeps` | `string[]?` | Array of replaceDeps package names |
244
+ | `onScopeRebuild` | `(() => void)?` | Callback when replaceDeps package dist changes |
245
+ | `outDir` | `string?` | Override `build.outDir` |
246
+ | `base` | `string?` | Override base path |
247
+ | `exclude` | `string[]?` | Packages to exclude from optimizeDeps |
248
+
249
+ ### `createViteConfig`
120
250
 
121
251
  ```typescript
122
- function createViteConfig(options: ViteConfigOptions): ViteUserConfig;
252
+ function createViteConfig(options: ViteConfigOptions): ViteUserConfig
123
253
  ```
124
254
 
125
- Create Vite config for SolidJS + TailwindCSS client packages. Includes plugins for tsconfig paths, SolidJS, PWA, scope package watching, Tailwind config dependency tracking, and public-dev directory serving.
255
+ Creates a Vite config for SolidJS + Tailwind CSS client packages. Includes plugins for tsconfig paths, SolidJS, PWA, Tailwind config deps watching, scope package watching, and public-dev directory serving.
126
256
 
127
257
  ## Usage Examples
128
258
 
129
- ### Define sd.config.ts
259
+ ### sd.config.ts
130
260
 
131
261
  ```typescript
132
- import type { SdConfigFn } from "@simplysm/sd-cli";
262
+ import type { SdConfigFn, SdConfigParams } from "@simplysm/sd-cli";
133
263
 
134
- const config: SdConfigFn = (params) => ({
264
+ const config: SdConfigFn = (params: SdConfigParams) => ({
135
265
  packages: {
136
266
  "core-common": { target: "neutral" },
137
267
  "core-node": { target: "node" },
@@ -144,57 +274,22 @@ const config: SdConfigFn = (params) => ({
144
274
  pm2: { name: "my-app" },
145
275
  },
146
276
  },
147
- replaceDeps: {
148
- "@simplysm/*": "../simplysm/packages/*",
149
- },
150
277
  });
151
278
 
152
279
  export default config;
153
280
  ```
154
281
 
155
- ### Create a Vite config for a client package
282
+ ### Custom Vite config
156
283
 
157
284
  ```typescript
158
285
  import { createViteConfig } from "@simplysm/sd-cli";
159
286
 
160
287
  const config = createViteConfig({
161
- pkgDir: "/path/to/my-client",
288
+ pkgDir: "/path/to/package",
162
289
  name: "my-client",
163
- tsconfigPath: "/path/to/my-client/tsconfig.json",
290
+ tsconfigPath: "/path/to/tsconfig.json",
164
291
  compilerOptions: { jsx: "preserve" },
165
292
  mode: "dev",
166
293
  serverPort: 3000,
167
294
  });
168
295
  ```
169
-
170
- ### Configure Capacitor build
171
-
172
- ```typescript
173
- import type { SdConfigFn } from "@simplysm/sd-cli";
174
-
175
- const config: SdConfigFn = (params) => ({
176
- packages: {
177
- "my-app": {
178
- target: "client",
179
- server: "my-server",
180
- capacitor: {
181
- appId: "com.example.myapp",
182
- appName: "My App",
183
- icon: "resources/icon.png",
184
- platform: {
185
- android: {
186
- sign: {
187
- keystore: "keystore.jks",
188
- storePassword: "pass",
189
- alias: "key0",
190
- password: "pass",
191
- },
192
- },
193
- },
194
- },
195
- },
196
- },
197
- });
198
-
199
- export default config;
200
- ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simplysm/sd-cli",
3
- "version": "13.0.98",
3
+ "version": "13.0.99",
4
4
  "description": "Simplysm package - CLI tool",
5
5
  "author": "simplysm",
6
6
  "license": "Apache-2.0",
@@ -14,7 +14,6 @@
14
14
  "types": "./dist/index.d.ts",
15
15
  "files": [
16
16
  "dist",
17
- "docs",
18
17
  "src",
19
18
  "templates",
20
19
  "tests"
@@ -43,9 +42,9 @@
43
42
  "vite-plugin-solid": "^2.11.11",
44
43
  "vite-tsconfig-paths": "^6.1.1",
45
44
  "yargs": "^18.0.0",
46
- "@simplysm/core-common": "13.0.98",
47
- "@simplysm/core-node": "13.0.98",
48
- "@simplysm/storage": "13.0.98"
45
+ "@simplysm/core-common": "13.0.99",
46
+ "@simplysm/core-node": "13.0.99",
47
+ "@simplysm/storage": "13.0.99"
49
48
  },
50
49
  "devDependencies": {
51
50
  "@types/semver": "^7.7.1",
@@ -15,19 +15,18 @@
15
15
  "lint:fix": "sd-cli lint --fix",
16
16
  "check": "sd-cli check",
17
17
  "test": "vitest run",
18
- "test:e2e": "vitest run -c vitest-e2e.config.ts",
19
18
  "postinstall": "playwright-cli install --skills"
20
19
  },
21
20
  "devDependencies": {
22
- "@simplysm/lint": "~13.0.98",
23
- "@simplysm/sd-cli": "~13.0.98",
24
- "@simplysm/sd-claude": "~13.0.98",
21
+ "@simplysm/lint": "~13.0.99",
22
+ "@simplysm/sd-cli": "~13.0.99",
23
+ "@simplysm/sd-claude": "~13.0.99",
25
24
  "@playwright/cli": "^0.1.1",
26
25
  "@types/node": "^20.19.37",
27
26
  "eslint": "^9.39.4",
28
27
  "prettier": "^3.8.1",
29
28
  "typescript": "^5.9.3",
30
29
  "vite-tsconfig-paths": "^6.1.1",
31
- "vitest": "^4.1.0",
30
+ "vitest": "^4.1.0"
32
31
  }
33
32
  }
@@ -6,13 +6,13 @@
6
6
  "private": true,
7
7
  "dependencies": {
8
8
  "@{{projectName}}/db-main": "workspace:*",
9
- "@simplysm/core-browser": "~13.0.98",
10
- "@simplysm/core-common": "~13.0.98",
11
- "@simplysm/excel": "~13.0.98",
12
- "@simplysm/orm-common": "~13.0.98",
13
- "@simplysm/service-client": "~13.0.98",
14
- "@simplysm/service-common": "~13.0.98",
15
- "@simplysm/solid": "~13.0.98",
9
+ "@simplysm/core-browser": "~13.0.99",
10
+ "@simplysm/core-common": "~13.0.99",
11
+ "@simplysm/excel": "~13.0.99",
12
+ "@simplysm/orm-common": "~13.0.99",
13
+ "@simplysm/service-client": "~13.0.99",
14
+ "@simplysm/service-common": "~13.0.99",
15
+ "@simplysm/solid": "~13.0.99",
16
16
  "@solid-primitives/event-listener": "^2.4.5",
17
17
  "@solidjs/router": "^0.15.4",
18
18
  "@tabler/icons-solidjs": "^3.40.0",
@@ -7,7 +7,7 @@
7
7
  ".": "./src/index.ts"
8
8
  },
9
9
  "dependencies": {
10
- "@simplysm/core-common": "~13.0.98",
11
- "@simplysm/orm-common": "~13.0.98"
10
+ "@simplysm/core-common": "~13.0.99",
11
+ "@simplysm/orm-common": "~13.0.99"
12
12
  }
13
13
  }
@@ -5,11 +5,11 @@
5
5
  "private": true,
6
6
  "dependencies": {
7
7
  "@{{projectName}}/db-main": "workspace:*",
8
- "@simplysm/core-common": "~13.0.98",
9
- "@simplysm/excel": "~13.0.98",
10
- "@simplysm/orm-common": "~13.0.98",
11
- "@simplysm/orm-node": "~13.0.98",
12
- "@simplysm/service-server": "~13.0.98",
8
+ "@simplysm/core-common": "~13.0.99",
9
+ "@simplysm/excel": "~13.0.99",
10
+ "@simplysm/orm-common": "~13.0.99",
11
+ "@simplysm/orm-node": "~13.0.99",
12
+ "@simplysm/service-server": "~13.0.99",
13
13
  "bcrypt": "^6.0.0",
14
14
  "pg": "^8.20.0",
15
15
  "pg-copy-streams": "^7.0.0"
@@ -1,7 +1,6 @@
1
1
  packages:
2
2
  - packages/*
3
3
  - tests/*
4
- - tests-e2e
5
4
 
6
5
  onlyBuiltDependencies:
7
6
  - "@simplysm/sd-claude"
@@ -34,7 +34,6 @@
34
34
  "packages/*/src/**/*.tsx",
35
35
  "packages/*/tests/**/*.ts",
36
36
  "packages/*/tests/**/*.tsx",
37
- "tests/**/*.ts",
38
- "tests-e2e/**/*.ts"
37
+ "tests/**/*.ts"
39
38
  ]
40
39
  }
@@ -1,16 +0,0 @@
1
- {
2
- "name": "@{{projectName}}-test/e2e",
3
- "version": "1.0.0",
4
- "description": "{{projectName}} E2E tests",
5
- "type": "module",
6
- "private": true,
7
- "dependencies": {
8
- "@{{projectName}}/db-main": "workspace:*",
9
- "@simplysm/orm-node": "~13.0.98",
10
- "bcrypt": "^6.0.0",
11
- "playwright": "^1.58.2"
12
- },
13
- "devDependencies": {
14
- "@types/bcrypt": "^6.0.0"
15
- }
16
- }
@@ -1,36 +0,0 @@
1
- import { describe, inject, beforeAll, afterAll } from "vitest";
2
- import { chromium, type Browser, type BrowserContext, type Page } from "playwright";
3
- import { loginTests } from "./login";
4
- import { employeeCrudTests } from "./employee-crud";
5
-
6
- const ctx = {} as { page: Page; baseUrl: string };
7
-
8
- let browser: Browser;
9
- let context: BrowserContext;
10
-
11
- describe("E2E", () => {
12
- beforeAll(async () => {
13
- ctx.baseUrl = inject("baseUrl");
14
- browser = await chromium.launch({ headless: true });
15
- context = await browser.newContext();
16
- ctx.page = await context.newPage();
17
- ctx.page.setDefaultTimeout(500);
18
-
19
- // Output browser console errors to test console
20
- ctx.page.on("pageerror", (err) => {
21
- console.error(`[PAGE_ERROR] ${err.message}`);
22
- });
23
- ctx.page.on("console", (msg) => {
24
- if (msg.type() === "error") {
25
- console.error(`[CONSOLE_ERROR] ${msg.text()}`);
26
- }
27
- });
28
- });
29
-
30
- afterAll(async () => {
31
- await browser.close();
32
- });
33
-
34
- loginTests(ctx);
35
- employeeCrudTests(ctx);
36
- });
@@ -1,204 +0,0 @@
1
- import { describe, it, expect, beforeAll } from "vitest";
2
- import type { Page } from "playwright";
3
-
4
- export function employeeCrudTests(ctx: { page: Page; baseUrl: string }) {
5
- describe("EmployeeSheet CRUD", () => {
6
- const sheetSelector = '[data-sheet="employee-page-sheet"]';
7
-
8
- function sheetRows() {
9
- return ctx.page.locator(`${sheetSelector} tbody tr`);
10
- }
11
-
12
- function dialogLocator() {
13
- return ctx.page.locator("[data-dialog-panel]").last();
14
- }
15
-
16
- async function waitForSheetLoaded() {
17
- await sheetRows().first().waitFor({ timeout: 1000 });
18
- }
19
-
20
- async function waitForBusyDone() {
21
- await ctx.page.waitForTimeout(50);
22
- await ctx.page.waitForFunction(() => !document.querySelector(".animate-spin"));
23
- }
24
-
25
- async function waitForDialogClosed() {
26
- await ctx.page.waitForFunction(
27
- () =>
28
- document.querySelectorAll("[data-dialog-panel]").length === 0 &&
29
- document.querySelectorAll("[data-dialog-backdrop]").length === 0,
30
- );
31
- }
32
-
33
- async function assertNotification(text: string) {
34
- const alert = ctx.page
35
- .locator("[data-notification-banner]")
36
- .filter({ hasText: text })
37
- .first();
38
- await alert.waitFor();
39
- const closeButtons = ctx.page.locator(
40
- '[data-notification-banner] button[aria-label="알림 닫기"]',
41
- );
42
- const count = await closeButtons.count();
43
- for (let i = count - 1; i >= 0; i--) {
44
- await closeButtons
45
- .nth(i)
46
- .click()
47
- .catch(() => {});
48
- }
49
- await ctx.page.waitForFunction(
50
- () => document.querySelectorAll("[data-notification-banner]").length === 0,
51
- );
52
- }
53
-
54
- async function clickSearch() {
55
- await ctx.page.getByRole("button", { name: "조회" }).click();
56
- await waitForBusyDone();
57
- }
58
-
59
- async function clickAdd() {
60
- await ctx.page.getByRole("button", { name: /등록/ }).click();
61
- await dialogLocator().waitFor();
62
- }
63
-
64
- async function clickEditRow(rowIndex: number) {
65
- await sheetRows().nth(rowIndex).locator("a").first().click();
66
- await dialogLocator().waitFor();
67
- await waitForBusyDone();
68
- }
69
-
70
- async function fillDialogField(label: string, value: string) {
71
- const dlg = dialogLocator();
72
- const th = dlg.locator("th").filter({ hasText: label });
73
- const td = th.locator("xpath=following-sibling::td[1]");
74
- const field = td.locator("input:not([aria-hidden])").first();
75
- await field.fill(value);
76
- }
77
-
78
- async function clickDialogSubmit() {
79
- await dialogLocator().getByRole("button", { name: "확인" }).click();
80
- }
81
-
82
- async function closeDialog() {
83
- await ctx.page.keyboard.press("Escape");
84
- await waitForDialogClosed();
85
- }
86
-
87
- beforeAll(async () => {
88
- await ctx.page.goto(`${ctx.baseUrl}/#/home/base/employee`);
89
- await waitForSheetLoaded();
90
- });
91
-
92
- describe("Create", () => {
93
- it("새 직원 등록", async () => {
94
- await clickAdd();
95
-
96
- await fillDialogField("이름", "E2E테스트유저");
97
- await fillDialogField("이메일", "e2e@test.com");
98
-
99
- await clickDialogSubmit();
100
- await assertNotification("저장되었습니다");
101
- await waitForDialogClosed();
102
- await clickSearch();
103
-
104
- const firstCell = sheetRows().first().locator("td").nth(1);
105
- await expect(firstCell.textContent()).resolves.toContain("E2E테스트유저");
106
- });
107
- });
108
-
109
- describe("Read / Filter", () => {
110
- it("검색어 필터링", async () => {
111
- const searchInput = ctx.page.locator("form").first().locator("input").first();
112
- await searchInput.fill("E2E테스트");
113
- await clickSearch();
114
-
115
- const rowCount = await sheetRows().count();
116
- expect(rowCount).toBe(1);
117
- await expect(sheetRows().first().locator("td").nth(1).textContent()).resolves.toContain(
118
- "E2E테스트유저",
119
- );
120
-
121
- await searchInput.fill("");
122
- await clickSearch();
123
- const allRowCount = await sheetRows().count();
124
- expect(allRowCount).toBe(2);
125
- });
126
- });
127
-
128
- describe("Update", () => {
129
- it("기존 유저 이름 수정", async () => {
130
- await clickEditRow(0);
131
-
132
- await fillDialogField("이름", "E2E수정유저");
133
-
134
- await clickDialogSubmit();
135
- await assertNotification("저장되었습니다");
136
- await waitForDialogClosed();
137
- await clickSearch();
138
-
139
- await expect(sheetRows().first().locator("td").nth(1).textContent()).resolves.toContain(
140
- "E2E수정유저",
141
- );
142
- });
143
- });
144
-
145
- describe("Delete", () => {
146
- it("유저 soft delete", async () => {
147
- await clickEditRow(0);
148
-
149
- await dialogLocator().getByRole("button", { name: "삭제" }).click();
150
- await assertNotification("삭제되었습니다");
151
- await waitForDialogClosed();
152
- await clickSearch();
153
-
154
- const rowCount = await sheetRows().count();
155
- expect(rowCount).toBe(1);
156
- await expect(sheetRows().first().locator("td").nth(1).textContent()).resolves.toContain(
157
- "테스트",
158
- );
159
- });
160
-
161
- it("삭제항목 포함 필터로 삭제된 유저 확인", async () => {
162
- await ctx.page.getByText("삭제항목 포함").click();
163
- await clickSearch();
164
-
165
- const rowCount = await sheetRows().count();
166
- expect(rowCount).toBe(2);
167
-
168
- await ctx.page.getByText("삭제항목 포함").click();
169
- await clickSearch();
170
- });
171
- });
172
-
173
- describe("Error cases", () => {
174
- it("이름 중복 에러", async () => {
175
- await clickAdd();
176
- await fillDialogField("이름", "테스트");
177
- await clickDialogSubmit();
178
- await assertNotification("동일한 이름이 이미 등록되어 있습니다");
179
-
180
- await closeDialog();
181
- });
182
-
183
- it("이메일 중복 에러", async () => {
184
- await clickAdd();
185
- await fillDialogField("이름", "고유이름");
186
- await fillDialogField("이메일", "admin@test.com");
187
- await clickDialogSubmit();
188
- await assertNotification("동일한 이메일이 이미 등록되어 있습니다");
189
-
190
- await closeDialog();
191
- });
192
-
193
- it("자기 자신 삭제 불가", async () => {
194
- await clickEditRow(0);
195
- await waitForBusyDone();
196
-
197
- const deleteBtn = dialogLocator().getByRole("button", { name: "삭제" });
198
- expect(await deleteBtn.count()).toBe(0);
199
-
200
- await closeDialog();
201
- });
202
- });
203
- });
204
- }
@@ -1,61 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import type { Page } from "playwright";
3
-
4
- export function loginTests(ctx: { page: Page; baseUrl: string }) {
5
- describe("Login", () => {
6
- it("/ 접속 시 /login으로 리다이렉트", async () => {
7
- await ctx.page.goto(`${ctx.baseUrl}/`, { timeout: 5000 });
8
- await ctx.page.waitForURL(`${ctx.baseUrl}/#/login`);
9
- await expect(
10
- ctx.page.getByRole("button", { name: "로그인" }).textContent(),
11
- ).resolves.toContain("로그인");
12
- });
13
-
14
- it("토큰 없이 /home/main 접근 시 /login으로 리다이렉트", async () => {
15
- await ctx.page.goto(`${ctx.baseUrl}/#/home/main`);
16
- await ctx.page.waitForURL(`${ctx.baseUrl}/#/login`);
17
- await expect(
18
- ctx.page.getByRole("button", { name: "로그인" }).textContent(),
19
- ).resolves.toContain("로그인");
20
- });
21
-
22
- it("틀린 비밀번호 시 에러 메시지", async () => {
23
- await ctx.page.getByPlaceholder("이메일을 입력하세요").fill("admin@test.com");
24
- await ctx.page.getByPlaceholder("비밀번호를 입력하세요").fill("wrongpassword");
25
- await ctx.page.getByRole("button", { name: "로그인" }).click();
26
-
27
- const notification = ctx.page.getByRole("alert");
28
- await notification.waitFor({ timeout: 1000 });
29
- await expect(notification.textContent()).resolves.toContain(
30
- "이메일 또는 비밀번호가 올바르지 않습니다",
31
- );
32
- });
33
-
34
- it("올바른 자격 증명으로 로그인 성공", async () => {
35
- await ctx.page.goto(`${ctx.baseUrl}/#/login`);
36
- await ctx.page.getByRole("button", { name: "로그인" }).waitFor({ timeout: 2000 });
37
-
38
- await ctx.page.getByPlaceholder("이메일을 입력하세요").fill("admin@test.com");
39
- await ctx.page.getByPlaceholder("비밀번호를 입력하세요").fill("test1234");
40
- await ctx.page.getByRole("button", { name: "로그인" }).click({ timeout: 2000 });
41
-
42
- await ctx.page.waitForURL(`${ctx.baseUrl}/#/home/main`, { timeout: 2000 });
43
- const mainContent = ctx.page.locator("main").last();
44
- await expect(mainContent.locator("h1").textContent()).resolves.toContain("테스트");
45
- });
46
-
47
- it("마지막 로그인 이메일 기억", async () => {
48
- const lastLoginEmail = await ctx.page.evaluate(() =>
49
- JSON.parse(localStorage.getItem("client-admin.last-login-email") ?? "null"),
50
- );
51
- expect(lastLoginEmail).toBe("admin@test.com");
52
- });
53
-
54
- it("페이지 새로고침 시 자동 로그인", async () => {
55
- await ctx.page.reload();
56
- await ctx.page.waitForURL(`${ctx.baseUrl}/#/home/main`, { timeout: 2000 });
57
- const mainContent = ctx.page.locator("main").last();
58
- await expect(mainContent.locator("h1").textContent()).resolves.toContain("테스트");
59
- });
60
- });
61
- }
@@ -1,220 +0,0 @@
1
- import { type ChildProcess, spawn, execSync } from "child_process";
2
- import path from "path";
3
- import { fileURLToPath } from "url";
4
- import { createDbConn, createOrm } from "@simplysm/orm-node";
5
- import { MainDbContext } from "@{{projectName}}/db-main";
6
- import bcrypt from "bcrypt";
7
- import { chromium } from "playwright";
8
- import type { TestProject } from "vitest/node";
9
-
10
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
- const rootDir = path.resolve(__dirname, "..");
12
-
13
- const DB_CONFIG = {
14
- dialect: "postgresql" as const,
15
- host: "localhost",
16
- port: 5432,
17
- username: "postgres",
18
- password: "1234",
19
- database: "{{projectName}}-test",
20
- };
21
-
22
- const TEST_USER = {
23
- name: "테스트",
24
- email: "admin@test.com",
25
- password: "test1234",
26
- };
27
-
28
- let devServerProcess: ChildProcess | undefined;
29
-
30
- function killProcessTree(proc: ChildProcess): void {
31
- if (proc.pid == null) return;
32
- try {
33
- if (process.platform === "win32") {
34
- execSync(`taskkill /T /F /PID ${proc.pid}`, { stdio: "ignore" });
35
- } else {
36
- process.kill(-proc.pid, "SIGTERM");
37
- }
38
- } catch {
39
- /* 무시 */
40
- }
41
- }
42
-
43
- function stripAnsi(str: string): string {
44
- return str.replace(/\x1B\[[0-9;]*m/g, "");
45
- }
46
-
47
- function waitForServerUrl(process: ChildProcess, timeout: number): Promise<string> {
48
- return new Promise((resolve, reject) => {
49
- let settled = false;
50
- let output = "";
51
-
52
- const timer = setTimeout(() => {
53
- if (!settled) {
54
- settled = true;
55
- reject(
56
- new Error(`dev 서버 URL 대기 시간 초과 (${timeout}ms)\n출력:\n${output.slice(-2000)}`),
57
- );
58
- }
59
- }, timeout);
60
-
61
- function onData(data: Buffer) {
62
- const text = stripAnsi(data.toString());
63
- console.log(text);
64
- output += text;
65
-
66
- const urlMatch = text.match(/http:\/\/localhost:\d+\/client-admin\/?/);
67
- if (urlMatch && !settled) {
68
- settled = true;
69
- clearTimeout(timer);
70
- resolve(urlMatch[0].endsWith("/") ? urlMatch[0].slice(0, -1) : urlMatch[0]);
71
- }
72
- }
73
-
74
- process.stdout?.on("data", onData);
75
- process.stderr?.on("data", onData);
76
-
77
- process.on("exit", (code) => {
78
- if (!settled) {
79
- settled = true;
80
- clearTimeout(timer);
81
- reject(
82
- new Error(`dev 서버가 비정상 종료됨 (code: ${code})\n출력:\n${output.slice(-2000)}`),
83
- );
84
- }
85
- });
86
- });
87
- }
88
-
89
- async function warmup(baseUrl: string, timeout: number): Promise<void> {
90
- const browser = await chromium.launch({ headless: true });
91
- try {
92
- const page = await browser.newPage();
93
- await page.goto(baseUrl, { timeout });
94
-
95
- const result = await Promise.race([
96
- page
97
- .locator(".app-loading")
98
- .waitFor({ state: "detached", timeout })
99
- .then(() => "ready" as const),
100
- page
101
- .locator("vite-error-overlay")
102
- .waitFor({ state: "attached", timeout })
103
- .then(async () => {
104
- const errorText = await page
105
- .locator("vite-error-overlay")
106
- .evaluate((el) => el.shadowRoot?.textContent ?? "");
107
- return `error:${errorText}` as const;
108
- }),
109
- ]);
110
-
111
- if (result.startsWith("error:")) {
112
- throw new Error(`Vite 빌드 에러:\n${result.slice(6).slice(0, 2000)}`);
113
- }
114
- } finally {
115
- await browser.close();
116
- }
117
- }
118
-
119
- export async function setup(project: TestProject) {
120
- // 1. 테스트 DB 생성 (없으면)
121
- console.log("[e2e] 테스트 DB 확인...");
122
- const adminConn = await createDbConn({ ...DB_CONFIG, database: "postgres" });
123
- await adminConn.connect();
124
- try {
125
- const result = await adminConn.executeParametrized(
126
- `SELECT 1 FROM pg_database WHERE datname = $1`,
127
- [DB_CONFIG.database],
128
- );
129
- if (result[0].length === 0) {
130
- await adminConn.execute([`CREATE DATABASE "${DB_CONFIG.database}"`]);
131
- console.log(`[e2e] DB "${DB_CONFIG.database}" 생성됨.`);
132
- } else {
133
- console.log(`[e2e] DB "${DB_CONFIG.database}" 이미 존재.`);
134
- }
135
- } finally {
136
- await adminConn.close();
137
- }
138
-
139
- // 2. DB 초기화 및 시딩
140
- const orm = createOrm(MainDbContext, DB_CONFIG);
141
- await orm.connectWithoutTransaction(async (db) => {
142
- await db.initialize({ force: true });
143
-
144
- // 권한그룹 생성
145
- await db.role().insert([{ name: "관리자" }]);
146
- const role = (await db.role().first())!;
147
-
148
- // 전체 권한 부여
149
- await db.rolePermission().insert([
150
- { roleId: role.id, code: "/home/base/employee/use", valueJson: "true" },
151
- { roleId: role.id, code: "/home/base/employee/edit", valueJson: "true" },
152
- { roleId: role.id, code: "/home/base/employee/auth/use", valueJson: "true" },
153
- { roleId: role.id, code: "/home/base/employee/auth/edit", valueJson: "true" },
154
- { roleId: role.id, code: "/home/base/employee/personal/use", valueJson: "true" },
155
- { roleId: role.id, code: "/home/base/employee/personal/edit", valueJson: "true" },
156
- { roleId: role.id, code: "/home/base/employee/payroll/use", valueJson: "true" },
157
- { roleId: role.id, code: "/home/base/employee/payroll/edit", valueJson: "true" },
158
- ]);
159
-
160
- // 테스트 유저 생성 (roleId 포함)
161
- const encryptedPassword = await bcrypt.hash(TEST_USER.password, 10);
162
- await db.employee().insert([
163
- {
164
- name: TEST_USER.name,
165
- email: TEST_USER.email,
166
- encryptedPassword,
167
- roleId: role.id,
168
- isDeleted: false,
169
- },
170
- ]);
171
- });
172
- console.log("[e2e] DB 초기화 및 시딩 완료.");
173
-
174
- // 3. dev 서버 시작
175
- console.log("[e2e] dev 서버 시작...");
176
- devServerProcess = spawn("node", ["node_modules/@simplysm/sd-cli/dist/sd-cli.js", "dev"], {
177
- cwd: rootDir,
178
- stdio: "pipe",
179
- ...(process.platform !== "win32" ? { detached: true } : {}),
180
- env: {
181
- ...process.env,
182
- DB_DATABASE: DB_CONFIG.database,
183
- DB_PORT: String(DB_CONFIG.port),
184
- NODE_ENV: undefined, // consola가 콘솔을 강제로 warn이상으로 설정하지 못하도록 하기 위함
185
- TEST: undefined, // consola가 콘솔을 강제로 warn이상으로 설정하지 못하도록 하기 위함
186
- },
187
- });
188
-
189
- let baseUrl: string;
190
- try {
191
- // 4. stdout에서 URL 파싱 (90초 타임아웃)
192
- baseUrl = await waitForServerUrl(devServerProcess, 90_000);
193
- console.log(`[e2e] dev 서버 URL 감지: ${baseUrl}`);
194
-
195
- // 5. Playwright warmup — Vite 빌드 트리거 및 에러 감지 (60초 타임아웃)
196
- console.log("[e2e] Vite 빌드 warmup...");
197
- await warmup(baseUrl, 60_000);
198
- console.log("[e2e] warmup 완료. 테스트 시작 준비됨.");
199
-
200
- project.provide("baseUrl", baseUrl);
201
- } catch (err) {
202
- killProcessTree(devServerProcess);
203
- devServerProcess = undefined;
204
- throw err;
205
- }
206
- }
207
-
208
- export function teardown() {
209
- if (devServerProcess != null) {
210
- console.log("[e2e] dev 서버 종료...");
211
- killProcessTree(devServerProcess);
212
- devServerProcess = undefined;
213
- }
214
- }
215
-
216
- declare module "vitest" {
217
- export interface ProvidedContext {
218
- baseUrl: string;
219
- }
220
- }
@@ -1,23 +0,0 @@
1
- import { defineConfig } from "vitest/config";
2
- import tsconfigPaths from "vite-tsconfig-paths";
3
-
4
- export default defineConfig({
5
- plugins: [
6
- tsconfigPaths({
7
- root: ".",
8
- projects: ["./tsconfig.json"],
9
- }),
10
- ],
11
- define: {
12
- "process.env.DEV": JSON.stringify("true"),
13
- "process.env.VER": JSON.stringify("1.0.0-test"),
14
- },
15
- test: {
16
- globals: true,
17
- environment: "node",
18
- include: ["tests-e2e/src/**/*.spec.ts"],
19
- globalSetup: "./tests-e2e/vitest.setup.ts",
20
- fileParallelism: false,
21
- testTimeout: 30000,
22
- },
23
- });