@simplysm/sd-cli 13.0.72 → 13.0.75

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.
@@ -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
+ }