@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
package/src/inspect.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { readdir, readFile, stat } from "node:fs/promises";
2
+ import { createRequire } from "node:module";
2
3
  import path from "node:path";
3
4
  import { parseProviderPackageMetadata } from "@beignet/core/providers";
5
+ import semver from "semver";
4
6
  import ts from "typescript";
5
7
  import {
6
8
  type ContractFactoryBindings,
@@ -10,7 +12,23 @@ import {
10
12
  } from "./analysis/contract-factories.js";
11
13
  import { canonicalFeatureLayerPath } from "./analysis/layers.js";
12
14
  import { analyzePortWiringFiles } from "./analysis/port-wiring.js";
13
- import { exportedVariableDeclarations } from "./analysis/source-index.js";
15
+ import {
16
+ analysisSourceFileExtensions,
17
+ isAnalysisSourceFile,
18
+ isAnalysisSourceFileExtension,
19
+ isJavaScriptSourceFile,
20
+ javascriptAnalysisSourceFileExtensions,
21
+ sourceFileScriptKind,
22
+ stripSourceFileExtension,
23
+ typescriptAnalysisSourceFileExtensions,
24
+ } from "./analysis/source-files.js";
25
+ import {
26
+ createSourceIndex,
27
+ exportedVariableDeclarations,
28
+ type SourceIndex,
29
+ } from "./analysis/source-index.js";
30
+ import { createAnalysisWorkspace, projectPath } from "./analysis/workspace.js";
31
+ import { workspaceRouteContracts } from "./analysis/workspace-routes.js";
14
32
  import { createPainter } from "./ansi.js";
15
33
  import type { DatabaseSchemaDialect, DatabaseSchemaTable } from "./choices.js";
16
34
  import {
@@ -86,6 +104,7 @@ import {
86
104
  legacyGeneratedPackageScripts,
87
105
  } from "./templates/shared.js";
88
106
  import { testSupportTemplateFiles } from "./templates/testing.js";
107
+ import { isAppTestFile } from "./test-discovery.js";
89
108
  import { getCliVersion } from "./version.js";
90
109
 
91
110
  type InspectAppOptions = {
@@ -200,6 +219,7 @@ type RouteExport = {
200
219
  handlerFile: string;
201
220
  contractRef?: string;
202
221
  contractFile?: string;
222
+ contractUnresolved?: boolean;
203
223
  catchAllPrefix?: string;
204
224
  source: "next-route" | "route-local";
205
225
  };
@@ -248,19 +268,76 @@ export async function inspectApp(
248
268
  const targetDir = path.resolve(options.cwd ?? process.cwd());
249
269
  await assertDirectory(targetDir);
250
270
 
251
- const files = await listFiles(targetDir);
252
- const config = options.config
253
- ? resolveConfig(options.config)
254
- : await loadBeignetConfig(targetDir, files);
255
- const convention = inspectConvention(files, config);
271
+ const workspace = await createAnalysisWorkspace(targetDir, {
272
+ config: options.config,
273
+ });
274
+ const { config } = workspace;
275
+ const files = workspace.projects[0].files;
256
276
  const inspectedContracts = await readContracts(targetDir, files, config);
277
+ for (const project of workspace.projects.slice(1)) {
278
+ const shared = await readContracts(
279
+ project.targetDir,
280
+ project.files,
281
+ project.config,
282
+ );
283
+ inspectedContracts.contracts.push(
284
+ ...shared.contracts.map((contract) => ({
285
+ ...contract,
286
+ file: projectPath(project, contract.file),
287
+ })),
288
+ );
289
+ inspectedContracts.queryTransportDiagnostics.push(
290
+ ...shared.queryTransportDiagnostics.map((diagnostic) => ({
291
+ ...diagnostic,
292
+ ...(diagnostic.file
293
+ ? { file: projectPath(project, diagnostic.file) }
294
+ : {}),
295
+ ...(diagnostic.subject
296
+ ? {
297
+ subject: {
298
+ ...diagnostic.subject,
299
+ file: projectPath(project, diagnostic.subject.file),
300
+ },
301
+ }
302
+ : {}),
303
+ })),
304
+ );
305
+ }
257
306
  const { contracts } = inspectedContracts;
307
+ contracts.sort(compareContracts);
308
+ const convention = inspectConvention(
309
+ files,
310
+ config,
311
+ contracts.length > 0 && workspace.projects.length > 1,
312
+ );
258
313
  const matchedRoutes = await inspectRouteSurface(
259
314
  targetDir,
260
315
  files,
261
316
  config,
262
317
  contracts,
263
- );
318
+ workspace.projects.length > 1 ? createSourceIndex(workspace) : undefined,
319
+ );
320
+ let workspaceRoutesUnresolved = false;
321
+ if (workspace.projects.length > 1) {
322
+ const registration = await workspaceRouteContracts(workspace);
323
+ workspaceRoutesUnresolved = registration.unresolved;
324
+ matchedRoutes.routes = matchedRoutes.routes.map((route) => {
325
+ if (route.handlerSource === "route-local") return route;
326
+ if (!registration.contracts.has(`${route.file}#${route.exportName}`))
327
+ return {
328
+ ...route,
329
+ handlerFile: undefined,
330
+ handlerSource: "missing" as const,
331
+ };
332
+ return config.framework === "web"
333
+ ? {
334
+ ...route,
335
+ handlerFile: config.paths.server,
336
+ handlerSource: "web-server" as const,
337
+ }
338
+ : route;
339
+ });
340
+ }
264
341
  const diagnostics = await inspectDiagnostics(
265
342
  targetDir,
266
343
  files,
@@ -272,6 +349,15 @@ export async function inspectApp(
272
349
  inspectedContracts.queryTransportDiagnostics,
273
350
  );
274
351
 
352
+ if (workspaceRoutesUnresolved)
353
+ diagnostics.push({
354
+ severity: "warning",
355
+ code: "BEIGNET_WORKSPACE_ROUTES_UNRESOLVED",
356
+ file: config.paths.server,
357
+ message:
358
+ "Could not fully resolve workspace route composition. Use explicit defineRoutes arrays and defineRouteGroup bindings reachable from the server routes option.",
359
+ });
360
+
275
361
  return {
276
362
  schemaVersion: 1,
277
363
  targetDir,
@@ -354,11 +440,15 @@ async function buildDoctorFixPlan(
354
440
  : await loadBeignetConfig(targetDir, files);
355
441
  const convention = inspectConvention(files, config);
356
442
  const { contracts } = await readContracts(targetDir, files, config);
443
+ const sourceIndex = config.workspace.packages.length
444
+ ? createSourceIndex(await createAnalysisWorkspace(targetDir, { config }))
445
+ : undefined;
357
446
  const matchedRoutes = await inspectRouteSurface(
358
447
  targetDir,
359
448
  files,
360
449
  config,
361
450
  contracts,
451
+ sourceIndex,
362
452
  );
363
453
  const operations: PlannedDoctorFixOperation[] = [];
364
454
 
@@ -883,14 +973,15 @@ async function readContracts(
883
973
  function inspectConvention(
884
974
  files: string[],
885
975
  config: ResolvedBeignetConfig,
976
+ sharedContracts = false,
886
977
  ): InspectConvention {
887
978
  const missingNextLayout = missingRequirements(files, [
888
- `${directoryPath(config.paths.contracts)}/`,
979
+ ...(sharedContracts ? [] : [`${directoryPath(config.paths.contracts)}/`]),
889
980
  `${directoryPath(config.paths.routes)}/`,
890
981
  config.paths.server,
891
982
  ]);
892
983
  const missingWebLayout = missingRequirements(files, [
893
- `${directoryPath(config.paths.contracts)}/`,
984
+ ...(sharedContracts ? [] : [`${directoryPath(config.paths.contracts)}/`]),
894
985
  config.paths.server,
895
986
  ]);
896
987
  const missingResourceGenerator = missingRequirements(files, [
@@ -946,7 +1037,7 @@ function parseContracts(source: string, file: string): ParsedContract[] {
946
1037
  source,
947
1038
  ts.ScriptTarget.Latest,
948
1039
  true,
949
- ts.ScriptKind.TS,
1040
+ sourceFileScriptKind(file),
950
1041
  );
951
1042
  const assignments = variableAssignments(sourceFile);
952
1043
  const assignmentsByName = new Map(
@@ -1556,34 +1647,53 @@ function normalizeContractPath(routePath: string): string {
1556
1647
  async function readRouteExports(
1557
1648
  targetDir: string,
1558
1649
  routeFiles: string[],
1650
+ files: string[],
1559
1651
  config: ResolvedBeignetConfig,
1652
+ sourceIndex?: SourceIndex,
1560
1653
  ): Promise<Map<string, RouteExport[]>> {
1561
1654
  const routeExports = new Map<string, RouteExport[]>();
1562
1655
 
1563
1656
  for (const file of routeFiles) {
1564
1657
  const routePath = nextRoutePath(file, config.paths.routes);
1565
1658
  const source = await readFile(path.join(targetDir, file), "utf8");
1566
- routeExports.set(file, parseRouteExports(source, file, routePath, config));
1659
+ routeExports.set(
1660
+ file,
1661
+ await parseRouteExports(
1662
+ source,
1663
+ file,
1664
+ routePath,
1665
+ files,
1666
+ config,
1667
+ sourceIndex,
1668
+ ),
1669
+ );
1567
1670
  }
1568
1671
 
1569
1672
  return routeExports;
1570
1673
  }
1571
1674
 
1572
- function parseRouteExports(
1675
+ async function parseRouteExports(
1573
1676
  source: string,
1574
1677
  handlerFile: string,
1575
1678
  routePath: string,
1679
+ files: string[],
1576
1680
  config: ResolvedBeignetConfig,
1577
- ): RouteExport[] {
1681
+ sourceIndex?: SourceIndex,
1682
+ ): Promise<RouteExport[]> {
1578
1683
  const exports: RouteExport[] = [];
1579
1684
  const sourceFile = ts.createSourceFile(
1580
1685
  handlerFile,
1581
1686
  source,
1582
1687
  ts.ScriptTarget.Latest,
1583
1688
  true,
1584
- ts.ScriptKind.TS,
1689
+ sourceFileScriptKind(handlerFile),
1690
+ );
1691
+ const imports = parseRouteNamedImports(
1692
+ sourceFile,
1693
+ config,
1694
+ handlerFile,
1695
+ files,
1585
1696
  );
1586
- const imports = parseRouteNamedImports(sourceFile, config, handlerFile);
1587
1697
 
1588
1698
  for (const statement of sourceFile.statements) {
1589
1699
  if (
@@ -1628,11 +1738,17 @@ function parseRouteExports(
1628
1738
 
1629
1739
  if (localName) {
1630
1740
  const imported = imports.get(localName);
1741
+ const resolved = await sourceIndex?.resolveValue(
1742
+ handlerFile,
1743
+ ts.factory.createIdentifier(localName),
1744
+ );
1631
1745
  exports.push({
1632
1746
  method,
1633
1747
  handlerFile,
1634
- contractRef: imported?.importedName ?? localName,
1635
- contractFile: imported?.contractFile,
1748
+ contractRef:
1749
+ resolved?.exportName ?? imported?.importedName ?? localName,
1750
+ contractFile: resolved?.file ?? imported?.contractFile,
1751
+ ...(sourceIndex && !resolved ? { contractUnresolved: true } : {}),
1636
1752
  source: "route-local",
1637
1753
  });
1638
1754
  continue;
@@ -1656,6 +1772,7 @@ function parseRouteNamedImports(
1656
1772
  sourceFile: ts.SourceFile,
1657
1773
  config: ResolvedBeignetConfig,
1658
1774
  handlerFile: string,
1775
+ files: string[],
1659
1776
  ): Map<string, { importedName: string; contractFile?: string }> {
1660
1777
  const imports = new Map<
1661
1778
  string,
@@ -1677,6 +1794,7 @@ function parseRouteNamedImports(
1677
1794
  statement.moduleSpecifier.text,
1678
1795
  config,
1679
1796
  handlerFile,
1797
+ files,
1680
1798
  );
1681
1799
  for (const element of statement.importClause.namedBindings.elements) {
1682
1800
  if (element.isTypeOnly) continue;
@@ -1757,6 +1875,7 @@ function parseNamedImports(
1757
1875
  source: string,
1758
1876
  config: ResolvedBeignetConfig,
1759
1877
  importerFile?: string,
1878
+ files?: string[],
1760
1879
  ): Map<
1761
1880
  string,
1762
1881
  {
@@ -1779,6 +1898,7 @@ function parseNamedImports(
1779
1898
  sourcePath,
1780
1899
  config,
1781
1900
  importerFile,
1901
+ files,
1782
1902
  );
1783
1903
  for (const member of match[1].split(",")) {
1784
1904
  const parsed = parseImportMember(member);
@@ -1849,9 +1969,7 @@ function sourceFileFromImport(
1849
1969
  files?: string[],
1850
1970
  ): string | undefined {
1851
1971
  const resolveCandidate = (candidate: string) => {
1852
- if (path.extname(candidate)) return candidate;
1853
-
1854
- const candidates = [`${candidate}.ts`, `${candidate}/index.ts`];
1972
+ const candidates = sourceFileSpecifierCandidates(candidate, importerFile);
1855
1973
  return files
1856
1974
  ? candidates.find((file) => files.includes(file))
1857
1975
  : candidates[0];
@@ -1870,33 +1988,76 @@ function sourceFileFromImport(
1870
1988
  return undefined;
1871
1989
  }
1872
1990
 
1991
+ function sourceFileCandidates(base: string, importerFile?: string): string[] {
1992
+ const extensions =
1993
+ importerFile && isJavaScriptSourceFile(importerFile)
1994
+ ? [
1995
+ ...javascriptAnalysisSourceFileExtensions,
1996
+ ...typescriptAnalysisSourceFileExtensions,
1997
+ ]
1998
+ : analysisSourceFileExtensions;
1999
+ return [
2000
+ ...extensions.map((extension) => `${base}${extension}`),
2001
+ ...extensions.map((extension) => `${base}/index${extension}`),
2002
+ ];
2003
+ }
2004
+
2005
+ function sourceFileSpecifierCandidates(
2006
+ candidate: string,
2007
+ importerFile?: string,
2008
+ ): string[] {
2009
+ const extension = path.posix.extname(candidate).toLowerCase();
2010
+ if (!extension) return sourceFileCandidates(candidate, importerFile);
2011
+ if (!isAnalysisSourceFileExtension(extension)) return [candidate];
2012
+
2013
+ const sourceExtensions =
2014
+ extension === ".js"
2015
+ ? [".ts", ".tsx"]
2016
+ : extension === ".jsx"
2017
+ ? [".tsx"]
2018
+ : extension === ".mjs"
2019
+ ? [".mts"]
2020
+ : extension === ".cjs"
2021
+ ? [".cts"]
2022
+ : [];
2023
+ const sourceCandidates = sourceExtensions.map(
2024
+ (sourceExtension) =>
2025
+ `${candidate.slice(0, -extension.length)}${sourceExtension}`,
2026
+ );
2027
+ const directCandidates =
2028
+ importerFile && isJavaScriptSourceFile(importerFile)
2029
+ ? [candidate, ...sourceCandidates]
2030
+ : [...sourceCandidates, candidate];
2031
+ return [
2032
+ ...new Set([
2033
+ ...directCandidates,
2034
+ ...directCandidates.map(directoryIndexSourceFile),
2035
+ ]),
2036
+ ];
2037
+ }
2038
+
2039
+ function isSourceFileNamed(file: string, name: string): boolean {
2040
+ return (
2041
+ isAnalysisSourceFile(file) &&
2042
+ path.posix.basename(stripSourceFileExtension(file)) === name
2043
+ );
2044
+ }
2045
+
2046
+ function directoryIndexSourceFile(file: string): string {
2047
+ const extension = path.posix.extname(file);
2048
+ return `${stripSourceFileExtension(file)}/index${extension}`;
2049
+ }
2050
+
1873
2051
  function contractFileFromImport(
1874
2052
  sourcePath: string,
1875
2053
  config: ResolvedBeignetConfig,
1876
2054
  importerFile?: string,
2055
+ files?: string[],
1877
2056
  ): string | undefined {
1878
2057
  const resolveCandidate = (candidate: string) => {
1879
- const extension = path.extname(candidate).toLowerCase();
1880
- if (!extension) return `${candidate}.ts`;
1881
-
1882
- let sourceExtension: string | undefined;
1883
- switch (extension) {
1884
- case ".js":
1885
- sourceExtension = ".ts";
1886
- break;
1887
- case ".jsx":
1888
- sourceExtension = ".tsx";
1889
- break;
1890
- case ".mjs":
1891
- sourceExtension = ".mts";
1892
- break;
1893
- case ".cjs":
1894
- sourceExtension = ".cts";
1895
- break;
1896
- }
1897
- return sourceExtension
1898
- ? `${candidate.slice(0, -extension.length)}${sourceExtension}`
1899
- : candidate;
2058
+ const candidates = sourceFileSpecifierCandidates(candidate, importerFile);
2059
+ if (!files) return candidates[0];
2060
+ return candidates.find((file) => files.includes(file)) ?? candidates[0];
1900
2061
  };
1901
2062
  const contractsPath = directoryPath(config.paths.contracts);
1902
2063
  const aliasPrefix = `@/${contractsPath}/`;
@@ -1905,7 +2066,7 @@ function contractFileFromImport(
1905
2066
  const relativeExact = contractsPath;
1906
2067
 
1907
2068
  if (sourcePath === aliasExact || sourcePath === relativeExact) {
1908
- return `${contractsPath}/index.ts`;
2069
+ return resolveCandidate(`${contractsPath}/index`);
1909
2070
  }
1910
2071
  if (sourcePath.startsWith(aliasPrefix)) {
1911
2072
  return resolveCandidate(sourcePath.slice("@/".length));
@@ -1997,14 +2158,21 @@ async function inspectRouteSurface(
1997
2158
  files: string[],
1998
2159
  config: ResolvedBeignetConfig,
1999
2160
  contracts: InspectedContract[],
2161
+ sourceIndex?: SourceIndex,
2000
2162
  ): Promise<MatchedRoutesResult> {
2001
2163
  if (config.framework === "next") {
2002
2164
  const routeFiles = files.filter(
2003
2165
  (file) =>
2004
2166
  file.startsWith(`${directoryPath(config.paths.routes)}/`) &&
2005
- file.endsWith("/route.ts"),
2167
+ isNextRouteFile(file, config),
2168
+ );
2169
+ const routeExports = await readRouteExports(
2170
+ targetDir,
2171
+ routeFiles,
2172
+ files,
2173
+ config,
2174
+ sourceIndex,
2006
2175
  );
2007
- const routeExports = await readRouteExports(targetDir, routeFiles, config);
2008
2176
  return matchRoutes(contracts, routeExports);
2009
2177
  }
2010
2178
 
@@ -2071,13 +2239,13 @@ function findContracts(
2071
2239
  routeExport: RouteExport,
2072
2240
  ): InspectedContract[] {
2073
2241
  if (routeExport.source === "route-local") {
2242
+ if (routeExport.contractUnresolved) return [];
2074
2243
  const contract = contracts.find(
2075
2244
  (contract) =>
2076
2245
  contract.exportName === routeExport.contractRef &&
2077
2246
  (!routeExport.contractFile ||
2078
2247
  contract.file === routeExport.contractFile ||
2079
- contract.file ===
2080
- routeExport.contractFile.replace(/\.ts$/, "/index.ts")),
2248
+ contract.file === directoryIndexSourceFile(routeExport.contractFile)),
2081
2249
  );
2082
2250
  return contract ? [contract] : [];
2083
2251
  }
@@ -2319,6 +2487,7 @@ async function inspectCanonicalConformance(
2319
2487
  files.some((file) => file.startsWith(`${clientDir}/`)) ||
2320
2488
  [
2321
2489
  "@beignet/react-query",
2490
+ "@beignet/react-form",
2322
2491
  "@beignet/react-hook-form",
2323
2492
  "@beignet/react-uploads",
2324
2493
  "@beignet/nuqs",
@@ -2365,12 +2534,13 @@ async function inspectCanonicalConformance(
2365
2534
  }
2366
2535
 
2367
2536
  if (
2537
+ installedPackages.has("@beignet/react-form") ||
2368
2538
  installedPackages.has("@beignet/react-hook-form") ||
2369
2539
  files.includes(clientForms)
2370
2540
  ) {
2371
2541
  requiredFiles.push({
2372
2542
  file: clientForms,
2373
- reason: `${clientForms} should own React Hook Form adapter setup when form helpers are used.`,
2543
+ reason: `${clientForms} should own form adapter setup (createReactForm or createReactHookForm) when form helpers are used.`,
2374
2544
  });
2375
2545
  }
2376
2546
 
@@ -2521,7 +2691,12 @@ async function inspectCanonicalConformance(
2521
2691
 
2522
2692
  const featureRoutesPath = `${featuresPath}/`;
2523
2693
  for (const file of files) {
2524
- if (!file.startsWith(featureRoutesPath) || !file.endsWith("/routes.ts")) {
2694
+ if (
2695
+ !file.startsWith(featureRoutesPath) ||
2696
+ !isAnalysisSourceFile(file) ||
2697
+ isAppTestFile(file) ||
2698
+ !stripSourceFileExtension(file).endsWith("/routes")
2699
+ ) {
2525
2700
  continue;
2526
2701
  }
2527
2702
 
@@ -2910,7 +3085,7 @@ async function inspectProductionReadiness(
2910
3085
  let hasSecurityHeadersHook = false;
2911
3086
 
2912
3087
  for (const file of files) {
2913
- if (!file.endsWith(".ts") || file.endsWith(".test.ts")) continue;
3088
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file)) continue;
2914
3089
 
2915
3090
  const source = await readCachedSource(targetDir, file, sourceCache);
2916
3091
  const routeServerOrInfra = isRouteServerOrInfraFile(file, config);
@@ -2937,7 +3112,7 @@ async function inspectProductionReadiness(
2937
3112
 
2938
3113
  if (
2939
3114
  fileWithinDirectory(file, cronDirectory) &&
2940
- file.endsWith("/route.ts") &&
3115
+ isNextRouteFile(file, config) &&
2941
3116
  !/\bCRON_SECRET\b/.test(source) &&
2942
3117
  !/\bcreateOutboxDrainRoute\s*\(/.test(source) &&
2943
3118
  !/\bcreateScheduleRoute\s*\(/.test(source)
@@ -3239,6 +3414,25 @@ async function inspectProductionReadiness(
3239
3414
  "@beignet/provider-auth-better-auth is installed, but BETTER_AUTH_TRUSTED_ORIGINS is not mentioned in app env/config files. Add it before deploying across multiple origins or document why the app is single-origin only.",
3240
3415
  });
3241
3416
  }
3417
+
3418
+ if (
3419
+ (await betterAuthRequiresIssuerSchema(targetDir, packageJson)) &&
3420
+ (await anyFileContains(
3421
+ targetDir,
3422
+ configFiles,
3423
+ "drizzleAdapter",
3424
+ sourceCache,
3425
+ )) &&
3426
+ !(await hasBetterAuthIssuerSchema(targetDir, files, sourceCache))
3427
+ ) {
3428
+ diagnostics.push({
3429
+ severity: "warning",
3430
+ code: "BEIGNET_BETTER_AUTH_ISSUER_SCHEMA_MISSING",
3431
+ file: schemaDir,
3432
+ message:
3433
+ "This app uses Better Auth 1.7 or a version range that can install it with the Drizzle adapter, but its account schema does not include the required issuer field and unique (issuer, accountId) index. Complete the provider-specific account identity backfill, run bunx auth generate, and apply the reviewed migration before deploying Better Auth 1.7.",
3434
+ });
3435
+ }
3242
3436
  }
3243
3437
 
3244
3438
  const installedReadinessProviders = [...installedPackages].filter(
@@ -3284,7 +3478,7 @@ async function uploadHasExplicitSizeLimit(
3284
3478
  );
3285
3479
  if (!importedName) continue;
3286
3480
 
3287
- const importedFile = resolveLocalTypeScriptModule(file, moduleName, files);
3481
+ const importedFile = sourceFileFromImport(moduleName, file, files);
3288
3482
  if (!importedFile) continue;
3289
3483
 
3290
3484
  const importedSource = await readCachedSource(
@@ -3339,20 +3533,6 @@ function exportedObjectHasProperty(
3339
3533
  return new RegExp(`\\b${escapeRegExp(propertyName)}\\s*:`).test(initializer);
3340
3534
  }
3341
3535
 
3342
- function resolveLocalTypeScriptModule(
3343
- importer: string,
3344
- moduleName: string,
3345
- files: string[],
3346
- ): string | undefined {
3347
- const base = normalizePath(
3348
- path.posix.normalize(
3349
- path.posix.join(path.posix.dirname(importer), moduleName),
3350
- ),
3351
- );
3352
- const candidates = [base, `${base}.ts`, `${base}.tsx`, `${base}/index.ts`];
3353
- return candidates.find((candidate) => files.includes(candidate));
3354
- }
3355
-
3356
3536
  async function hasReadinessCheck(
3357
3537
  targetDir: string,
3358
3538
  files: string[],
@@ -3360,10 +3540,18 @@ async function hasReadinessCheck(
3360
3540
  sourceCache: Map<string, string>,
3361
3541
  ): Promise<boolean> {
3362
3542
  const routesDir = directoryPath(config.paths.routes);
3363
- if (files.includes(`${routesDir}/ready/route.ts`)) return true;
3543
+ if (
3544
+ files.some(
3545
+ (file) =>
3546
+ path.posix.dirname(file) === `${routesDir}/ready` &&
3547
+ isNextRouteFile(file, config),
3548
+ )
3549
+ ) {
3550
+ return true;
3551
+ }
3364
3552
 
3365
3553
  for (const file of files) {
3366
- if (!file.endsWith(".ts") || file.endsWith(".test.ts")) continue;
3554
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file)) continue;
3367
3555
  if (!isRouteServerOrInfraFile(file, config)) continue;
3368
3556
 
3369
3557
  const source = await readCachedSource(targetDir, file, sourceCache);
@@ -3905,10 +4093,11 @@ function isFeatureUploadDefinition(
3905
4093
  file: string,
3906
4094
  config: ResolvedBeignetConfig,
3907
4095
  ): boolean {
3908
- const featuresPath = directoryPath(config.paths.features);
3909
- return new RegExp(
3910
- `^${escapeRegExp(featuresPath)}/[^/]+/uploads/[^/]+\\.ts$`,
3911
- ).test(file);
4096
+ return isFeatureArtifactFile(
4097
+ file,
4098
+ directoryPath(config.paths.features),
4099
+ "uploads",
4100
+ );
3912
4101
  }
3913
4102
 
3914
4103
  function operationalConfigFiles(
@@ -3918,17 +4107,263 @@ function operationalConfigFiles(
3918
4107
  return providerOperationalConfigFiles(files, config);
3919
4108
  }
3920
4109
 
3921
- async function readPackageJson(
4110
+ type InspectedPackageJson = {
4111
+ dependencies?: Record<string, string>;
4112
+ devDependencies?: Record<string, string>;
4113
+ peerDependencies?: Record<string, string>;
4114
+ };
4115
+
4116
+ function declaredBetterAuthMayRequireIssuerSchema(
4117
+ packageJson: InspectedPackageJson | undefined,
4118
+ ): boolean {
4119
+ const version =
4120
+ packageJson?.dependencies?.["better-auth"] ??
4121
+ packageJson?.devDependencies?.["better-auth"] ??
4122
+ packageJson?.peerDependencies?.["better-auth"];
4123
+ if (!version) return false;
4124
+
4125
+ const normalized = version.trim();
4126
+ const workspaceRange = normalized.startsWith("workspace:")
4127
+ ? normalized.slice("workspace:".length)
4128
+ : normalized;
4129
+ const npmAlias = /^npm:better-auth@(.+)$/.exec(workspaceRange);
4130
+ const candidate = npmAlias?.[1] ?? workspaceRange;
4131
+ const range = semver.validRange(candidate);
4132
+
4133
+ // Tags, catalogs, local paths, and other non-semver declarations can resolve
4134
+ // to 1.7. Prefer a migration warning until an installed version is readable.
4135
+ if (!range) return true;
4136
+
4137
+ return semver.intersects(range, ">=1.7.0");
4138
+ }
4139
+
4140
+ async function betterAuthRequiresIssuerSchema(
4141
+ targetDir: string,
4142
+ packageJson: InspectedPackageJson | undefined,
4143
+ ): Promise<boolean> {
4144
+ const installed = await readInstalledBetterAuthPackageJson(targetDir);
4145
+ if (installed?.version) {
4146
+ const version = semver.valid(installed.version.trim());
4147
+ if (version) return semver.gte(version, "1.7.0");
4148
+ }
4149
+
4150
+ return declaredBetterAuthMayRequireIssuerSchema(packageJson);
4151
+ }
4152
+
4153
+ async function readInstalledBetterAuthPackageJson(
4154
+ targetDir: string,
4155
+ ): Promise<{ name?: string; version?: string } | undefined> {
4156
+ const nodeModulesPackage =
4157
+ await readBetterAuthPackageFromNodeModules(targetDir);
4158
+ if (nodeModulesPackage) return nodeModulesPackage;
4159
+
4160
+ const requireFromTarget = createRequire(path.join(targetDir, "package.json"));
4161
+ const packageJsonPath = resolveModuleFromTarget(
4162
+ requireFromTarget,
4163
+ "better-auth/package.json",
4164
+ );
4165
+ if (packageJsonPath) {
4166
+ return readJsonIfExists(packageJsonPath);
4167
+ }
4168
+
4169
+ const entryPath = resolveModuleFromTarget(requireFromTarget, "better-auth");
4170
+ if (!entryPath) return undefined;
4171
+
4172
+ let directory = path.dirname(entryPath);
4173
+ while (true) {
4174
+ const candidate = await readJsonIfExists<{
4175
+ name?: string;
4176
+ version?: string;
4177
+ }>(path.join(directory, "package.json"));
4178
+ if (candidate?.name === "better-auth") return candidate;
4179
+
4180
+ const parent = path.dirname(directory);
4181
+ if (parent === directory) return undefined;
4182
+ directory = parent;
4183
+ }
4184
+ }
4185
+
4186
+ async function readBetterAuthPackageFromNodeModules(
4187
+ targetDir: string,
4188
+ ): Promise<{ name?: string; version?: string } | undefined> {
4189
+ let directory = targetDir;
4190
+ while (true) {
4191
+ const candidate = await readJsonIfExists<{
4192
+ name?: string;
4193
+ version?: string;
4194
+ }>(path.join(directory, "node_modules", "better-auth", "package.json"));
4195
+ if (candidate?.name === "better-auth") return candidate;
4196
+
4197
+ const parent = path.dirname(directory);
4198
+ if (parent === directory) return undefined;
4199
+ directory = parent;
4200
+ }
4201
+ }
4202
+
4203
+ function resolveModuleFromTarget(
4204
+ requireFromTarget: ReturnType<typeof createRequire>,
4205
+ request: string,
4206
+ ): string | undefined {
4207
+ try {
4208
+ return requireFromTarget.resolve(request);
4209
+ } catch {
4210
+ return undefined;
4211
+ }
4212
+ }
4213
+
4214
+ async function hasBetterAuthIssuerSchema(
3922
4215
  targetDir: string,
3923
4216
  files: string[],
3924
- ): Promise<
3925
- | {
3926
- dependencies?: Record<string, string>;
3927
- devDependencies?: Record<string, string>;
3928
- peerDependencies?: Record<string, string>;
4217
+ sourceCache: Map<string, string>,
4218
+ ): Promise<boolean> {
4219
+ const schemaFiles = files.filter(
4220
+ (file) =>
4221
+ !isTestSourceFile(file) &&
4222
+ /\.(?:ts|tsx|js|jsx|mts|mjs|cts|cjs)$/.test(file),
4223
+ );
4224
+
4225
+ for (const file of schemaFiles) {
4226
+ const source = await readCachedSource(targetDir, file, sourceCache);
4227
+ if (sourceHasBetterAuthIssuerSchema(file, source)) return true;
4228
+ }
4229
+
4230
+ return false;
4231
+ }
4232
+
4233
+ function sourceHasBetterAuthIssuerSchema(
4234
+ file: string,
4235
+ source: string,
4236
+ ): boolean {
4237
+ const sourceFile = ts.createSourceFile(
4238
+ file,
4239
+ source,
4240
+ ts.ScriptTarget.Latest,
4241
+ true,
4242
+ );
4243
+ let found = false;
4244
+ const visit = (node: ts.Node): void => {
4245
+ if (found) return;
4246
+ if (
4247
+ ts.isCallExpression(node) &&
4248
+ isBetterAuthAccountTableCall(node) &&
4249
+ betterAuthAccountTableHasIssuerIdentity(node)
4250
+ ) {
4251
+ found = true;
4252
+ return;
3929
4253
  }
3930
- | undefined
3931
- > {
4254
+ ts.forEachChild(node, visit);
4255
+ };
4256
+ visit(sourceFile);
4257
+ return found;
4258
+ }
4259
+
4260
+ function isBetterAuthAccountTableCall(call: ts.CallExpression): boolean {
4261
+ const callee = unwrapContractExpression(call.expression);
4262
+ const factoryName = ts.isIdentifier(callee)
4263
+ ? callee.text
4264
+ : ts.isPropertyAccessExpression(callee)
4265
+ ? callee.name.text
4266
+ : undefined;
4267
+ return (
4268
+ factoryName !== undefined &&
4269
+ ["mysqlTable", "pgTable", "sqliteTable"].includes(factoryName) &&
4270
+ staticString(call.arguments[0]) === "account"
4271
+ );
4272
+ }
4273
+
4274
+ function betterAuthAccountTableHasIssuerIdentity(
4275
+ call: ts.CallExpression,
4276
+ ): boolean {
4277
+ const columns = call.arguments[1];
4278
+ if (!columns || !ts.isObjectLiteralExpression(columns)) return false;
4279
+ const issuer = columns.properties.find(
4280
+ (property) =>
4281
+ ts.isPropertyAssignment(property) &&
4282
+ staticPropertyName(property.name) === "issuer",
4283
+ );
4284
+ if (!issuer || !ts.isPropertyAssignment(issuer)) return false;
4285
+ if (!isBetterAuthIssuerColumn(issuer.initializer)) return false;
4286
+
4287
+ const constraints = call.arguments[2];
4288
+ if (
4289
+ !constraints ||
4290
+ (!ts.isArrowFunction(constraints) &&
4291
+ !ts.isFunctionExpression(constraints)) ||
4292
+ constraints.parameters.length === 0 ||
4293
+ !ts.isIdentifier(constraints.parameters[0]?.name)
4294
+ ) {
4295
+ return false;
4296
+ }
4297
+
4298
+ const tableName = constraints.parameters[0].name.text;
4299
+ let found = false;
4300
+ const visit = (node: ts.Node): void => {
4301
+ if (found) return;
4302
+ if (
4303
+ ts.isCallExpression(node) &&
4304
+ isBetterAuthIssuerAccountUniqueConstraint(node, tableName)
4305
+ ) {
4306
+ found = true;
4307
+ return;
4308
+ }
4309
+ ts.forEachChild(node, visit);
4310
+ };
4311
+ visit(constraints.body);
4312
+ return found;
4313
+ }
4314
+
4315
+ function isBetterAuthIssuerColumn(expression: ts.Expression): boolean {
4316
+ let current = unwrapContractExpression(expression);
4317
+ while (ts.isCallExpression(current)) {
4318
+ const callee = unwrapContractExpression(current.expression);
4319
+ if (
4320
+ ts.isIdentifier(callee) &&
4321
+ ["text", "varchar"].includes(callee.text) &&
4322
+ staticString(current.arguments[0]) === "issuer"
4323
+ ) {
4324
+ return true;
4325
+ }
4326
+ if (!ts.isPropertyAccessExpression(callee)) return false;
4327
+ current = unwrapContractExpression(callee.expression);
4328
+ }
4329
+ return false;
4330
+ }
4331
+
4332
+ function isBetterAuthIssuerAccountUniqueConstraint(
4333
+ call: ts.CallExpression,
4334
+ tableName: string,
4335
+ ): boolean {
4336
+ const callee = unwrapContractExpression(call.expression);
4337
+ if (!ts.isPropertyAccessExpression(callee) || callee.name.text !== "on") {
4338
+ return false;
4339
+ }
4340
+
4341
+ const factoryCall = unwrapContractExpression(callee.expression);
4342
+ if (!ts.isCallExpression(factoryCall)) return false;
4343
+ const factory = unwrapContractExpression(factoryCall.expression);
4344
+ if (
4345
+ !ts.isIdentifier(factory) ||
4346
+ !["unique", "uniqueIndex"].includes(factory.text)
4347
+ ) {
4348
+ return false;
4349
+ }
4350
+
4351
+ if (call.arguments.length !== 2) return false;
4352
+ const columnNames = call.arguments.map((argument) => {
4353
+ const column = unwrapContractExpression(argument);
4354
+ return ts.isPropertyAccessExpression(column) &&
4355
+ ts.isIdentifier(column.expression) &&
4356
+ column.expression.text === tableName
4357
+ ? column.name.text
4358
+ : undefined;
4359
+ });
4360
+ return columnNames.includes("issuer") && columnNames.includes("accountId");
4361
+ }
4362
+
4363
+ async function readPackageJson(
4364
+ targetDir: string,
4365
+ files: string[],
4366
+ ): Promise<InspectedPackageJson | undefined> {
3932
4367
  if (!files.includes("package.json")) return undefined;
3933
4368
 
3934
4369
  return JSON.parse(
@@ -4089,7 +4524,7 @@ async function hasBillingEntitlementsProviderWiring(
4089
4524
  const serverPath = path.dirname(config.paths.server);
4090
4525
  const candidateFiles = files.filter(
4091
4526
  (file) =>
4092
- /\.(?:ts|tsx|mts|cts)$/.test(file) &&
4527
+ isAnalysisSourceFile(file) &&
4093
4528
  !isTestSourceFile(file) &&
4094
4529
  (file.startsWith(`${infraPath}/`) || file.startsWith(`${serverPath}/`)),
4095
4530
  );
@@ -4105,10 +4540,7 @@ async function hasBillingEntitlementsProviderWiring(
4105
4540
  }
4106
4541
 
4107
4542
  function isTestSourceFile(file: string): boolean {
4108
- return (
4109
- /(?:^|\/)tests\//.test(file) ||
4110
- /\.(?:test|spec)\.(?:ts|tsx|mts|cts)$/.test(file)
4111
- );
4543
+ return /(?:^|\/)tests\//.test(file) || isAppTestFile(file);
4112
4544
  }
4113
4545
 
4114
4546
  function exportedConstInitializer(
@@ -4135,8 +4567,8 @@ function featureNameFromContractFile(
4135
4567
  ): string | undefined {
4136
4568
  const featuresPath = directoryPath(config.paths.features);
4137
4569
  const match = new RegExp(
4138
- `^${escapeRegExp(featuresPath)}/([^/]+)/contracts\\.ts$`,
4139
- ).exec(file);
4570
+ `^${escapeRegExp(featuresPath)}/([^/]+)/contracts$`,
4571
+ ).exec(stripSourceFileExtension(file));
4140
4572
  return match?.[1];
4141
4573
  }
4142
4574
 
@@ -4153,7 +4585,7 @@ function featureRuntimeFiles(
4153
4585
  "use-cases",
4154
4586
  ];
4155
4587
  return files.filter((file) => {
4156
- if (!file.endsWith(".ts") || file.endsWith(".test.ts")) return false;
4588
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file)) return false;
4157
4589
  return runtimeFolders.some((folder) =>
4158
4590
  file.startsWith(`${featuresPath}/${feature}/${folder}/`),
4159
4591
  );
@@ -4169,11 +4601,16 @@ function featureTestFiles(
4169
4601
  return files.filter((file) => {
4170
4602
  if (
4171
4603
  !file.startsWith(`${featuresPath}/${feature}/`) ||
4172
- !file.endsWith(".test.ts")
4604
+ !isAppTestFile(file)
4173
4605
  ) {
4174
4606
  return false;
4175
4607
  }
4176
- return !options.policyOnly || file.endsWith("/policy.test.ts");
4608
+ return (
4609
+ !options.policyOnly ||
4610
+ ["policy.test", "policy.spec"].includes(
4611
+ path.posix.basename(stripSourceFileExtension(file)),
4612
+ )
4613
+ );
4177
4614
  });
4178
4615
  }
4179
4616
 
@@ -4340,17 +4777,12 @@ async function inspectFeatureArtifactDrift(
4340
4777
  severity: "warning",
4341
4778
  code: "BEIGNET_FEATURE_TEST_PLACEMENT",
4342
4779
  file,
4343
- message: `${file} places a test directly under ${featuresPath}/${match[1]}/. Move feature behavior tests into ${featuresPath}/${match[1]}/tests/ so feature suites stay in the canonical folder; adjacent *.test.ts files are for infra and server modules.`,
4780
+ message: `${file} places a test directly under ${featuresPath}/${match[1]}/. Move feature behavior tests into ${featuresPath}/${match[1]}/tests/ so feature suites stay in the canonical folder; adjacent test files are for infra and server modules.`,
4344
4781
  });
4345
4782
  }
4346
4783
 
4347
4784
  for (const file of files) {
4348
- if (
4349
- !/\.(?:ts|tsx|mts|cts)$/.test(file) ||
4350
- /\.d\.(?:ts|mts|cts)$/.test(file)
4351
- ) {
4352
- continue;
4353
- }
4785
+ if (!isAnalysisSourceFile(file)) continue;
4354
4786
 
4355
4787
  const misplaced = directFeatureTestMatch(file, featuresPath)
4356
4788
  ? undefined
@@ -4365,7 +4797,6 @@ async function inspectFeatureArtifactDrift(
4365
4797
  }
4366
4798
 
4367
4799
  if (isTestSourceFile(file)) continue;
4368
- if (!file.endsWith(".ts")) continue;
4369
4800
 
4370
4801
  if (
4371
4802
  isNotificationDefinitionFile(file, featuresPath) ||
@@ -4391,9 +4822,10 @@ function directFeatureTestMatch(
4391
4822
  file: string,
4392
4823
  featuresPath: string,
4393
4824
  ): RegExpExecArray | null {
4825
+ if (!isAppTestFile(file)) return null;
4394
4826
  return new RegExp(
4395
- `^${escapeRegExp(featuresPath)}/([^/]+)/[^/]+\\.(?:test|spec)\\.(?:ts|tsx|mts|cts)$`,
4396
- ).exec(file);
4827
+ `^${escapeRegExp(featuresPath)}/([^/]+)/[^/]+\\.(?:test|spec)$`,
4828
+ ).exec(stripSourceFileExtension(file));
4397
4829
  }
4398
4830
 
4399
4831
  function isFeatureArtifactFile(
@@ -4401,9 +4833,10 @@ function isFeatureArtifactFile(
4401
4833
  featuresPath: string,
4402
4834
  folder: "uploads" | "notifications",
4403
4835
  ): boolean {
4836
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file)) return false;
4404
4837
  const match = new RegExp(
4405
- `^${escapeRegExp(featuresPath)}/[^/]+/${folder}/([^/]+)\\.ts$`,
4406
- ).exec(file);
4838
+ `^${escapeRegExp(featuresPath)}/[^/]+/${folder}/([^/]+)$`,
4839
+ ).exec(stripSourceFileExtension(file));
4407
4840
  return Boolean(match && match[1] !== "index");
4408
4841
  }
4409
4842
 
@@ -4413,7 +4846,7 @@ async function appHasUploadRoute(
4413
4846
  config: ResolvedBeignetConfig,
4414
4847
  ): Promise<boolean> {
4415
4848
  for (const file of files) {
4416
- if (!file.endsWith(".ts") || file.endsWith(".test.ts")) continue;
4849
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file)) continue;
4417
4850
  if (!isRouteServerOrInfraFile(file, config)) continue;
4418
4851
 
4419
4852
  const source = await readFile(path.join(targetDir, file), "utf8");
@@ -4967,11 +5400,17 @@ async function readFeatureWorkflowRegistries(
4967
5400
  for (const kind of kinds) {
4968
5401
  const folder = kind === "events" ? "domain/events" : kind;
4969
5402
  const pattern = new RegExp(
4970
- `^${escapeRegExp(featuresPath)}/[^/]+/${escapeRegExp(folder)}/index\\.ts$`,
5403
+ `^${escapeRegExp(featuresPath)}/[^/]+/${escapeRegExp(folder)}/index$`,
4971
5404
  );
4972
5405
 
4973
5406
  for (const file of files) {
4974
- if (!pattern.test(file)) continue;
5407
+ if (
5408
+ !isAnalysisSourceFile(file) ||
5409
+ isWorkflowTestFile(file) ||
5410
+ !pattern.test(stripSourceFileExtension(file))
5411
+ ) {
5412
+ continue;
5413
+ }
4975
5414
 
4976
5415
  const source = await readFile(path.join(targetDir, file), "utf8");
4977
5416
  const memberFiles = exportedMemberFiles(source, file, files);
@@ -5031,8 +5470,7 @@ async function readRuntimeManifestIdentifiers(
5031
5470
  const manifests: RuntimeManifestIdentifierSets[] = [];
5032
5471
 
5033
5472
  for (const file of files) {
5034
- if (!file.endsWith(".ts") && !file.endsWith(".tsx")) continue;
5035
- if (file.endsWith(".d.ts") || isWorkflowTestFile(file)) continue;
5473
+ if (!isAnalysisSourceFile(file) || isWorkflowTestFile(file)) continue;
5036
5474
 
5037
5475
  const source = await readFile(path.join(targetDir, file), "utf8");
5038
5476
  if (!source.includes("defineRuntimeManifest")) continue;
@@ -5169,12 +5607,18 @@ async function listenedEventNames(
5169
5607
  ): Promise<Set<string>> {
5170
5608
  const featuresPath = directoryPath(config.paths.features);
5171
5609
  const listenerPattern = new RegExp(
5172
- `^${escapeRegExp(featuresPath)}/[^/]+/listeners/[^/]+\\.ts$`,
5610
+ `^${escapeRegExp(featuresPath)}/[^/]+/listeners/[^/]+$`,
5173
5611
  );
5174
5612
  const listened = new Set<string>();
5175
5613
 
5176
5614
  for (const file of files) {
5177
- if (!listenerPattern.test(file) || isWorkflowTestFile(file)) continue;
5615
+ if (
5616
+ !isAnalysisSourceFile(file) ||
5617
+ isWorkflowTestFile(file) ||
5618
+ !listenerPattern.test(stripSourceFileExtension(file))
5619
+ ) {
5620
+ continue;
5621
+ }
5178
5622
 
5179
5623
  const source = await readFile(path.join(targetDir, file), "utf8");
5180
5624
  const callPattern = /\bdefineListener\s*\(/g;
@@ -5223,8 +5667,7 @@ async function listenerWiringReferences(
5223
5667
  const listenerRegistryFile = listenersFilePath(config);
5224
5668
 
5225
5669
  for (const file of files) {
5226
- if (!file.endsWith(".ts") && !file.endsWith(".tsx")) continue;
5227
- if (file.endsWith(".d.ts") || isWorkflowTestFile(file)) continue;
5670
+ if (!isAnalysisSourceFile(file) || isWorkflowTestFile(file)) continue;
5228
5671
 
5229
5672
  const source = await readFile(path.join(targetDir, file), "utf8");
5230
5673
  const namedImports = parseNamedImportSources(source);
@@ -5233,7 +5676,7 @@ async function listenerWiringReferences(
5233
5676
  source,
5234
5677
  ts.ScriptTarget.Latest,
5235
5678
  true,
5236
- file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
5679
+ sourceFileScriptKind(file),
5237
5680
  );
5238
5681
  const registerListenersBindings = importedRegisterListenersBindings(
5239
5682
  source,
@@ -5693,10 +6136,7 @@ function callReferencesCentralListenerRegistry(args: {
5693
6136
 
5694
6137
  function isWorkflowTestFile(file: string): boolean {
5695
6138
  return (
5696
- file.endsWith(".test.ts") ||
5697
- file.endsWith(".test.tsx") ||
5698
- file.startsWith("tests/") ||
5699
- file.includes("/tests/")
6139
+ isAppTestFile(file) || file.startsWith("tests/") || file.includes("/tests/")
5700
6140
  );
5701
6141
  }
5702
6142
 
@@ -5711,13 +6151,7 @@ async function inspectServerlessFootguns(
5711
6151
  const cronDirectory = path.posix.join(routeDirectory, "cron");
5712
6152
 
5713
6153
  for (const file of files) {
5714
- if (
5715
- !file.endsWith(".ts") ||
5716
- file.endsWith(".test.ts") ||
5717
- file.endsWith(".d.ts")
5718
- ) {
5719
- continue;
5720
- }
6154
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file)) continue;
5721
6155
 
5722
6156
  const source = await readFile(path.join(targetDir, file), "utf8");
5723
6157
  const isInfraOrServerFile = isRuntimeFile(file, config);
@@ -5863,9 +6297,10 @@ function fileWithinDirectory(file: string, directory: string): boolean {
5863
6297
  }
5864
6298
 
5865
6299
  function isProviderLifecycleFile(file: string, source?: string): boolean {
6300
+ const sourcePath = stripSourceFileExtension(file);
5866
6301
  return (
5867
- /(^|\/)provider(s)?\.ts$/.test(file) ||
5868
- /(^|\/)[^/]*provider[^/]*\.ts$/.test(file) ||
6302
+ /(^|\/)provider(s)?$/.test(sourcePath) ||
6303
+ /(^|\/)[^/]*provider[^/]*$/.test(sourcePath) ||
5869
6304
  /(^|\/)providers?\//.test(file) ||
5870
6305
  (source !== undefined && containsCallExpression(source, "createProvider"))
5871
6306
  );
@@ -5876,8 +6311,9 @@ function isNextRouteFile(file: string, config: ResolvedBeignetConfig): boolean {
5876
6311
 
5877
6312
  return (
5878
6313
  file.startsWith(`${routesPath}/`) &&
5879
- file.endsWith("/route.ts") &&
5880
- !file.endsWith(".test.ts")
6314
+ isSourceFileNamed(file, "route") &&
6315
+ [".js", ".jsx", ".ts", ".tsx"].includes(path.posix.extname(file)) &&
6316
+ !isAppTestFile(file)
5881
6317
  );
5882
6318
  }
5883
6319
 
@@ -6000,6 +6436,10 @@ async function inspectPortProviderDrift(
6000
6436
  const portsSource = files.includes(config.paths.ports)
6001
6437
  ? await readFile(path.join(targetDir, config.paths.ports), "utf8")
6002
6438
  : "";
6439
+ const sharedPorts =
6440
+ config.workspace.packages.length > 0
6441
+ ? await analyzePortWiringFiles({ targetDir, files, config })
6442
+ : undefined;
6003
6443
  const packageJson = files.includes("package.json")
6004
6444
  ? await readPackageJson(targetDir, files)
6005
6445
  : undefined;
@@ -6023,7 +6463,12 @@ async function inspectPortProviderDrift(
6023
6463
 
6024
6464
  for (const expected of providerPortRequirements) {
6025
6465
  if (!installedPackages.has(expected.packageName)) continue;
6026
- if (portsSource.includes(`${expected.portName}:`)) continue;
6466
+ if (
6467
+ sharedPorts
6468
+ ? sharedPorts.declared.has(expected.portName)
6469
+ : portsSource.includes(`${expected.portName}:`)
6470
+ )
6471
+ continue;
6027
6472
  diagnostics.push({
6028
6473
  severity: "warning",
6029
6474
  code: "BEIGNET_PROVIDER_PORT_MISSING",
@@ -7812,7 +8257,13 @@ async function inspectFeatureRouteRegistration(
7812
8257
  const feature = featureNameFromContractFile(contract.file, config);
7813
8258
  if (!feature) continue;
7814
8259
 
7815
- const routeFile = `${featuresPath}/${feature}/routes.ts`;
8260
+ const routeStem = `${featuresPath}/${feature}/routes`;
8261
+ const routeFile =
8262
+ files.find(
8263
+ (file) =>
8264
+ isAnalysisSourceFile(file) &&
8265
+ stripSourceFileExtension(file) === routeStem,
8266
+ ) ?? `${routeStem}${path.posix.extname(contract.file) || ".ts"}`;
7816
8267
  const featureRouteGroups = routeGroups.filter(
7817
8268
  (routeGroup) => routeGroup.file === routeFile,
7818
8269
  );
@@ -7902,13 +8353,14 @@ async function readFeatureRouteGroups(
7902
8353
  ): Promise<FeatureRouteGroup[]> {
7903
8354
  const featuresPath = `${directoryPath(config.paths.features)}/`;
7904
8355
  const routeFiles = files.filter(
7905
- (file) => file.startsWith(featuresPath) && file.endsWith("/routes.ts"),
8356
+ (file) =>
8357
+ file.startsWith(featuresPath) && isSourceFileNamed(file, "routes"),
7906
8358
  );
7907
8359
  const routeGroups: FeatureRouteGroup[] = [];
7908
8360
 
7909
8361
  for (const file of routeFiles) {
7910
8362
  const source = await readFile(path.join(targetDir, file), "utf8");
7911
- const imports = parseNamedImports(source, config, file);
8363
+ const imports = parseNamedImports(source, config, file, files);
7912
8364
  routeGroups.push(...parseFeatureRouteGroups(source, file, imports));
7913
8365
  }
7914
8366
 
@@ -8071,8 +8523,7 @@ function findRouteGroupContract(
8071
8523
  contract.exportName === routeGroupContract.exportName &&
8072
8524
  (!routeGroupContract.file ||
8073
8525
  contract.file === routeGroupContract.file ||
8074
- contract.file ===
8075
- routeGroupContract.file.replace(/\.ts$/, "/index.ts")),
8526
+ contract.file === directoryIndexSourceFile(routeGroupContract.file)),
8076
8527
  );
8077
8528
  }
8078
8529
 
@@ -9449,7 +9900,7 @@ async function readContractLists(
9449
9900
 
9450
9901
  for (const file of contractFiles) {
9451
9902
  const source = await readFile(path.join(targetDir, file), "utf8");
9452
- const imports = parseNamedImports(source, config, file);
9903
+ const imports = parseNamedImports(source, config, file, files);
9453
9904
  for (const list of parseContractLists(source, file, imports)) {
9454
9905
  lists.set(contractListKey(list.file, list.name), list);
9455
9906
  }
@@ -9466,18 +9917,18 @@ function contractSourceFiles(
9466
9917
  if (usesFeatureOwnedContracts(config)) {
9467
9918
  return files.filter(
9468
9919
  (file) =>
9469
- new RegExp(
9470
- `^${escapeRegExp(contractsPath)}/[^/]+/contracts\\.ts$`,
9471
- ).test(file) && !file.endsWith(".test.ts"),
9920
+ isAnalysisSourceFile(file) &&
9921
+ !isAppTestFile(file) &&
9922
+ new RegExp(`^${escapeRegExp(contractsPath)}/[^/]+/contracts$`).test(
9923
+ stripSourceFileExtension(file),
9924
+ ),
9472
9925
  );
9473
9926
  }
9474
9927
 
9475
- return files.filter(
9476
- (file) =>
9477
- file.startsWith(`${contractsPath}/`) &&
9478
- file.endsWith(".ts") &&
9479
- !file.endsWith(".test.ts"),
9480
- );
9928
+ return files.filter((file) => {
9929
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file)) return false;
9930
+ return stripSourceFileExtension(file).startsWith(`${contractsPath}/`);
9931
+ });
9481
9932
  }
9482
9933
 
9483
9934
  function contractListSourceFiles(
@@ -9487,22 +9938,23 @@ function contractListSourceFiles(
9487
9938
  const contractsPath = directoryPath(config.paths.contracts);
9488
9939
  if (usesFeatureOwnedContracts(config)) {
9489
9940
  return files.filter((file) => {
9490
- if (!file.endsWith(".ts") || file.endsWith(".test.ts")) return false;
9491
- if (file === `${contractsPath}/index.ts`) return true;
9492
- if (file === `${contractsPath}/contracts.ts`) return true;
9941
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file)) return false;
9942
+ const sourcePath = stripSourceFileExtension(file);
9943
+ if (sourcePath === `${contractsPath}/index`) return true;
9944
+ if (sourcePath === `${contractsPath}/contracts`) return true;
9493
9945
  return new RegExp(
9494
- `^${escapeRegExp(contractsPath)}/[^/]+/contracts\\.ts$`,
9495
- ).test(file);
9946
+ `^${escapeRegExp(contractsPath)}/[^/]+/contracts$`,
9947
+ ).test(sourcePath);
9496
9948
  });
9497
9949
  }
9498
9950
 
9499
- return files.filter(
9500
- (file) =>
9501
- (file === `${contractsPath}.ts` ||
9502
- file.startsWith(`${contractsPath}/`)) &&
9503
- file.endsWith(".ts") &&
9504
- !file.endsWith(".test.ts"),
9505
- );
9951
+ return files.filter((file) => {
9952
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file)) return false;
9953
+ const sourcePath = stripSourceFileExtension(file);
9954
+ return (
9955
+ sourcePath === contractsPath || sourcePath.startsWith(`${contractsPath}/`)
9956
+ );
9957
+ });
9506
9958
  }
9507
9959
 
9508
9960
  function parseContractLists(
@@ -9556,7 +10008,7 @@ function resolveContractList(
9556
10008
  if (seen.has(key)) return new Set();
9557
10009
  const list =
9558
10010
  lists.get(key) ??
9559
- lists.get(contractListKey(file.replace(/\.ts$/, "/index.ts"), name));
10011
+ lists.get(contractListKey(directoryIndexSourceFile(file), name));
9560
10012
  if (!list) return undefined;
9561
10013
 
9562
10014
  seen.add(key);
@@ -10146,7 +10598,7 @@ function nextRoutePath(file: string, routeRoot: string): string {
10146
10598
  const normalizedRouteRoot = directoryPath(routeRoot);
10147
10599
  const routeSegments = file
10148
10600
  .slice(normalizedRouteRoot.length + 1)
10149
- .replace(/\/route\.ts$/, "")
10601
+ .replace(/\/route\.(?:js|jsx|ts|tsx)$/, "")
10150
10602
  .split("/")
10151
10603
  .filter(Boolean);
10152
10604
  const apiPath = routeSegments.map(nextSegmentToContractSegment).join("/");