@simplysm/sd-cli 13.0.71 → 13.0.74
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 +62 -14
- package/dist/commands/init.d.ts +4 -5
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +26 -8
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/publish.js +1 -1
- package/dist/commands/publish.js.map +1 -1
- package/dist/sd-cli-entry.d.ts.map +1 -1
- package/dist/sd-cli-entry.js +0 -20
- package/dist/sd-cli-entry.js.map +1 -1
- package/package.json +4 -4
- package/src/commands/init.ts +40 -21
- package/src/commands/publish.ts +1 -1
- package/src/sd-cli-entry.ts +0 -24
- package/src/utils/replace-deps.ts +361 -361
- package/src/utils/sd-config.ts +44 -44
- package/src/utils/tailwind-config-deps.ts +98 -98
- package/src/utils/template.ts +56 -56
- package/src/utils/tsconfig.ts +127 -127
- package/src/utils/typecheck-serialization.ts +86 -86
- package/templates/init/{.prettierrc.yaml.hbs → .prettierrc.yaml} +1 -1
- package/templates/init/eslint.config.ts +15 -0
- package/templates/init/mise.toml +3 -0
- package/templates/init/package.json.hbs +8 -7
- package/templates/init/packages/client-admin/index.html.hbs +144 -0
- package/templates/init/packages/client-admin/package.json.hbs +26 -0
- package/templates/init/packages/client-admin/public/assets/logo-landscape.png +0 -0
- package/templates/init/packages/client-admin/public/assets/logo.png +0 -0
- package/templates/init/packages/client-admin/src/App.tsx +42 -0
- package/templates/init/packages/client-admin/src/dev/DevDialog.tsx +34 -0
- package/templates/{add-client/__CLIENT__/src/main.css.hbs → init/packages/client-admin/src/main.css} +1 -1
- package/templates/init/packages/client-admin/src/main.tsx.hbs +146 -0
- package/templates/init/packages/client-admin/src/providers/AppServiceProvider.tsx.hbs +103 -0
- package/templates/init/packages/client-admin/src/providers/AppStructureProvider.tsx +84 -0
- package/templates/init/packages/client-admin/src/providers/AuthProvider.tsx.hbs +71 -0
- package/templates/init/packages/client-admin/src/providers/configureSharedData.ts.hbs +67 -0
- package/templates/init/packages/client-admin/src/views/auth/LoginView.tsx +132 -0
- package/templates/init/packages/client-admin/src/views/home/HomeView.tsx +108 -0
- package/templates/init/packages/client-admin/src/views/home/base/employee/EmployeeDetail.tsx.hbs +262 -0
- package/templates/init/packages/client-admin/src/views/home/base/employee/EmployeeSheet.tsx.hbs +271 -0
- package/templates/init/packages/client-admin/src/views/home/base/role-permission/RoleDetail.tsx.hbs +154 -0
- package/templates/init/packages/client-admin/src/views/home/base/role-permission/RolePermissionDetail.tsx.hbs +123 -0
- package/templates/init/packages/client-admin/src/views/home/base/role-permission/RolePermissionView.tsx +52 -0
- package/templates/init/packages/client-admin/src/views/home/base/role-permission/RoleSheet.tsx.hbs +125 -0
- package/templates/init/packages/client-admin/src/views/home/main/MainView.tsx.hbs +13 -0
- package/templates/init/packages/client-admin/src/views/home/my-info/MyInfoDetail.tsx.hbs +248 -0
- package/templates/init/packages/client-admin/src/views/home/system/system-log/SystemLogSheet.tsx.hbs +169 -0
- package/templates/init/packages/client-admin/src/views/not-found/NotFoundView.tsx +15 -0
- package/templates/init/packages/client-admin/tailwind.config.ts +10 -0
- package/templates/init/packages/db-main/package.json.hbs +13 -0
- package/templates/init/packages/db-main/src/MainDbContext.ts +20 -0
- package/templates/init/packages/db-main/src/dataLogExt.ts +127 -0
- package/templates/init/packages/db-main/src/index.ts +10 -0
- package/templates/init/packages/db-main/src/tables/Employee.ts +24 -0
- package/templates/init/packages/db-main/src/tables/EmployeeConfig.ts +13 -0
- package/templates/init/packages/db-main/src/tables/Role.ts +9 -0
- package/templates/init/packages/db-main/src/tables/RolePermission.ts +13 -0
- package/templates/init/packages/db-main/src/tables/_DataLog.ts +19 -0
- package/templates/init/packages/db-main/src/tables/_Log.ts +16 -0
- package/templates/init/packages/server/package.json.hbs +20 -0
- package/templates/init/packages/server/public-dev/dev//354/264/210/352/270/260/355/231/224.xlsx +0 -0
- package/templates/init/packages/server/src/index.ts +4 -0
- package/templates/init/packages/server/src/main.ts.hbs +34 -0
- package/templates/init/packages/server/src/services/AuthService.ts.hbs +171 -0
- package/templates/init/packages/server/src/services/DevService.ts.hbs +94 -0
- package/templates/init/packages/server/src/services/EmployeeService.ts.hbs +122 -0
- package/templates/init/packages/server/src/services/RoleService.ts.hbs +59 -0
- package/templates/init/{pnpm-workspace.yaml.hbs → pnpm-workspace.yaml} +3 -1
- package/templates/init/sd.config.ts.hbs +30 -1
- package/templates/init/tests/e2e/package.json.hbs +16 -0
- package/templates/init/tests/e2e/src/e2e.spec.ts +36 -0
- package/templates/init/tests/e2e/src/employee-crud.ts +204 -0
- package/templates/init/tests/e2e/src/login.ts +61 -0
- package/templates/init/tests/e2e/vitest.setup.ts.hbs +220 -0
- package/templates/init/tsconfig.json.hbs +0 -11
- package/templates/init/{vitest.config.ts.hbs → vitest.config.ts} +16 -12
- package/dist/commands/add-client.d.ts +0 -18
- package/dist/commands/add-client.d.ts.map +0 -1
- package/dist/commands/add-client.js +0 -79
- package/dist/commands/add-client.js.map +0 -6
- package/dist/commands/add-server.d.ts +0 -18
- package/dist/commands/add-server.d.ts.map +0 -1
- package/dist/commands/add-server.js +0 -83
- package/dist/commands/add-server.js.map +0 -6
- package/dist/utils/config-editor.d.ts +0 -17
- package/dist/utils/config-editor.d.ts.map +0 -1
- package/dist/utils/config-editor.js +0 -79
- package/dist/utils/config-editor.js.map +0 -6
- package/src/commands/add-client.ts +0 -126
- package/src/commands/add-server.ts +0 -138
- package/src/utils/config-editor.ts +0 -141
- package/templates/add-client/__CLIENT__/index.html.hbs +0 -13
- package/templates/add-client/__CLIENT__/package.json.hbs +0 -16
- package/templates/add-client/__CLIENT__/src/App.tsx.hbs +0 -65
- package/templates/add-client/__CLIENT__/src/appStructure.ts.hbs +0 -20
- package/templates/add-client/__CLIENT__/src/main.tsx.hbs +0 -24
- package/templates/add-client/__CLIENT__/src/pages/HomePage.tsx.hbs +0 -9
- package/templates/add-client/__CLIENT__/tailwind.config.ts.hbs +0 -15
- package/templates/add-server/__SERVER__/package.json.hbs +0 -10
- package/templates/add-server/__SERVER__/src/main.ts.hbs +0 -14
- package/templates/init/.gitignore.hbs +0 -26
- package/templates/init/.npmrc.hbs +0 -1
- package/templates/init/eslint.config.ts.hbs +0 -5
- package/templates/init/mise.toml.hbs +0 -3
- package/tests/config-editor.spec.ts +0 -160
- /package/templates/init/{.prettierignore.hbs → .prettierignore} +0 -0
- /package/templates/{add-client/__CLIENT__ → init/packages/client-admin}/public/favicon.ico +0 -0
- /package/templates/init/{stylelint.config.ts.hbs → stylelint.config.ts} +0 -0
|
@@ -1,361 +1,361 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import { glob } from "glob";
|
|
4
|
-
import { consola } from "consola";
|
|
5
|
-
import { fsCopy, fsMkdir, fsRm, FsWatcher, pathIsChildPath } from "@simplysm/core-node";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Match glob patterns from replaceDeps config with target package list
|
|
9
|
-
* and return { targetName, sourcePath } pairs
|
|
10
|
-
*
|
|
11
|
-
* @param replaceDeps - replaceDeps config from sd.config.ts (key: glob pattern, value: source path)
|
|
12
|
-
* @param targetNames - List of package names found in node_modules (e.g., ["@simplysm/solid", ...])
|
|
13
|
-
* @returns Array of matched { targetName, sourcePath }
|
|
14
|
-
*/
|
|
15
|
-
export function resolveReplaceDepEntries(
|
|
16
|
-
replaceDeps: Record<string, string>,
|
|
17
|
-
targetNames: string[],
|
|
18
|
-
): Array<{ targetName: string; sourcePath: string }> {
|
|
19
|
-
const results: Array<{ targetName: string; sourcePath: string }> = [];
|
|
20
|
-
|
|
21
|
-
for (const [pattern, sourceTemplate] of Object.entries(replaceDeps)) {
|
|
22
|
-
// Convert glob pattern to regex: * → (.*), . → \., / → [\\/]
|
|
23
|
-
const regexpText = pattern.replace(/[\\/.+*]/g, (ch) => {
|
|
24
|
-
if (ch === "*") return "(.*)";
|
|
25
|
-
if (ch === ".") return "\\.";
|
|
26
|
-
if (ch === "/" || ch === "\\") return "[\\\\/]";
|
|
27
|
-
if (ch === "+") return "\\+";
|
|
28
|
-
return ch;
|
|
29
|
-
});
|
|
30
|
-
const regex = new RegExp(`^${regexpText}$`);
|
|
31
|
-
const hasWildcard = pattern.includes("*");
|
|
32
|
-
|
|
33
|
-
for (const targetName of targetNames) {
|
|
34
|
-
const match = regex.exec(targetName);
|
|
35
|
-
if (match == null) continue;
|
|
36
|
-
|
|
37
|
-
// If capture group exists, substitute * in source path with captured value
|
|
38
|
-
const sourcePath = hasWildcard ? sourceTemplate.replace(/\*/g, match[1]) : sourceTemplate;
|
|
39
|
-
|
|
40
|
-
results.push({ targetName, sourcePath });
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return results;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Parse pnpm-workspace.yaml content and return array of workspace packages globs
|
|
49
|
-
* Simple line parsing without separate YAML library
|
|
50
|
-
*
|
|
51
|
-
* @param content - Content of pnpm-workspace.yaml file
|
|
52
|
-
* @returns Array of glob patterns (e.g., ["packages/*", "tools/*"])
|
|
53
|
-
*/
|
|
54
|
-
export function parseWorkspaceGlobs(content: string): string[] {
|
|
55
|
-
const lines = content.split("\n");
|
|
56
|
-
const globs: string[] = [];
|
|
57
|
-
let inPackages = false;
|
|
58
|
-
|
|
59
|
-
for (const line of lines) {
|
|
60
|
-
const trimmed = line.trim();
|
|
61
|
-
|
|
62
|
-
if (trimmed === "packages:") {
|
|
63
|
-
inPackages = true;
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// List items in packages section
|
|
68
|
-
if (inPackages && trimmed.startsWith("- ")) {
|
|
69
|
-
const value = trimmed
|
|
70
|
-
.slice(2)
|
|
71
|
-
.trim()
|
|
72
|
-
.replace(/^["']|["']$/g, "");
|
|
73
|
-
globs.push(value);
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// End when other section starts
|
|
78
|
-
if (inPackages && trimmed !== "" && !trimmed.startsWith("#")) {
|
|
79
|
-
break;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return globs;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Names to exclude during copy
|
|
88
|
-
*/
|
|
89
|
-
const EXCLUDED_NAMES = new Set(["node_modules", "package.json", ".cache", "tests"]);
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Filter function for replaceDeps copy
|
|
93
|
-
* Excludes node_modules, package.json, .cache, tests
|
|
94
|
-
*
|
|
95
|
-
* @param itemPath - Absolute path of item to copy
|
|
96
|
-
* @returns true if copy target, false if excluded
|
|
97
|
-
*/
|
|
98
|
-
function replaceDepsCopyFilter(itemPath: string): boolean {
|
|
99
|
-
const basename = path.basename(itemPath);
|
|
100
|
-
return !EXCLUDED_NAMES.has(basename);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* replaceDeps copy/replace item
|
|
105
|
-
*/
|
|
106
|
-
export interface ReplaceDepEntry {
|
|
107
|
-
targetName: string;
|
|
108
|
-
sourcePath: string;
|
|
109
|
-
targetPath: string;
|
|
110
|
-
resolvedSourcePath: string;
|
|
111
|
-
actualTargetPath: string;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Return type of watchReplaceDeps
|
|
116
|
-
*/
|
|
117
|
-
export interface WatchReplaceDepResult {
|
|
118
|
-
entries: ReplaceDepEntry[];
|
|
119
|
-
dispose: () => void;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Collect project root and workspace package paths.
|
|
124
|
-
*
|
|
125
|
-
* Parse pnpm-workspace.yaml to collect absolute paths of workspace packages.
|
|
126
|
-
* If file is missing or parsing fails, return only root path.
|
|
127
|
-
*
|
|
128
|
-
* @param projectRoot - Project root path
|
|
129
|
-
* @returns [root, ...workspace package paths] array
|
|
130
|
-
*/
|
|
131
|
-
async function collectSearchRoots(projectRoot: string): Promise<string[]> {
|
|
132
|
-
const searchRoots = [projectRoot];
|
|
133
|
-
|
|
134
|
-
const workspaceYamlPath = path.join(projectRoot, "pnpm-workspace.yaml");
|
|
135
|
-
try {
|
|
136
|
-
const yamlContent = await fs.promises.readFile(workspaceYamlPath, "utf-8");
|
|
137
|
-
const workspaceGlobs = parseWorkspaceGlobs(yamlContent);
|
|
138
|
-
|
|
139
|
-
for (const pattern of workspaceGlobs) {
|
|
140
|
-
const dirs = await glob(pattern, { cwd: projectRoot, absolute: true });
|
|
141
|
-
searchRoots.push(...dirs);
|
|
142
|
-
}
|
|
143
|
-
} catch {
|
|
144
|
-
// If pnpm-workspace.yaml doesn't exist, only process root
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return searchRoots;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Resolve all replacement target items from replaceDeps config.
|
|
152
|
-
*
|
|
153
|
-
* 1. Parse pnpm-workspace.yaml → workspace package paths
|
|
154
|
-
* 2. Find matching packages in [root, ...workspace packages] node_modules
|
|
155
|
-
* 3. Pattern matching + verify source path exists + resolve symlinks
|
|
156
|
-
*
|
|
157
|
-
* @param projectRoot - Project root path
|
|
158
|
-
* @param replaceDeps - replaceDeps config from sd.config.ts
|
|
159
|
-
* @param logger - consola logger
|
|
160
|
-
* @returns Array of resolved replacement target items
|
|
161
|
-
*/
|
|
162
|
-
async function resolveAllReplaceDepEntries(
|
|
163
|
-
projectRoot: string,
|
|
164
|
-
replaceDeps: Record<string, string>,
|
|
165
|
-
logger: ReturnType<typeof consola.withTag>,
|
|
166
|
-
): Promise<ReplaceDepEntry[]> {
|
|
167
|
-
const entries: ReplaceDepEntry[] = [];
|
|
168
|
-
|
|
169
|
-
const searchRoots = await collectSearchRoots(projectRoot);
|
|
170
|
-
|
|
171
|
-
for (const searchRoot of searchRoots) {
|
|
172
|
-
const nodeModulesDir = path.join(searchRoot, "node_modules");
|
|
173
|
-
|
|
174
|
-
try {
|
|
175
|
-
await fs.promises.access(nodeModulesDir);
|
|
176
|
-
} catch {
|
|
177
|
-
continue; // Skip if node_modules doesn't exist
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Search node_modules directories using each glob pattern from replaceDeps
|
|
181
|
-
const targetNames: string[] = [];
|
|
182
|
-
for (const pattern of Object.keys(replaceDeps)) {
|
|
183
|
-
const matches = await glob(pattern, { cwd: nodeModulesDir });
|
|
184
|
-
targetNames.push(...matches);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (targetNames.length === 0) continue;
|
|
188
|
-
|
|
189
|
-
// Pattern matching and path resolution
|
|
190
|
-
const matchedEntries = resolveReplaceDepEntries(replaceDeps, targetNames);
|
|
191
|
-
|
|
192
|
-
for (const { targetName, sourcePath } of matchedEntries) {
|
|
193
|
-
const targetPath = path.join(nodeModulesDir, targetName);
|
|
194
|
-
const resolvedSourcePath = path.resolve(projectRoot, sourcePath);
|
|
195
|
-
|
|
196
|
-
// Verify source path exists
|
|
197
|
-
try {
|
|
198
|
-
await fs.promises.access(resolvedSourcePath);
|
|
199
|
-
} catch {
|
|
200
|
-
logger.warn(`Source path does not exist, skipping: ${resolvedSourcePath}`);
|
|
201
|
-
continue;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// If targetPath is symlink, resolve to get actual .pnpm store path
|
|
205
|
-
let actualTargetPath = targetPath;
|
|
206
|
-
try {
|
|
207
|
-
const stat = await fs.promises.lstat(targetPath);
|
|
208
|
-
if (stat.isSymbolicLink()) {
|
|
209
|
-
actualTargetPath = await fs.promises.realpath(targetPath);
|
|
210
|
-
}
|
|
211
|
-
} catch {
|
|
212
|
-
// If targetPath doesn't exist, use as-is
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
entries.push({
|
|
216
|
-
targetName,
|
|
217
|
-
sourcePath,
|
|
218
|
-
targetPath,
|
|
219
|
-
resolvedSourcePath,
|
|
220
|
-
actualTargetPath,
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
return entries;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Replace packages in node_modules with source directories according to replaceDeps config.
|
|
230
|
-
*
|
|
231
|
-
* 1. Parse pnpm-workspace.yaml → workspace package paths
|
|
232
|
-
* 2. Find matching packages in [root, ...workspace packages] node_modules
|
|
233
|
-
* 3. Remove existing symlinks/directories → copy source path (excluding node_modules, package.json, .cache, tests)
|
|
234
|
-
*
|
|
235
|
-
* @param projectRoot - Project root path
|
|
236
|
-
* @param replaceDeps - replaceDeps config from sd.config.ts
|
|
237
|
-
*/
|
|
238
|
-
export async function setupReplaceDeps(
|
|
239
|
-
projectRoot: string,
|
|
240
|
-
replaceDeps: Record<string, string>,
|
|
241
|
-
): Promise<void> {
|
|
242
|
-
const logger = consola.withTag("sd:cli:replace-deps");
|
|
243
|
-
let setupCount = 0;
|
|
244
|
-
|
|
245
|
-
logger.start("Setting up replace-deps");
|
|
246
|
-
|
|
247
|
-
const entries = await resolveAllReplaceDepEntries(projectRoot, replaceDeps, logger);
|
|
248
|
-
|
|
249
|
-
for (const { targetName, resolvedSourcePath, actualTargetPath } of entries) {
|
|
250
|
-
try {
|
|
251
|
-
// Overwrite-copy source files to actualTargetPath (maintain existing directory, preserve symlinks)
|
|
252
|
-
await fsCopy(resolvedSourcePath, actualTargetPath, replaceDepsCopyFilter);
|
|
253
|
-
|
|
254
|
-
setupCount += 1;
|
|
255
|
-
} catch (err) {
|
|
256
|
-
logger.error(`Copy replace failed (${targetName}): ${err instanceof Error ? err.message : err}`);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
logger.success(`Replaced ${setupCount} dependencies`);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Watch source directories according to replaceDeps config and copy changes to target paths.
|
|
265
|
-
*
|
|
266
|
-
* 1. Parse pnpm-workspace.yaml → workspace package paths
|
|
267
|
-
* 2. Find matching packages in [root, ...workspace packages] node_modules
|
|
268
|
-
* 3. Watch source directories with FsWatcher (300ms delay)
|
|
269
|
-
* 4. Copy changes to target paths (excluding node_modules, package.json, .cache, tests)
|
|
270
|
-
*
|
|
271
|
-
* @param projectRoot - Project root path
|
|
272
|
-
* @param replaceDeps - replaceDeps config from sd.config.ts
|
|
273
|
-
* @returns entries and dispose function
|
|
274
|
-
*/
|
|
275
|
-
export async function watchReplaceDeps(
|
|
276
|
-
projectRoot: string,
|
|
277
|
-
replaceDeps: Record<string, string>,
|
|
278
|
-
): Promise<WatchReplaceDepResult> {
|
|
279
|
-
const logger = consola.withTag("sd:cli:replace-deps:watch");
|
|
280
|
-
|
|
281
|
-
const entries = await resolveAllReplaceDepEntries(projectRoot, replaceDeps, logger);
|
|
282
|
-
|
|
283
|
-
// Setup source directory watchers
|
|
284
|
-
const watchers: FsWatcher[] = [];
|
|
285
|
-
const watchedSources = new Set<string>();
|
|
286
|
-
|
|
287
|
-
logger.start(`Watching ${entries.length} replace-deps target(s)`);
|
|
288
|
-
|
|
289
|
-
for (const entry of entries) {
|
|
290
|
-
if (watchedSources.has(entry.resolvedSourcePath)) continue;
|
|
291
|
-
watchedSources.add(entry.resolvedSourcePath);
|
|
292
|
-
|
|
293
|
-
const excludedPaths = [...EXCLUDED_NAMES].map((name) =>
|
|
294
|
-
path.join(entry.resolvedSourcePath, name),
|
|
295
|
-
);
|
|
296
|
-
|
|
297
|
-
const watcher = await FsWatcher.watch([entry.resolvedSourcePath], { followSymlinks: false });
|
|
298
|
-
watcher.onChange({ delay: 300 }, async (changeInfos) => {
|
|
299
|
-
for (const { path: changedPath } of changeInfos) {
|
|
300
|
-
// Filter excluded items: basename match or path within excluded directory
|
|
301
|
-
if (
|
|
302
|
-
EXCLUDED_NAMES.has(path.basename(changedPath)) ||
|
|
303
|
-
excludedPaths.some((ep) => pathIsChildPath(changedPath, ep))
|
|
304
|
-
) {
|
|
305
|
-
continue;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// Copy for all entries using this source path
|
|
309
|
-
for (const e of entries) {
|
|
310
|
-
if (e.resolvedSourcePath !== entry.resolvedSourcePath) continue;
|
|
311
|
-
|
|
312
|
-
// Calculate relative path from source
|
|
313
|
-
const relativePath = path.relative(e.resolvedSourcePath, changedPath);
|
|
314
|
-
const destPath = path.join(e.actualTargetPath, relativePath);
|
|
315
|
-
|
|
316
|
-
try {
|
|
317
|
-
// Check if source exists
|
|
318
|
-
let sourceExists = false;
|
|
319
|
-
try {
|
|
320
|
-
await fs.promises.access(changedPath);
|
|
321
|
-
sourceExists = true;
|
|
322
|
-
} catch {
|
|
323
|
-
// Source was deleted
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
if (sourceExists) {
|
|
327
|
-
// Check if source is directory or file
|
|
328
|
-
const stat = await fs.promises.stat(changedPath);
|
|
329
|
-
if (stat.isDirectory()) {
|
|
330
|
-
await fsMkdir(destPath);
|
|
331
|
-
} else {
|
|
332
|
-
await fsMkdir(path.dirname(destPath));
|
|
333
|
-
await fsCopy(changedPath, destPath, replaceDepsCopyFilter);
|
|
334
|
-
}
|
|
335
|
-
} else {
|
|
336
|
-
// Source was deleted → delete target
|
|
337
|
-
await fsRm(destPath);
|
|
338
|
-
}
|
|
339
|
-
} catch (err) {
|
|
340
|
-
logger.error(
|
|
341
|
-
`Copy failed (${e.targetName}/${relativePath}): ${err instanceof Error ? err.message : err}`,
|
|
342
|
-
);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
watchers.push(watcher);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
logger.success(`Replace-deps watch ready`);
|
|
352
|
-
|
|
353
|
-
return {
|
|
354
|
-
entries,
|
|
355
|
-
dispose: () => {
|
|
356
|
-
for (const watcher of watchers) {
|
|
357
|
-
void watcher.close();
|
|
358
|
-
}
|
|
359
|
-
},
|
|
360
|
-
};
|
|
361
|
-
}
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { glob } from "glob";
|
|
4
|
+
import { consola } from "consola";
|
|
5
|
+
import { fsCopy, fsMkdir, fsRm, FsWatcher, pathIsChildPath } from "@simplysm/core-node";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Match glob patterns from replaceDeps config with target package list
|
|
9
|
+
* and return { targetName, sourcePath } pairs
|
|
10
|
+
*
|
|
11
|
+
* @param replaceDeps - replaceDeps config from sd.config.ts (key: glob pattern, value: source path)
|
|
12
|
+
* @param targetNames - List of package names found in node_modules (e.g., ["@simplysm/solid", ...])
|
|
13
|
+
* @returns Array of matched { targetName, sourcePath }
|
|
14
|
+
*/
|
|
15
|
+
export function resolveReplaceDepEntries(
|
|
16
|
+
replaceDeps: Record<string, string>,
|
|
17
|
+
targetNames: string[],
|
|
18
|
+
): Array<{ targetName: string; sourcePath: string }> {
|
|
19
|
+
const results: Array<{ targetName: string; sourcePath: string }> = [];
|
|
20
|
+
|
|
21
|
+
for (const [pattern, sourceTemplate] of Object.entries(replaceDeps)) {
|
|
22
|
+
// Convert glob pattern to regex: * → (.*), . → \., / → [\\/]
|
|
23
|
+
const regexpText = pattern.replace(/[\\/.+*]/g, (ch) => {
|
|
24
|
+
if (ch === "*") return "(.*)";
|
|
25
|
+
if (ch === ".") return "\\.";
|
|
26
|
+
if (ch === "/" || ch === "\\") return "[\\\\/]";
|
|
27
|
+
if (ch === "+") return "\\+";
|
|
28
|
+
return ch;
|
|
29
|
+
});
|
|
30
|
+
const regex = new RegExp(`^${regexpText}$`);
|
|
31
|
+
const hasWildcard = pattern.includes("*");
|
|
32
|
+
|
|
33
|
+
for (const targetName of targetNames) {
|
|
34
|
+
const match = regex.exec(targetName);
|
|
35
|
+
if (match == null) continue;
|
|
36
|
+
|
|
37
|
+
// If capture group exists, substitute * in source path with captured value
|
|
38
|
+
const sourcePath = hasWildcard ? sourceTemplate.replace(/\*/g, match[1]) : sourceTemplate;
|
|
39
|
+
|
|
40
|
+
results.push({ targetName, sourcePath });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return results;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Parse pnpm-workspace.yaml content and return array of workspace packages globs
|
|
49
|
+
* Simple line parsing without separate YAML library
|
|
50
|
+
*
|
|
51
|
+
* @param content - Content of pnpm-workspace.yaml file
|
|
52
|
+
* @returns Array of glob patterns (e.g., ["packages/*", "tools/*"])
|
|
53
|
+
*/
|
|
54
|
+
export function parseWorkspaceGlobs(content: string): string[] {
|
|
55
|
+
const lines = content.split("\n");
|
|
56
|
+
const globs: string[] = [];
|
|
57
|
+
let inPackages = false;
|
|
58
|
+
|
|
59
|
+
for (const line of lines) {
|
|
60
|
+
const trimmed = line.trim();
|
|
61
|
+
|
|
62
|
+
if (trimmed === "packages:") {
|
|
63
|
+
inPackages = true;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// List items in packages section
|
|
68
|
+
if (inPackages && trimmed.startsWith("- ")) {
|
|
69
|
+
const value = trimmed
|
|
70
|
+
.slice(2)
|
|
71
|
+
.trim()
|
|
72
|
+
.replace(/^["']|["']$/g, "");
|
|
73
|
+
globs.push(value);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// End when other section starts
|
|
78
|
+
if (inPackages && trimmed !== "" && !trimmed.startsWith("#")) {
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return globs;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Names to exclude during copy
|
|
88
|
+
*/
|
|
89
|
+
const EXCLUDED_NAMES = new Set(["node_modules", "package.json", ".cache", "tests"]);
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Filter function for replaceDeps copy
|
|
93
|
+
* Excludes node_modules, package.json, .cache, tests
|
|
94
|
+
*
|
|
95
|
+
* @param itemPath - Absolute path of item to copy
|
|
96
|
+
* @returns true if copy target, false if excluded
|
|
97
|
+
*/
|
|
98
|
+
function replaceDepsCopyFilter(itemPath: string): boolean {
|
|
99
|
+
const basename = path.basename(itemPath);
|
|
100
|
+
return !EXCLUDED_NAMES.has(basename);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* replaceDeps copy/replace item
|
|
105
|
+
*/
|
|
106
|
+
export interface ReplaceDepEntry {
|
|
107
|
+
targetName: string;
|
|
108
|
+
sourcePath: string;
|
|
109
|
+
targetPath: string;
|
|
110
|
+
resolvedSourcePath: string;
|
|
111
|
+
actualTargetPath: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Return type of watchReplaceDeps
|
|
116
|
+
*/
|
|
117
|
+
export interface WatchReplaceDepResult {
|
|
118
|
+
entries: ReplaceDepEntry[];
|
|
119
|
+
dispose: () => void;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Collect project root and workspace package paths.
|
|
124
|
+
*
|
|
125
|
+
* Parse pnpm-workspace.yaml to collect absolute paths of workspace packages.
|
|
126
|
+
* If file is missing or parsing fails, return only root path.
|
|
127
|
+
*
|
|
128
|
+
* @param projectRoot - Project root path
|
|
129
|
+
* @returns [root, ...workspace package paths] array
|
|
130
|
+
*/
|
|
131
|
+
async function collectSearchRoots(projectRoot: string): Promise<string[]> {
|
|
132
|
+
const searchRoots = [projectRoot];
|
|
133
|
+
|
|
134
|
+
const workspaceYamlPath = path.join(projectRoot, "pnpm-workspace.yaml");
|
|
135
|
+
try {
|
|
136
|
+
const yamlContent = await fs.promises.readFile(workspaceYamlPath, "utf-8");
|
|
137
|
+
const workspaceGlobs = parseWorkspaceGlobs(yamlContent);
|
|
138
|
+
|
|
139
|
+
for (const pattern of workspaceGlobs) {
|
|
140
|
+
const dirs = await glob(pattern, { cwd: projectRoot, absolute: true });
|
|
141
|
+
searchRoots.push(...dirs);
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
// If pnpm-workspace.yaml doesn't exist, only process root
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return searchRoots;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Resolve all replacement target items from replaceDeps config.
|
|
152
|
+
*
|
|
153
|
+
* 1. Parse pnpm-workspace.yaml → workspace package paths
|
|
154
|
+
* 2. Find matching packages in [root, ...workspace packages] node_modules
|
|
155
|
+
* 3. Pattern matching + verify source path exists + resolve symlinks
|
|
156
|
+
*
|
|
157
|
+
* @param projectRoot - Project root path
|
|
158
|
+
* @param replaceDeps - replaceDeps config from sd.config.ts
|
|
159
|
+
* @param logger - consola logger
|
|
160
|
+
* @returns Array of resolved replacement target items
|
|
161
|
+
*/
|
|
162
|
+
async function resolveAllReplaceDepEntries(
|
|
163
|
+
projectRoot: string,
|
|
164
|
+
replaceDeps: Record<string, string>,
|
|
165
|
+
logger: ReturnType<typeof consola.withTag>,
|
|
166
|
+
): Promise<ReplaceDepEntry[]> {
|
|
167
|
+
const entries: ReplaceDepEntry[] = [];
|
|
168
|
+
|
|
169
|
+
const searchRoots = await collectSearchRoots(projectRoot);
|
|
170
|
+
|
|
171
|
+
for (const searchRoot of searchRoots) {
|
|
172
|
+
const nodeModulesDir = path.join(searchRoot, "node_modules");
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
await fs.promises.access(nodeModulesDir);
|
|
176
|
+
} catch {
|
|
177
|
+
continue; // Skip if node_modules doesn't exist
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Search node_modules directories using each glob pattern from replaceDeps
|
|
181
|
+
const targetNames: string[] = [];
|
|
182
|
+
for (const pattern of Object.keys(replaceDeps)) {
|
|
183
|
+
const matches = await glob(pattern, { cwd: nodeModulesDir });
|
|
184
|
+
targetNames.push(...matches);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (targetNames.length === 0) continue;
|
|
188
|
+
|
|
189
|
+
// Pattern matching and path resolution
|
|
190
|
+
const matchedEntries = resolveReplaceDepEntries(replaceDeps, targetNames);
|
|
191
|
+
|
|
192
|
+
for (const { targetName, sourcePath } of matchedEntries) {
|
|
193
|
+
const targetPath = path.join(nodeModulesDir, targetName);
|
|
194
|
+
const resolvedSourcePath = path.resolve(projectRoot, sourcePath);
|
|
195
|
+
|
|
196
|
+
// Verify source path exists
|
|
197
|
+
try {
|
|
198
|
+
await fs.promises.access(resolvedSourcePath);
|
|
199
|
+
} catch {
|
|
200
|
+
logger.warn(`Source path does not exist, skipping: ${resolvedSourcePath}`);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// If targetPath is symlink, resolve to get actual .pnpm store path
|
|
205
|
+
let actualTargetPath = targetPath;
|
|
206
|
+
try {
|
|
207
|
+
const stat = await fs.promises.lstat(targetPath);
|
|
208
|
+
if (stat.isSymbolicLink()) {
|
|
209
|
+
actualTargetPath = await fs.promises.realpath(targetPath);
|
|
210
|
+
}
|
|
211
|
+
} catch {
|
|
212
|
+
// If targetPath doesn't exist, use as-is
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
entries.push({
|
|
216
|
+
targetName,
|
|
217
|
+
sourcePath,
|
|
218
|
+
targetPath,
|
|
219
|
+
resolvedSourcePath,
|
|
220
|
+
actualTargetPath,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return entries;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Replace packages in node_modules with source directories according to replaceDeps config.
|
|
230
|
+
*
|
|
231
|
+
* 1. Parse pnpm-workspace.yaml → workspace package paths
|
|
232
|
+
* 2. Find matching packages in [root, ...workspace packages] node_modules
|
|
233
|
+
* 3. Remove existing symlinks/directories → copy source path (excluding node_modules, package.json, .cache, tests)
|
|
234
|
+
*
|
|
235
|
+
* @param projectRoot - Project root path
|
|
236
|
+
* @param replaceDeps - replaceDeps config from sd.config.ts
|
|
237
|
+
*/
|
|
238
|
+
export async function setupReplaceDeps(
|
|
239
|
+
projectRoot: string,
|
|
240
|
+
replaceDeps: Record<string, string>,
|
|
241
|
+
): Promise<void> {
|
|
242
|
+
const logger = consola.withTag("sd:cli:replace-deps");
|
|
243
|
+
let setupCount = 0;
|
|
244
|
+
|
|
245
|
+
logger.start("Setting up replace-deps");
|
|
246
|
+
|
|
247
|
+
const entries = await resolveAllReplaceDepEntries(projectRoot, replaceDeps, logger);
|
|
248
|
+
|
|
249
|
+
for (const { targetName, resolvedSourcePath, actualTargetPath } of entries) {
|
|
250
|
+
try {
|
|
251
|
+
// Overwrite-copy source files to actualTargetPath (maintain existing directory, preserve symlinks)
|
|
252
|
+
await fsCopy(resolvedSourcePath, actualTargetPath, replaceDepsCopyFilter);
|
|
253
|
+
|
|
254
|
+
setupCount += 1;
|
|
255
|
+
} catch (err) {
|
|
256
|
+
logger.error(`Copy replace failed (${targetName}): ${err instanceof Error ? err.message : err}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
logger.success(`Replaced ${setupCount} dependencies`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Watch source directories according to replaceDeps config and copy changes to target paths.
|
|
265
|
+
*
|
|
266
|
+
* 1. Parse pnpm-workspace.yaml → workspace package paths
|
|
267
|
+
* 2. Find matching packages in [root, ...workspace packages] node_modules
|
|
268
|
+
* 3. Watch source directories with FsWatcher (300ms delay)
|
|
269
|
+
* 4. Copy changes to target paths (excluding node_modules, package.json, .cache, tests)
|
|
270
|
+
*
|
|
271
|
+
* @param projectRoot - Project root path
|
|
272
|
+
* @param replaceDeps - replaceDeps config from sd.config.ts
|
|
273
|
+
* @returns entries and dispose function
|
|
274
|
+
*/
|
|
275
|
+
export async function watchReplaceDeps(
|
|
276
|
+
projectRoot: string,
|
|
277
|
+
replaceDeps: Record<string, string>,
|
|
278
|
+
): Promise<WatchReplaceDepResult> {
|
|
279
|
+
const logger = consola.withTag("sd:cli:replace-deps:watch");
|
|
280
|
+
|
|
281
|
+
const entries = await resolveAllReplaceDepEntries(projectRoot, replaceDeps, logger);
|
|
282
|
+
|
|
283
|
+
// Setup source directory watchers
|
|
284
|
+
const watchers: FsWatcher[] = [];
|
|
285
|
+
const watchedSources = new Set<string>();
|
|
286
|
+
|
|
287
|
+
logger.start(`Watching ${entries.length} replace-deps target(s)`);
|
|
288
|
+
|
|
289
|
+
for (const entry of entries) {
|
|
290
|
+
if (watchedSources.has(entry.resolvedSourcePath)) continue;
|
|
291
|
+
watchedSources.add(entry.resolvedSourcePath);
|
|
292
|
+
|
|
293
|
+
const excludedPaths = [...EXCLUDED_NAMES].map((name) =>
|
|
294
|
+
path.join(entry.resolvedSourcePath, name),
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
const watcher = await FsWatcher.watch([entry.resolvedSourcePath], { followSymlinks: false });
|
|
298
|
+
watcher.onChange({ delay: 300 }, async (changeInfos) => {
|
|
299
|
+
for (const { path: changedPath } of changeInfos) {
|
|
300
|
+
// Filter excluded items: basename match or path within excluded directory
|
|
301
|
+
if (
|
|
302
|
+
EXCLUDED_NAMES.has(path.basename(changedPath)) ||
|
|
303
|
+
excludedPaths.some((ep) => pathIsChildPath(changedPath, ep))
|
|
304
|
+
) {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Copy for all entries using this source path
|
|
309
|
+
for (const e of entries) {
|
|
310
|
+
if (e.resolvedSourcePath !== entry.resolvedSourcePath) continue;
|
|
311
|
+
|
|
312
|
+
// Calculate relative path from source
|
|
313
|
+
const relativePath = path.relative(e.resolvedSourcePath, changedPath);
|
|
314
|
+
const destPath = path.join(e.actualTargetPath, relativePath);
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
// Check if source exists
|
|
318
|
+
let sourceExists = false;
|
|
319
|
+
try {
|
|
320
|
+
await fs.promises.access(changedPath);
|
|
321
|
+
sourceExists = true;
|
|
322
|
+
} catch {
|
|
323
|
+
// Source was deleted
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (sourceExists) {
|
|
327
|
+
// Check if source is directory or file
|
|
328
|
+
const stat = await fs.promises.stat(changedPath);
|
|
329
|
+
if (stat.isDirectory()) {
|
|
330
|
+
await fsMkdir(destPath);
|
|
331
|
+
} else {
|
|
332
|
+
await fsMkdir(path.dirname(destPath));
|
|
333
|
+
await fsCopy(changedPath, destPath, replaceDepsCopyFilter);
|
|
334
|
+
}
|
|
335
|
+
} else {
|
|
336
|
+
// Source was deleted → delete target
|
|
337
|
+
await fsRm(destPath);
|
|
338
|
+
}
|
|
339
|
+
} catch (err) {
|
|
340
|
+
logger.error(
|
|
341
|
+
`Copy failed (${e.targetName}/${relativePath}): ${err instanceof Error ? err.message : err}`,
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
watchers.push(watcher);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
logger.success(`Replace-deps watch ready`);
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
entries,
|
|
355
|
+
dispose: () => {
|
|
356
|
+
for (const watcher of watchers) {
|
|
357
|
+
void watcher.close();
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
}
|