@beignet/cli 0.0.51 → 0.0.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +182 -19
  3. package/dist/analysis/layers.d.ts +2 -0
  4. package/dist/analysis/layers.d.ts.map +1 -1
  5. package/dist/analysis/layers.js +9 -2
  6. package/dist/analysis/layers.js.map +1 -1
  7. package/dist/analysis/port-wiring.d.ts +4 -2
  8. package/dist/analysis/port-wiring.d.ts.map +1 -1
  9. package/dist/analysis/port-wiring.js +41 -11
  10. package/dist/analysis/port-wiring.js.map +1 -1
  11. package/dist/analysis/source-files.d.ts +15 -0
  12. package/dist/analysis/source-files.d.ts.map +1 -0
  13. package/dist/analysis/source-files.js +52 -0
  14. package/dist/analysis/source-files.js.map +1 -0
  15. package/dist/analysis/source-index.d.ts +2 -1
  16. package/dist/analysis/source-index.d.ts.map +1 -1
  17. package/dist/analysis/source-index.js +34 -5
  18. package/dist/analysis/source-index.js.map +1 -1
  19. package/dist/analysis/workspace-routes.d.ts +8 -0
  20. package/dist/analysis/workspace-routes.d.ts.map +1 -0
  21. package/dist/analysis/workspace-routes.js +291 -0
  22. package/dist/analysis/workspace-routes.js.map +1 -0
  23. package/dist/analysis/workspace.d.ts +24 -3
  24. package/dist/analysis/workspace.d.ts.map +1 -1
  25. package/dist/analysis/workspace.js +184 -43
  26. package/dist/analysis/workspace.js.map +1 -1
  27. package/dist/app-map-changes.d.ts.map +1 -1
  28. package/dist/app-map-changes.js +24 -14
  29. package/dist/app-map-changes.js.map +1 -1
  30. package/dist/app-map.d.ts.map +1 -1
  31. package/dist/app-map.js +87 -81
  32. package/dist/app-map.js.map +1 -1
  33. package/dist/check.d.ts +12 -0
  34. package/dist/check.d.ts.map +1 -1
  35. package/dist/check.js +92 -25
  36. package/dist/check.js.map +1 -1
  37. package/dist/config.d.ts +7 -0
  38. package/dist/config.d.ts.map +1 -1
  39. package/dist/config.js +19 -0
  40. package/dist/config.js.map +1 -1
  41. package/dist/git-changes.d.ts +17 -1
  42. package/dist/git-changes.d.ts.map +1 -1
  43. package/dist/git-changes.js +118 -84
  44. package/dist/git-changes.js.map +1 -1
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +23 -3
  47. package/dist/index.js.map +1 -1
  48. package/dist/inspect.d.ts.map +1 -1
  49. package/dist/inspect.js +424 -129
  50. package/dist/inspect.js.map +1 -1
  51. package/dist/lint.d.ts.map +1 -1
  52. package/dist/lint.js +108 -43
  53. package/dist/lint.js.map +1 -1
  54. package/dist/make/shared.js +1 -1
  55. package/dist/make.js +18 -1
  56. package/dist/make.js.map +1 -1
  57. package/dist/mcp.js +1 -1
  58. package/dist/mcp.js.map +1 -1
  59. package/dist/provider-add.js +2 -2
  60. package/dist/provider-add.js.map +1 -1
  61. package/dist/provider-audit.d.ts.map +1 -1
  62. package/dist/provider-audit.js +22 -6
  63. package/dist/provider-audit.js.map +1 -1
  64. package/dist/templates/agents.d.ts.map +1 -1
  65. package/dist/templates/agents.js +36 -13
  66. package/dist/templates/agents.js.map +1 -1
  67. package/dist/templates/base.js +5 -5
  68. package/dist/templates/db/mysql.d.ts.map +1 -1
  69. package/dist/templates/db/mysql.js +51 -22
  70. package/dist/templates/db/mysql.js.map +1 -1
  71. package/dist/templates/db/postgres.d.ts.map +1 -1
  72. package/dist/templates/db/postgres.js +60 -20
  73. package/dist/templates/db/postgres.js.map +1 -1
  74. package/dist/templates/db/sqlite.d.ts.map +1 -1
  75. package/dist/templates/db/sqlite.js +51 -23
  76. package/dist/templates/db/sqlite.js.map +1 -1
  77. package/dist/templates/shadcn.js +1 -1
  78. package/dist/templates/shadcn.js.map +1 -1
  79. package/dist/templates/shared.js +6 -6
  80. package/dist/templates/shared.js.map +1 -1
  81. package/dist/templates/testing.d.ts.map +1 -1
  82. package/dist/templates/testing.js +7 -19
  83. package/dist/templates/testing.js.map +1 -1
  84. package/dist/test-discovery.d.ts +9 -0
  85. package/dist/test-discovery.d.ts.map +1 -0
  86. package/dist/test-discovery.js +40 -0
  87. package/dist/test-discovery.js.map +1 -0
  88. package/package.json +8 -5
  89. package/skills/app-structure/SKILL.md +74 -7
  90. package/src/analysis/layers.ts +13 -2
  91. package/src/analysis/port-wiring.ts +68 -11
  92. package/src/analysis/source-files.ts +61 -0
  93. package/src/analysis/source-index.ts +56 -4
  94. package/src/analysis/workspace-routes.ts +385 -0
  95. package/src/analysis/workspace.ts +302 -59
  96. package/src/app-map-changes.ts +41 -13
  97. package/src/app-map.ts +114 -103
  98. package/src/check.ts +115 -25
  99. package/src/config.ts +33 -0
  100. package/src/git-changes.ts +218 -86
  101. package/src/index.ts +33 -5
  102. package/src/inspect.ts +608 -156
  103. package/src/lint.ts +152 -48
  104. package/src/make/shared.ts +1 -1
  105. package/src/make.ts +31 -1
  106. package/src/mcp.ts +1 -1
  107. package/src/provider-add.ts +2 -2
  108. package/src/provider-audit.ts +30 -10
  109. package/src/templates/agents.ts +36 -13
  110. package/src/templates/base.ts +5 -5
  111. package/src/templates/db/mysql.ts +51 -22
  112. package/src/templates/db/postgres.ts +60 -20
  113. package/src/templates/db/sqlite.ts +51 -23
  114. package/src/templates/shadcn.ts +1 -1
  115. package/src/templates/shared.ts +6 -6
  116. package/src/templates/testing.ts +12 -19
  117. package/src/test-discovery.ts +53 -0
@@ -1,4 +1,4 @@
1
- import { readdir, readFile, stat } from "node:fs/promises";
1
+ import { readdir, readFile, realpath, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import ts from "typescript";
4
4
  import {
@@ -8,6 +8,11 @@ import {
8
8
  type ResolvedBeignetConfig,
9
9
  resolveConfig,
10
10
  } from "../config.js";
11
+ import {
12
+ isAnalysisSourceFile,
13
+ isAnalysisSourceFileExtension,
14
+ sourceFileScriptKind,
15
+ } from "./source-files.js";
11
16
 
12
17
  const ignoredDirectoryNames = new Set([
13
18
  ".git",
@@ -18,8 +23,23 @@ const ignoredDirectoryNames = new Set([
18
23
  "node_modules",
19
24
  ]);
20
25
 
26
+ export type AnalysisProject = {
27
+ targetDir: string;
28
+ canonicalDir: string;
29
+ prefix: string;
30
+ name?: string;
31
+ dependencies: Set<string>;
32
+ localDependencies: Set<string>;
33
+ config: ResolvedBeignetConfig;
34
+ files: string[];
35
+ compilerOptions: ts.CompilerOptions;
36
+ compilerConfigFiles: string[];
37
+ moduleResolutionCache: ts.ModuleResolutionCache;
38
+ };
39
+
21
40
  export type AnalysisWorkspace = {
22
41
  targetDir: string;
42
+ projects: AnalysisProject[];
23
43
  files: string[];
24
44
  fileSet: Set<string>;
25
45
  config: ResolvedBeignetConfig;
@@ -40,24 +60,70 @@ export async function createAnalysisWorkspace(
40
60
  ): Promise<AnalysisWorkspace> {
41
61
  const targetDir = path.resolve(cwd);
42
62
  await assertDirectory(targetDir);
43
- const files = await listProjectFiles(targetDir);
44
- const fileSet = new Set(files);
63
+ const appFiles = await listProjectFiles(targetDir);
45
64
  const config = options.config
46
65
  ? resolveConfig(options.config)
47
- : await loadBeignetConfig(targetDir, files);
48
- const { compilerConfigFiles, compilerOptions } =
49
- resolveCompilerConfiguration(targetDir);
50
- const moduleResolutionCache = ts.createModuleResolutionCache(
51
- targetDir,
52
- ts.sys.useCaseSensitiveFileNames
53
- ? (fileName) => fileName
54
- : (fileName) => fileName.toLowerCase(),
55
- compilerOptions,
56
- );
66
+ : await loadBeignetConfig(targetDir, appFiles);
67
+ const app = await createProject(targetDir, targetDir, appFiles, config);
68
+ const projects = [app];
69
+ for (const directory of config.workspace.packages) {
70
+ const packageDir = path.resolve(targetDir, directory);
71
+ await assertDirectory(packageDir);
72
+ const canonical = await realpath(packageDir);
73
+ for (const project of projects) {
74
+ const existing = project.canonicalDir;
75
+ if (within(canonical, existing) || within(existing, canonical)) {
76
+ throw new Error(
77
+ `Workspace package ${directory} overlaps ${project.prefix || "the app"}. Package roots must be separate directories.`,
78
+ );
79
+ }
80
+ }
81
+ const packageFiles = await listProjectFiles(packageDir);
82
+ const packageConfig = await loadBeignetConfig(packageDir, packageFiles);
83
+ const project = await createProject(
84
+ targetDir,
85
+ packageDir,
86
+ packageFiles,
87
+ packageConfig,
88
+ );
89
+ if (!project.name)
90
+ throw new Error(
91
+ `Workspace package ${directory} must have a name in package.json.`,
92
+ );
93
+ if (projects.some((candidate) => candidate.name === project.name)) {
94
+ throw new Error(`Duplicate workspace package name ${project.name}.`);
95
+ }
96
+ projects.push(project);
97
+ }
98
+ const reachable = new Set([app]);
99
+ for (const project of reachable) {
100
+ for (const candidate of projects.slice(1)) {
101
+ if (candidate.name && project.dependencies.has(candidate.name))
102
+ reachable.add(candidate);
103
+ }
104
+ }
105
+ for (const project of projects.slice(1)) {
106
+ if (!reachable.has(project))
107
+ throw new Error(
108
+ `Workspace package ${project.name} is not a declared dependency of the app or its configured source packages.`,
109
+ );
110
+ }
111
+ const files = projects
112
+ .flatMap((project) =>
113
+ project.files.map((file) => projectPath(project, file)),
114
+ )
115
+ .sort();
116
+ const fileSet = new Set(files);
117
+ const { compilerOptions, moduleResolutionCache } = app;
118
+ const compilerConfigFiles = [
119
+ ...new Set(projects.flatMap((project) => project.compilerConfigFiles)),
120
+ ].sort();
57
121
  const sources = new Map<string, string>();
58
122
  const sourceFiles = new Map<string, ts.SourceFile>();
59
123
 
60
124
  const readSource = async (file: string): Promise<string> => {
125
+ if (!fileSet.has(file))
126
+ throw new Error(`Source file is outside the analysis workspace: ${file}`);
61
127
  const cached = sources.get(file);
62
128
  if (cached !== undefined) return cached;
63
129
 
@@ -68,6 +134,7 @@ export async function createAnalysisWorkspace(
68
134
 
69
135
  return {
70
136
  targetDir,
137
+ projects,
71
138
  files,
72
139
  fileSet,
73
140
  config,
@@ -84,7 +151,7 @@ export async function createAnalysisWorkspace(
84
151
  await readSource(file),
85
152
  ts.ScriptTarget.Latest,
86
153
  true,
87
- scriptKind(file),
154
+ sourceFileScriptKind(file),
88
155
  );
89
156
  sourceFiles.set(file, sourceFile);
90
157
  return sourceFile;
@@ -132,7 +199,11 @@ export async function listProjectFiles(targetDir: string): Promise<string[]> {
132
199
  export function resolveLocalSourceFile(
133
200
  workspace: Pick<
134
201
  AnalysisWorkspace,
135
- "compilerOptions" | "fileSet" | "moduleResolutionCache" | "targetDir"
202
+ | "compilerOptions"
203
+ | "fileSet"
204
+ | "moduleResolutionCache"
205
+ | "targetDir"
206
+ | "projects"
136
207
  >,
137
208
  importerFile: string,
138
209
  importPath: string,
@@ -143,14 +214,36 @@ export function resolveLocalSourceFile(
143
214
  const file = normalizePath(
144
215
  path.relative(workspace.targetDir, resolved.resolvedFileName),
145
216
  );
146
- if (file === ".." || file.startsWith("../")) return undefined;
147
- return workspace.fileSet.has(file) ? file : undefined;
217
+ if (
218
+ workspace.fileSet.has(file) &&
219
+ (!projectForFile(workspace, file).prefix || isAnalysisSourceFile(file))
220
+ )
221
+ return file;
222
+ const canonicalFile =
223
+ ts.sys.realpath?.(resolved.resolvedFileName) ?? resolved.resolvedFileName;
224
+ for (const project of workspace.projects) {
225
+ if (!within(canonicalFile, project.canonicalDir)) continue;
226
+ const candidate = projectPath(
227
+ project,
228
+ path.relative(project.canonicalDir, canonicalFile),
229
+ );
230
+ if (
231
+ workspace.fileSet.has(candidate) &&
232
+ (!project.prefix || isAnalysisSourceFile(candidate))
233
+ )
234
+ return candidate;
235
+ }
236
+ return undefined;
148
237
  }
149
238
 
150
239
  export function resolveLocalModulePath(
151
240
  workspace: Pick<
152
241
  AnalysisWorkspace,
153
- "compilerOptions" | "fileSet" | "moduleResolutionCache" | "targetDir"
242
+ | "compilerOptions"
243
+ | "fileSet"
244
+ | "moduleResolutionCache"
245
+ | "targetDir"
246
+ | "projects"
154
247
  >,
155
248
  importerFile: string,
156
249
  importPath: string,
@@ -166,16 +259,34 @@ export function resolveLocalModulePath(
166
259
  return undefined;
167
260
  }
168
261
 
262
+ const project = projectForFile(workspace, importerFile);
263
+ if (
264
+ workspace.projects.length > 1 &&
265
+ (importPath.startsWith(".") ||
266
+ importPath.startsWith("#") ||
267
+ Object.keys(project.compilerOptions.paths ?? {}).some(
268
+ (pattern) =>
269
+ pattern !== "*" && modulePatternMatches(pattern, importPath),
270
+ ))
271
+ ) {
272
+ const resolved = resolveModule(workspace, importerFile, importPath);
273
+ if (resolved)
274
+ return normalizePath(
275
+ path.relative(
276
+ workspace.targetDir,
277
+ ts.sys.realpath?.(resolved.resolvedFileName) ??
278
+ resolved.resolvedFileName,
279
+ ),
280
+ );
281
+ }
282
+
169
283
  if (importPath.startsWith(".")) {
170
284
  return normalizePath(path.join(path.dirname(importerFile), importPath));
171
285
  }
172
286
 
173
- const baseUrl = compilerPathsBase(
174
- workspace.compilerOptions,
175
- workspace.targetDir,
176
- );
287
+ const baseUrl = compilerPathsBase(project.compilerOptions, project.targetDir);
177
288
  for (const [pattern, targets] of Object.entries(
178
- workspace.compilerOptions.paths ?? {},
289
+ project.compilerOptions.paths ?? {},
179
290
  )) {
180
291
  if (pattern === "*") continue;
181
292
  const wildcardValue = modulePatternWildcard(pattern, importPath);
@@ -187,14 +298,22 @@ export function resolveLocalModulePath(
187
298
  const relative = normalizePath(
188
299
  path.relative(workspace.targetDir, absolute),
189
300
  );
190
- if (relative !== ".." && !relative.startsWith("../")) return relative;
301
+ if (
302
+ workspace.projects.length > 1 ||
303
+ workspace.projects.some((candidate) =>
304
+ within(absolute, candidate.targetDir),
305
+ )
306
+ )
307
+ return relative;
191
308
  }
192
309
  }
193
310
 
194
311
  const relative = normalizePath(
195
312
  path.relative(workspace.targetDir, path.resolve(baseUrl, importPath)),
196
313
  );
197
- return relative !== ".." && !relative.startsWith("../")
314
+ return workspace.projects.some((candidate) =>
315
+ within(path.resolve(workspace.targetDir, relative), candidate.targetDir),
316
+ )
198
317
  ? relative
199
318
  : undefined;
200
319
  }
@@ -202,17 +321,20 @@ export function resolveLocalModulePath(
202
321
  export function isLocalModuleSpecifier(
203
322
  workspace: Pick<
204
323
  AnalysisWorkspace,
205
- "compilerOptions" | "fileSet" | "moduleResolutionCache" | "targetDir"
324
+ | "compilerOptions"
325
+ | "fileSet"
326
+ | "moduleResolutionCache"
327
+ | "targetDir"
328
+ | "projects"
206
329
  >,
207
330
  importerFile: string,
208
331
  importPath: string,
209
332
  ): boolean {
210
333
  const extension = path.posix.extname(importPath);
211
334
  if (
335
+ !importPath.startsWith("#") &&
212
336
  extension &&
213
- ![".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"].includes(
214
- extension,
215
- )
337
+ !isAnalysisSourceFileExtension(extension)
216
338
  ) {
217
339
  return false;
218
340
  }
@@ -223,12 +345,33 @@ export function isLocalModuleSpecifier(
223
345
  const file = normalizePath(
224
346
  path.relative(workspace.targetDir, resolved.resolvedFileName),
225
347
  );
226
- return (
227
- file !== ".." && !file.startsWith("../") && workspace.fileSet.has(file)
228
- );
348
+ if (
349
+ workspace.fileSet.has(file) ||
350
+ resolveLocalSourceFile(workspace, importerFile, importPath)
351
+ )
352
+ return true;
353
+ // Package-import aliases can resolve to an omitted workspace through an
354
+ // install symlink. Keep normal dependencies inside node_modules external.
355
+ const canonical =
356
+ ts.sys.realpath?.(resolved.resolvedFileName) ?? resolved.resolvedFileName;
357
+ if (
358
+ importPath.startsWith("#") &&
359
+ !normalizePath(canonical).split("/").includes("node_modules")
360
+ )
361
+ return true;
229
362
  }
230
363
 
231
- const pathPatterns = Object.keys(workspace.compilerOptions.paths ?? {});
364
+ const project = projectForFile(workspace, importerFile);
365
+ if (
366
+ workspace.projects.some(
367
+ (candidate) =>
368
+ candidate.name &&
369
+ (importPath === candidate.name ||
370
+ importPath.startsWith(`${candidate.name}/`)),
371
+ )
372
+ )
373
+ return true;
374
+ const pathPatterns = Object.keys(project.compilerOptions.paths ?? {});
232
375
  if (
233
376
  pathPatterns.some(
234
377
  (pattern) =>
@@ -240,7 +383,7 @@ export function isLocalModuleSpecifier(
240
383
  return true;
241
384
  }
242
385
 
243
- const baseUrl = workspace.compilerOptions.baseUrl;
386
+ const baseUrl = project.compilerOptions.baseUrl;
244
387
  if (!baseUrl) return false;
245
388
  const firstSegment = importPath.split("/")[0];
246
389
  const prefix = normalizePath(
@@ -255,17 +398,18 @@ export function isLocalModuleSpecifier(
255
398
  function resolveModule(
256
399
  workspace: Pick<
257
400
  AnalysisWorkspace,
258
- "compilerOptions" | "moduleResolutionCache" | "targetDir"
401
+ "compilerOptions" | "moduleResolutionCache" | "targetDir" | "projects"
259
402
  >,
260
403
  importerFile: string,
261
404
  importPath: string,
262
405
  ): ts.ResolvedModuleFull | undefined {
406
+ const project = projectForFile(workspace, importerFile);
263
407
  return ts.resolveModuleName(
264
408
  importPath,
265
409
  path.join(workspace.targetDir, importerFile),
266
- workspace.compilerOptions,
410
+ project.compilerOptions,
267
411
  ts.sys,
268
- workspace.moduleResolutionCache,
412
+ project.moduleResolutionCache,
269
413
  ).resolvedModule;
270
414
  }
271
415
 
@@ -298,28 +442,32 @@ function resolveCompilerConfiguration(targetDir: string): {
298
442
  compilerOptions: ts.CompilerOptions;
299
443
  } {
300
444
  const compilerConfigFiles = new Set<string>();
301
- const configPath = path.join(targetDir, "tsconfig.json");
302
- const compilerOptions =
303
- ts.getParsedCommandLineOfConfigFile(
304
- configPath,
305
- {},
306
- {
307
- ...ts.sys,
308
- readFile(file) {
309
- compilerConfigFiles.add(path.resolve(file));
310
- return ts.sys.readFile(file);
445
+ const configPath = ["tsconfig.json", "jsconfig.json"]
446
+ .map((file) => path.join(targetDir, file))
447
+ .find(ts.sys.fileExists);
448
+ const compilerOptions = configPath
449
+ ? (ts.getParsedCommandLineOfConfigFile(
450
+ configPath,
451
+ {},
452
+ {
453
+ ...ts.sys,
454
+ readFile(file) {
455
+ compilerConfigFiles.add(path.resolve(file));
456
+ return ts.sys.readFile(file);
457
+ },
458
+ onUnRecoverableConfigFileDiagnostic: () => {},
311
459
  },
312
- onUnRecoverableConfigFileDiagnostic: () => {},
313
- },
314
- )?.options ?? {};
460
+ )?.options ?? {})
461
+ : {};
462
+ const analysisOptions = { ...compilerOptions, allowJs: true };
315
463
 
316
464
  return {
317
465
  compilerConfigFiles: [...compilerConfigFiles].sort(),
318
- compilerOptions: compilerOptions.paths
319
- ? compilerOptions
466
+ compilerOptions: analysisOptions.paths
467
+ ? analysisOptions
320
468
  : {
321
- ...compilerOptions,
322
- baseUrl: compilerOptions.baseUrl ?? targetDir,
469
+ ...analysisOptions,
470
+ baseUrl: analysisOptions.baseUrl ?? targetDir,
323
471
  paths: {
324
472
  "@/*": ["*"],
325
473
  },
@@ -339,9 +487,104 @@ function compilerPathsBase(
339
487
  );
340
488
  }
341
489
 
342
- function scriptKind(file: string): ts.ScriptKind {
343
- if (file.endsWith(".tsx")) return ts.ScriptKind.TSX;
344
- if (file.endsWith(".jsx")) return ts.ScriptKind.JSX;
345
- if (file.endsWith(".js") || file.endsWith(".mjs")) return ts.ScriptKind.JS;
346
- return ts.ScriptKind.TS;
490
+ async function createProject(
491
+ appDir: string,
492
+ targetDir: string,
493
+ files: string[],
494
+ config: ResolvedBeignetConfig,
495
+ ): Promise<AnalysisProject> {
496
+ const manifest: unknown = files.includes("package.json")
497
+ ? JSON.parse(await readFile(path.join(targetDir, "package.json"), "utf8"))
498
+ : {};
499
+ if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
500
+ throw new Error(`Invalid package.json in ${targetDir}.`);
501
+ }
502
+ const data = manifest as Record<string, unknown>;
503
+ const dependencies = new Set<string>();
504
+ const localDependencies = new Set<string>();
505
+ for (const key of [
506
+ "dependencies",
507
+ "devDependencies",
508
+ "peerDependencies",
509
+ "optionalDependencies",
510
+ ]) {
511
+ const value = data[key];
512
+ if (value && typeof value === "object" && !Array.isArray(value)) {
513
+ for (const [name, specifier] of Object.entries(value)) {
514
+ dependencies.add(name);
515
+ if (
516
+ typeof specifier === "string" &&
517
+ /^(workspace|link|file):/.test(specifier) &&
518
+ !specifier.endsWith(".tgz")
519
+ )
520
+ localDependencies.add(name);
521
+ }
522
+ }
523
+ }
524
+ const { compilerConfigFiles, compilerOptions } =
525
+ resolveCompilerConfiguration(targetDir);
526
+ return {
527
+ targetDir,
528
+ canonicalDir: await realpath(targetDir),
529
+ prefix: normalizePath(path.relative(appDir, targetDir)),
530
+ ...(typeof data.name === "string" && data.name.trim()
531
+ ? { name: data.name }
532
+ : {}),
533
+ dependencies,
534
+ localDependencies,
535
+ config,
536
+ files,
537
+ compilerConfigFiles,
538
+ compilerOptions,
539
+ moduleResolutionCache: ts.createModuleResolutionCache(
540
+ targetDir,
541
+ ts.sys.useCaseSensitiveFileNames
542
+ ? (file) => file
543
+ : (file) => file.toLowerCase(),
544
+ compilerOptions,
545
+ ),
546
+ };
547
+ }
548
+
549
+ export function projectForFile(
550
+ workspace: Pick<AnalysisWorkspace, "projects" | "targetDir">,
551
+ file: string,
552
+ ): AnalysisProject {
553
+ return findProjectForFile(workspace, file) ?? workspace.projects[0];
554
+ }
555
+
556
+ export function findProjectForFile(
557
+ workspace: Pick<AnalysisWorkspace, "projects" | "targetDir">,
558
+ file: string,
559
+ ): AnalysisProject | undefined {
560
+ const absolute = path.resolve(workspace.targetDir, file);
561
+ return workspace.projects.find((project) =>
562
+ within(absolute, project.targetDir),
563
+ );
564
+ }
565
+
566
+ export function projectPath(project: AnalysisProject, file: string): string {
567
+ return normalizePath(path.join(project.prefix, file));
568
+ }
569
+
570
+ export function projectSource(
571
+ workspace: Pick<AnalysisWorkspace, "projects" | "targetDir">,
572
+ file: string,
573
+ ): { project: AnalysisProject; file: string } {
574
+ const project = projectForFile(workspace, file);
575
+ return {
576
+ project,
577
+ file: normalizePath(
578
+ path.relative(project.targetDir, path.resolve(workspace.targetDir, file)),
579
+ ),
580
+ };
581
+ }
582
+
583
+ function within(file: string, directory: string): boolean {
584
+ const relative = path.relative(directory, file);
585
+ return (
586
+ relative !== ".." &&
587
+ !relative.startsWith(`..${path.sep}`) &&
588
+ !path.isAbsolute(relative)
589
+ );
347
590
  }
@@ -1,5 +1,9 @@
1
1
  import { readFile, realpath } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import {
4
+ isAnalysisSourceFile,
5
+ stripSourceFileExtension,
6
+ } from "./analysis/source-files.js";
3
7
  import {
4
8
  type LocalImportReference,
5
9
  localImportReferences,
@@ -212,8 +216,22 @@ export async function mapAppChanges(
212
216
  createAnalysisWorkspace(targetDir),
213
217
  resolveWorkspaceScope(git.repositoryRoot, canonicalTargetDir),
214
218
  ]);
219
+ for (const project of workspace.projects.slice(1)) {
220
+ if (isAbsoluteWithin(project.canonicalDir, git.repositoryRoot)) {
221
+ workspaceScope.dependencyDirectories.add(
222
+ normalizePath(path.relative(git.repositoryRoot, project.canonicalDir)),
223
+ );
224
+ }
225
+ }
215
226
  const governingFiles = new Set(
216
- workspace.compilerConfigFiles
227
+ [
228
+ ...workspace.compilerConfigFiles,
229
+ ...workspace.projects.flatMap((project) =>
230
+ project.config.configFile
231
+ ? [path.join(project.targetDir, project.config.configFile)]
232
+ : [],
233
+ ),
234
+ ]
217
235
  .map((file) =>
218
236
  path.resolve(
219
237
  canonicalTargetDir,
@@ -281,6 +299,11 @@ export async function mapAppChanges(
281
299
  changedFiles,
282
300
  git.repositoryRoot,
283
301
  canonicalTargetDir,
302
+ workspace.projects
303
+ .slice(1)
304
+ .map((project) =>
305
+ normalizePath(path.relative(git.repositoryRoot, project.canonicalDir)),
306
+ ),
284
307
  );
285
308
  for (const change of appSourceChanges) {
286
309
  const directNodes = nodesByFile.get(change.file) ?? [];
@@ -746,6 +769,7 @@ function appRelativeSourceChanges(
746
769
  files: AppChangedFile[],
747
770
  repositoryRoot: string,
748
771
  canonicalTargetDir: string,
772
+ sourcePackages: string[],
749
773
  ): Array<{
750
774
  file: string;
751
775
  repositoryPath: string;
@@ -760,9 +784,15 @@ function appRelativeSourceChanges(
760
784
  status: GitChangedFileStatus;
761
785
  }> = [];
762
786
  for (const changed of files) {
763
- if (changed.scope !== "app") continue;
787
+ if (changed.scope !== "app" && changed.scope !== "workspace-dependency")
788
+ continue;
764
789
  for (const repositoryPath of changedFilePaths(changed)) {
765
- if (!isWithin(repositoryPath, targetPrefix)) continue;
790
+ if (
791
+ ![targetPrefix, ...sourcePackages].some((prefix) =>
792
+ isWithin(repositoryPath, prefix),
793
+ )
794
+ )
795
+ continue;
766
796
  const file = targetPrefix
767
797
  ? normalizePath(path.posix.relative(targetPrefix, repositoryPath))
768
798
  : repositoryPath;
@@ -794,7 +824,7 @@ async function addImportConsumers(args: {
794
824
  reference: LocalImportReference;
795
825
  }>
796
826
  >();
797
- const sourceFiles = args.workspace.files.filter(isSourceFile);
827
+ const sourceFiles = args.workspace.files.filter(isAnalysisSourceFile);
798
828
  await Promise.all(
799
829
  sourceFiles.map(async (file) => {
800
830
  for (const reference of await localImportReferences(
@@ -826,7 +856,9 @@ async function addImportConsumers(args: {
826
856
  );
827
857
  }
828
858
 
829
- for (const change of args.changes.filter((item) => isSourceFile(item.file))) {
859
+ for (const change of args.changes.filter((item) =>
860
+ isAnalysisSourceFile(item.file),
861
+ )) {
830
862
  const queue: Array<{
831
863
  confidence: "exact" | "partial";
832
864
  depth: number;
@@ -1002,7 +1034,7 @@ function addUnmappedChangeGaps(args: {
1002
1034
  const represented = reasons.some(
1003
1035
  (reason) => reason.changedFile === change.repositoryPath,
1004
1036
  );
1005
- const sourceFile = isSourceFile(change.file);
1037
+ const sourceFile = isAnalysisSourceFile(change.file);
1006
1038
  if (sourceFile && change.status === "deleted") {
1007
1039
  args.gaps.push({
1008
1040
  code: "deleted_source_unresolved",
@@ -1289,7 +1321,7 @@ function scopeSort(scope: AppChangeFileScope): number {
1289
1321
  }
1290
1322
 
1291
1323
  function moduleStem(file: string): string {
1292
- return normalizePath(file).replace(/\.(?:c|m)?[jt]sx?$/, "");
1324
+ return stripSourceFileExtension(normalizePath(file));
1293
1325
  }
1294
1326
 
1295
1327
  function resolvedModuleKey(file: string): string {
@@ -1333,10 +1365,6 @@ function highestPriorityScope(
1333
1365
  );
1334
1366
  }
1335
1367
 
1336
- function isSourceFile(file: string): boolean {
1337
- return /\.(?:c|m)?[jt]sx?$/.test(file) && !file.endsWith(".d.ts");
1338
- }
1339
-
1340
1368
  function isDocumentationPath(file: string, roots: string[]): boolean {
1341
1369
  const normalized = normalizePath(file);
1342
1370
  const basename = path.posix.basename(normalized);
@@ -1376,8 +1404,8 @@ function appWideChangeReasons(
1376
1404
  const message =
1377
1405
  scope === "workspace-dependency"
1378
1406
  ? scoped.length === 1
1379
- ? `Direct local dependency ${changedFile} changed.`
1380
- : `${scoped.length} direct local dependency files changed, including ${changedFile}.`
1407
+ ? `Local source dependency ${changedFile} changed.`
1408
+ : `${scoped.length} local source dependency files changed, including ${changedFile}.`
1381
1409
  : scoped.length === 1
1382
1410
  ? `Governing file ${changedFile} changed.`
1383
1411
  : `${scoped.length} governing files changed, including ${changedFile}.`;