@beignet/cli 0.0.50 → 0.0.52

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 (105) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +64 -22
  3. package/dist/analysis/layers.d.ts.map +1 -1
  4. package/dist/analysis/layers.js +4 -2
  5. package/dist/analysis/layers.js.map +1 -1
  6. package/dist/analysis/port-wiring.d.ts.map +1 -1
  7. package/dist/analysis/port-wiring.js +2 -1
  8. package/dist/analysis/port-wiring.js.map +1 -1
  9. package/dist/analysis/source-files.d.ts +15 -0
  10. package/dist/analysis/source-files.d.ts.map +1 -0
  11. package/dist/analysis/source-files.js +52 -0
  12. package/dist/analysis/source-files.js.map +1 -0
  13. package/dist/analysis/workspace.d.ts.map +1 -1
  14. package/dist/analysis/workspace.js +21 -25
  15. package/dist/analysis/workspace.js.map +1 -1
  16. package/dist/app-map-changes.d.ts.map +1 -1
  17. package/dist/app-map-changes.js +5 -7
  18. package/dist/app-map-changes.js.map +1 -1
  19. package/dist/app-map.d.ts.map +1 -1
  20. package/dist/app-map.js +33 -30
  21. package/dist/app-map.js.map +1 -1
  22. package/dist/check.d.ts +12 -0
  23. package/dist/check.d.ts.map +1 -1
  24. package/dist/check.js +92 -25
  25. package/dist/check.js.map +1 -1
  26. package/dist/git-changes.d.ts +17 -1
  27. package/dist/git-changes.d.ts.map +1 -1
  28. package/dist/git-changes.js +118 -84
  29. package/dist/git-changes.js.map +1 -1
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +35 -6
  33. package/dist/index.js.map +1 -1
  34. package/dist/inspect.d.ts.map +1 -1
  35. package/dist/inspect.js +436 -112
  36. package/dist/inspect.js.map +1 -1
  37. package/dist/lint.d.ts.map +1 -1
  38. package/dist/lint.js +3 -6
  39. package/dist/lint.js.map +1 -1
  40. package/dist/make.d.ts.map +1 -1
  41. package/dist/make.js +0 -2
  42. package/dist/make.js.map +1 -1
  43. package/dist/mcp.d.ts.map +1 -1
  44. package/dist/mcp.js +23 -5
  45. package/dist/mcp.js.map +1 -1
  46. package/dist/operational-process.d.ts +1 -0
  47. package/dist/operational-process.d.ts.map +1 -1
  48. package/dist/operational-process.js.map +1 -1
  49. package/dist/operational-runner.js +1 -0
  50. package/dist/operational-runner.js.map +1 -1
  51. package/dist/outbox.d.ts +1 -0
  52. package/dist/outbox.d.ts.map +1 -1
  53. package/dist/outbox.js +58 -12
  54. package/dist/outbox.js.map +1 -1
  55. package/dist/templates/agents.d.ts.map +1 -1
  56. package/dist/templates/agents.js +43 -15
  57. package/dist/templates/agents.js.map +1 -1
  58. package/dist/templates/base.js +5 -5
  59. package/dist/templates/db/mysql.d.ts.map +1 -1
  60. package/dist/templates/db/mysql.js +51 -22
  61. package/dist/templates/db/mysql.js.map +1 -1
  62. package/dist/templates/db/postgres.d.ts.map +1 -1
  63. package/dist/templates/db/postgres.js +60 -20
  64. package/dist/templates/db/postgres.js.map +1 -1
  65. package/dist/templates/db/sqlite.d.ts.map +1 -1
  66. package/dist/templates/db/sqlite.js +51 -23
  67. package/dist/templates/db/sqlite.js.map +1 -1
  68. package/dist/templates/shadcn.js +1 -1
  69. package/dist/templates/shadcn.js.map +1 -1
  70. package/dist/templates/shared.js +4 -4
  71. package/dist/templates/shared.js.map +1 -1
  72. package/dist/templates/testing.d.ts.map +1 -1
  73. package/dist/templates/testing.js +7 -19
  74. package/dist/templates/testing.js.map +1 -1
  75. package/dist/test-discovery.d.ts +9 -0
  76. package/dist/test-discovery.d.ts.map +1 -0
  77. package/dist/test-discovery.js +40 -0
  78. package/dist/test-discovery.js.map +1 -0
  79. package/package.json +7 -5
  80. package/skills/app-structure/SKILL.md +37 -10
  81. package/src/analysis/layers.ts +4 -2
  82. package/src/analysis/port-wiring.ts +2 -1
  83. package/src/analysis/source-files.ts +61 -0
  84. package/src/analysis/workspace.ts +27 -31
  85. package/src/app-map-changes.ts +10 -8
  86. package/src/app-map.ts +43 -32
  87. package/src/check.ts +115 -25
  88. package/src/git-changes.ts +218 -86
  89. package/src/index.ts +53 -8
  90. package/src/inspect.ts +627 -146
  91. package/src/lint.ts +6 -5
  92. package/src/make.ts +0 -2
  93. package/src/mcp.ts +34 -11
  94. package/src/operational-process.ts +1 -0
  95. package/src/operational-runner.ts +1 -0
  96. package/src/outbox.ts +63 -12
  97. package/src/templates/agents.ts +43 -15
  98. package/src/templates/base.ts +5 -5
  99. package/src/templates/db/mysql.ts +51 -22
  100. package/src/templates/db/postgres.ts +60 -20
  101. package/src/templates/db/sqlite.ts +51 -23
  102. package/src/templates/shadcn.ts +1 -1
  103. package/src/templates/shared.ts +4 -4
  104. package/src/templates/testing.ts +12 -19
  105. 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,6 +12,16 @@ 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";
15
+ import {
16
+ analysisSourceFileExtensions,
17
+ isAnalysisSourceFile,
18
+ isAnalysisSourceFileExtension,
19
+ isJavaScriptSourceFile,
20
+ javascriptAnalysisSourceFileExtensions,
21
+ sourceFileScriptKind,
22
+ stripSourceFileExtension,
23
+ typescriptAnalysisSourceFileExtensions,
24
+ } from "./analysis/source-files.js";
13
25
  import { exportedVariableDeclarations } from "./analysis/source-index.js";
14
26
  import { createPainter } from "./ansi.js";
15
27
  import type { DatabaseSchemaDialect, DatabaseSchemaTable } from "./choices.js";
@@ -86,6 +98,7 @@ import {
86
98
  legacyGeneratedPackageScripts,
87
99
  } from "./templates/shared.js";
88
100
  import { testSupportTemplateFiles } from "./templates/testing.js";
101
+ import { isAppTestFile } from "./test-discovery.js";
89
102
  import { getCliVersion } from "./version.js";
90
103
 
91
104
  type InspectAppOptions = {
@@ -946,7 +959,7 @@ function parseContracts(source: string, file: string): ParsedContract[] {
946
959
  source,
947
960
  ts.ScriptTarget.Latest,
948
961
  true,
949
- ts.ScriptKind.TS,
962
+ sourceFileScriptKind(file),
950
963
  );
951
964
  const assignments = variableAssignments(sourceFile);
952
965
  const assignmentsByName = new Map(
@@ -1556,6 +1569,7 @@ function normalizeContractPath(routePath: string): string {
1556
1569
  async function readRouteExports(
1557
1570
  targetDir: string,
1558
1571
  routeFiles: string[],
1572
+ files: string[],
1559
1573
  config: ResolvedBeignetConfig,
1560
1574
  ): Promise<Map<string, RouteExport[]>> {
1561
1575
  const routeExports = new Map<string, RouteExport[]>();
@@ -1563,7 +1577,10 @@ async function readRouteExports(
1563
1577
  for (const file of routeFiles) {
1564
1578
  const routePath = nextRoutePath(file, config.paths.routes);
1565
1579
  const source = await readFile(path.join(targetDir, file), "utf8");
1566
- routeExports.set(file, parseRouteExports(source, file, routePath, config));
1580
+ routeExports.set(
1581
+ file,
1582
+ parseRouteExports(source, file, routePath, files, config),
1583
+ );
1567
1584
  }
1568
1585
 
1569
1586
  return routeExports;
@@ -1573,39 +1590,66 @@ function parseRouteExports(
1573
1590
  source: string,
1574
1591
  handlerFile: string,
1575
1592
  routePath: string,
1593
+ files: string[],
1576
1594
  config: ResolvedBeignetConfig,
1577
1595
  ): RouteExport[] {
1578
1596
  const exports: RouteExport[] = [];
1579
- const imports = parseNamedImports(source, config);
1580
- const exportRegex =
1581
- /export const\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s*=\s*([^;\n]+)/g;
1582
- const apiRouteExportRegex =
1583
- /export const\s*\{([^}]+)\}\s*=\s*createApiRoute\s*\(/g;
1597
+ const sourceFile = ts.createSourceFile(
1598
+ handlerFile,
1599
+ source,
1600
+ ts.ScriptTarget.Latest,
1601
+ true,
1602
+ sourceFileScriptKind(handlerFile),
1603
+ );
1604
+ const imports = parseRouteNamedImports(
1605
+ sourceFile,
1606
+ config,
1607
+ handlerFile,
1608
+ files,
1609
+ );
1584
1610
 
1585
- for (const match of source.matchAll(apiRouteExportRegex)) {
1586
- for (const member of match[1].split(",")) {
1587
- const parts = member.split(":");
1588
- const method = (parts[1] ?? parts[0])?.trim();
1589
- if (!method || !isHttpMethod(method)) continue;
1611
+ for (const statement of sourceFile.statements) {
1612
+ if (
1613
+ !ts.isVariableStatement(statement) ||
1614
+ !statement.modifiers?.some(
1615
+ (modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword,
1616
+ )
1617
+ ) {
1618
+ continue;
1619
+ }
1590
1620
 
1591
- exports.push({
1592
- method,
1593
- handlerFile,
1594
- contractRef: routePath,
1595
- catchAllPrefix: catchAllRoutePrefix(routePath),
1596
- source: "next-route",
1597
- });
1621
+ for (const declaration of statement.declarationList.declarations) {
1622
+ if (
1623
+ !declaration.initializer ||
1624
+ !ts.isObjectBindingPattern(declaration.name) ||
1625
+ !isNamedRouteFactoryCall(declaration.initializer, "createApiRoute")
1626
+ ) {
1627
+ continue;
1628
+ }
1629
+
1630
+ for (const element of declaration.name.elements) {
1631
+ if (!ts.isIdentifier(element.name)) continue;
1632
+ const method = element.name.text;
1633
+ if (!isHttpMethod(method)) continue;
1634
+ exports.push({
1635
+ method,
1636
+ handlerFile,
1637
+ contractRef: routePath,
1638
+ catchAllPrefix: catchAllRoutePrefix(routePath),
1639
+ source: "next-route",
1640
+ });
1641
+ }
1598
1642
  }
1599
1643
  }
1600
1644
 
1601
- for (const match of source.matchAll(exportRegex)) {
1602
- const method = match[1] as HttpMethod;
1603
- const expression = match[2];
1604
- const contractMatch =
1605
- /server\.route\(\s*([A-Za-z_$][\w$]*)\s*\)\.handle/.exec(expression);
1645
+ for (const { exportName, declaration } of exportedVariableDeclarations(
1646
+ sourceFile,
1647
+ )) {
1648
+ if (!isHttpMethod(exportName) || !declaration.initializer) continue;
1649
+ const method = exportName;
1650
+ const localName = routeLocalContractIdentifier(declaration.initializer);
1606
1651
 
1607
- if (contractMatch) {
1608
- const localName = contractMatch[1];
1652
+ if (localName) {
1609
1653
  const imported = imports.get(localName);
1610
1654
  exports.push({
1611
1655
  method,
@@ -1617,7 +1661,7 @@ function parseRouteExports(
1617
1661
  continue;
1618
1662
  }
1619
1663
 
1620
- if (/server\.api\b/.test(expression)) {
1664
+ if (isServerApiExpression(declaration.initializer)) {
1621
1665
  exports.push({
1622
1666
  method,
1623
1667
  handlerFile,
@@ -1631,6 +1675,100 @@ function parseRouteExports(
1631
1675
  return exports;
1632
1676
  }
1633
1677
 
1678
+ function parseRouteNamedImports(
1679
+ sourceFile: ts.SourceFile,
1680
+ config: ResolvedBeignetConfig,
1681
+ handlerFile: string,
1682
+ files: string[],
1683
+ ): Map<string, { importedName: string; contractFile?: string }> {
1684
+ const imports = new Map<
1685
+ string,
1686
+ { importedName: string; contractFile?: string }
1687
+ >();
1688
+
1689
+ for (const statement of sourceFile.statements) {
1690
+ if (
1691
+ !ts.isImportDeclaration(statement) ||
1692
+ !ts.isStringLiteral(statement.moduleSpecifier) ||
1693
+ statement.importClause?.isTypeOnly ||
1694
+ !statement.importClause?.namedBindings ||
1695
+ !ts.isNamedImports(statement.importClause.namedBindings)
1696
+ ) {
1697
+ continue;
1698
+ }
1699
+
1700
+ const contractFile = contractFileFromImport(
1701
+ statement.moduleSpecifier.text,
1702
+ config,
1703
+ handlerFile,
1704
+ files,
1705
+ );
1706
+ for (const element of statement.importClause.namedBindings.elements) {
1707
+ if (element.isTypeOnly) continue;
1708
+ imports.set(element.name.text, {
1709
+ importedName: element.propertyName?.text ?? element.name.text,
1710
+ contractFile,
1711
+ });
1712
+ }
1713
+ }
1714
+
1715
+ return imports;
1716
+ }
1717
+
1718
+ function isNamedRouteFactoryCall(
1719
+ expression: ts.Expression,
1720
+ name: string,
1721
+ ): boolean {
1722
+ const unwrapped = unwrapContractExpression(expression);
1723
+ if (!ts.isCallExpression(unwrapped)) return false;
1724
+ const callee = unwrapContractExpression(unwrapped.expression);
1725
+ return ts.isIdentifier(callee) && callee.text === name;
1726
+ }
1727
+
1728
+ function routeLocalContractIdentifier(
1729
+ expression: ts.Expression,
1730
+ ): string | undefined {
1731
+ const handleCall = unwrapContractExpression(expression);
1732
+ if (!ts.isCallExpression(handleCall)) return undefined;
1733
+ const handleAccess = unwrapContractExpression(handleCall.expression);
1734
+ if (
1735
+ !ts.isPropertyAccessExpression(handleAccess) ||
1736
+ handleAccess.name.text !== "handle"
1737
+ ) {
1738
+ return undefined;
1739
+ }
1740
+
1741
+ const routeCall = unwrapContractExpression(handleAccess.expression);
1742
+ if (!ts.isCallExpression(routeCall)) return undefined;
1743
+ const routeAccess = unwrapContractExpression(routeCall.expression);
1744
+ if (
1745
+ !ts.isPropertyAccessExpression(routeAccess) ||
1746
+ routeAccess.name.text !== "route" ||
1747
+ !ts.isIdentifier(routeAccess.expression) ||
1748
+ routeAccess.expression.text !== "server"
1749
+ ) {
1750
+ return undefined;
1751
+ }
1752
+
1753
+ const contract = routeCall.arguments[0];
1754
+ const unwrappedContract = contract
1755
+ ? unwrapContractExpression(contract)
1756
+ : undefined;
1757
+ return unwrappedContract && ts.isIdentifier(unwrappedContract)
1758
+ ? unwrappedContract.text
1759
+ : undefined;
1760
+ }
1761
+
1762
+ function isServerApiExpression(expression: ts.Expression): boolean {
1763
+ const unwrapped = unwrapContractExpression(expression);
1764
+ return (
1765
+ ts.isPropertyAccessExpression(unwrapped) &&
1766
+ ts.isIdentifier(unwrapped.expression) &&
1767
+ unwrapped.expression.text === "server" &&
1768
+ unwrapped.name.text === "api"
1769
+ );
1770
+ }
1771
+
1634
1772
  function catchAllRoutePrefix(routePath: string): string | undefined {
1635
1773
  const segments = routePath.split("/").filter(Boolean);
1636
1774
  const catchAllIndex = segments.findIndex((segment) => segment.endsWith("*"));
@@ -1644,6 +1782,7 @@ function parseNamedImports(
1644
1782
  source: string,
1645
1783
  config: ResolvedBeignetConfig,
1646
1784
  importerFile?: string,
1785
+ files?: string[],
1647
1786
  ): Map<
1648
1787
  string,
1649
1788
  {
@@ -1666,6 +1805,7 @@ function parseNamedImports(
1666
1805
  sourcePath,
1667
1806
  config,
1668
1807
  importerFile,
1808
+ files,
1669
1809
  );
1670
1810
  for (const member of match[1].split(",")) {
1671
1811
  const parsed = parseImportMember(member);
@@ -1736,9 +1876,7 @@ function sourceFileFromImport(
1736
1876
  files?: string[],
1737
1877
  ): string | undefined {
1738
1878
  const resolveCandidate = (candidate: string) => {
1739
- if (path.extname(candidate)) return candidate;
1740
-
1741
- const candidates = [`${candidate}.ts`, `${candidate}/index.ts`];
1879
+ const candidates = sourceFileSpecifierCandidates(candidate, importerFile);
1742
1880
  return files
1743
1881
  ? candidates.find((file) => files.includes(file))
1744
1882
  : candidates[0];
@@ -1757,11 +1895,77 @@ function sourceFileFromImport(
1757
1895
  return undefined;
1758
1896
  }
1759
1897
 
1898
+ function sourceFileCandidates(base: string, importerFile?: string): string[] {
1899
+ const extensions =
1900
+ importerFile && isJavaScriptSourceFile(importerFile)
1901
+ ? [
1902
+ ...javascriptAnalysisSourceFileExtensions,
1903
+ ...typescriptAnalysisSourceFileExtensions,
1904
+ ]
1905
+ : analysisSourceFileExtensions;
1906
+ return [
1907
+ ...extensions.map((extension) => `${base}${extension}`),
1908
+ ...extensions.map((extension) => `${base}/index${extension}`),
1909
+ ];
1910
+ }
1911
+
1912
+ function sourceFileSpecifierCandidates(
1913
+ candidate: string,
1914
+ importerFile?: string,
1915
+ ): string[] {
1916
+ const extension = path.posix.extname(candidate).toLowerCase();
1917
+ if (!extension) return sourceFileCandidates(candidate, importerFile);
1918
+ if (!isAnalysisSourceFileExtension(extension)) return [candidate];
1919
+
1920
+ const sourceExtensions =
1921
+ extension === ".js"
1922
+ ? [".ts", ".tsx"]
1923
+ : extension === ".jsx"
1924
+ ? [".tsx"]
1925
+ : extension === ".mjs"
1926
+ ? [".mts"]
1927
+ : extension === ".cjs"
1928
+ ? [".cts"]
1929
+ : [];
1930
+ const sourceCandidates = sourceExtensions.map(
1931
+ (sourceExtension) =>
1932
+ `${candidate.slice(0, -extension.length)}${sourceExtension}`,
1933
+ );
1934
+ const directCandidates =
1935
+ importerFile && isJavaScriptSourceFile(importerFile)
1936
+ ? [candidate, ...sourceCandidates]
1937
+ : [...sourceCandidates, candidate];
1938
+ return [
1939
+ ...new Set([
1940
+ ...directCandidates,
1941
+ ...directCandidates.map(directoryIndexSourceFile),
1942
+ ]),
1943
+ ];
1944
+ }
1945
+
1946
+ function isSourceFileNamed(file: string, name: string): boolean {
1947
+ return (
1948
+ isAnalysisSourceFile(file) &&
1949
+ path.posix.basename(stripSourceFileExtension(file)) === name
1950
+ );
1951
+ }
1952
+
1953
+ function directoryIndexSourceFile(file: string): string {
1954
+ const extension = path.posix.extname(file);
1955
+ return `${stripSourceFileExtension(file)}/index${extension}`;
1956
+ }
1957
+
1760
1958
  function contractFileFromImport(
1761
1959
  sourcePath: string,
1762
1960
  config: ResolvedBeignetConfig,
1763
1961
  importerFile?: string,
1962
+ files?: string[],
1764
1963
  ): string | undefined {
1964
+ const resolveCandidate = (candidate: string) => {
1965
+ const candidates = sourceFileSpecifierCandidates(candidate, importerFile);
1966
+ if (!files) return candidates[0];
1967
+ return candidates.find((file) => files.includes(file)) ?? candidates[0];
1968
+ };
1765
1969
  const contractsPath = directoryPath(config.paths.contracts);
1766
1970
  const aliasPrefix = `@/${contractsPath}/`;
1767
1971
  const aliasExact = `@/${contractsPath}`;
@@ -1769,21 +1973,21 @@ function contractFileFromImport(
1769
1973
  const relativeExact = contractsPath;
1770
1974
 
1771
1975
  if (sourcePath === aliasExact || sourcePath === relativeExact) {
1772
- return `${contractsPath}/index.ts`;
1976
+ return resolveCandidate(`${contractsPath}/index`);
1773
1977
  }
1774
1978
  if (sourcePath.startsWith(aliasPrefix)) {
1775
- return `${sourcePath.slice("@/".length)}.ts`;
1979
+ return resolveCandidate(sourcePath.slice("@/".length));
1776
1980
  }
1777
1981
 
1778
1982
  if (sourcePath.startsWith(relativePrefix)) {
1779
- return `${sourcePath}.ts`;
1983
+ return resolveCandidate(sourcePath);
1780
1984
  }
1781
1985
 
1782
1986
  if (importerFile && sourcePath.startsWith(".")) {
1783
1987
  const resolved = normalizePath(
1784
1988
  path.join(path.dirname(importerFile), sourcePath),
1785
1989
  );
1786
- return `${resolved}.ts`;
1990
+ return resolveCandidate(resolved);
1787
1991
  }
1788
1992
 
1789
1993
  return undefined;
@@ -1866,9 +2070,14 @@ async function inspectRouteSurface(
1866
2070
  const routeFiles = files.filter(
1867
2071
  (file) =>
1868
2072
  file.startsWith(`${directoryPath(config.paths.routes)}/`) &&
1869
- file.endsWith("/route.ts"),
2073
+ isNextRouteFile(file, config),
2074
+ );
2075
+ const routeExports = await readRouteExports(
2076
+ targetDir,
2077
+ routeFiles,
2078
+ files,
2079
+ config,
1870
2080
  );
1871
- const routeExports = await readRouteExports(targetDir, routeFiles, config);
1872
2081
  return matchRoutes(contracts, routeExports);
1873
2082
  }
1874
2083
 
@@ -1940,8 +2149,7 @@ function findContracts(
1940
2149
  contract.exportName === routeExport.contractRef &&
1941
2150
  (!routeExport.contractFile ||
1942
2151
  contract.file === routeExport.contractFile ||
1943
- contract.file ===
1944
- routeExport.contractFile.replace(/\.ts$/, "/index.ts")),
2152
+ contract.file === directoryIndexSourceFile(routeExport.contractFile)),
1945
2153
  );
1946
2154
  return contract ? [contract] : [];
1947
2155
  }
@@ -2385,7 +2593,12 @@ async function inspectCanonicalConformance(
2385
2593
 
2386
2594
  const featureRoutesPath = `${featuresPath}/`;
2387
2595
  for (const file of files) {
2388
- if (!file.startsWith(featureRoutesPath) || !file.endsWith("/routes.ts")) {
2596
+ if (
2597
+ !file.startsWith(featureRoutesPath) ||
2598
+ !isAnalysisSourceFile(file) ||
2599
+ isAppTestFile(file) ||
2600
+ !stripSourceFileExtension(file).endsWith("/routes")
2601
+ ) {
2389
2602
  continue;
2390
2603
  }
2391
2604
 
@@ -2774,7 +2987,7 @@ async function inspectProductionReadiness(
2774
2987
  let hasSecurityHeadersHook = false;
2775
2988
 
2776
2989
  for (const file of files) {
2777
- if (!file.endsWith(".ts") || file.endsWith(".test.ts")) continue;
2990
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file)) continue;
2778
2991
 
2779
2992
  const source = await readCachedSource(targetDir, file, sourceCache);
2780
2993
  const routeServerOrInfra = isRouteServerOrInfraFile(file, config);
@@ -2801,7 +3014,7 @@ async function inspectProductionReadiness(
2801
3014
 
2802
3015
  if (
2803
3016
  fileWithinDirectory(file, cronDirectory) &&
2804
- file.endsWith("/route.ts") &&
3017
+ isNextRouteFile(file, config) &&
2805
3018
  !/\bCRON_SECRET\b/.test(source) &&
2806
3019
  !/\bcreateOutboxDrainRoute\s*\(/.test(source) &&
2807
3020
  !/\bcreateScheduleRoute\s*\(/.test(source)
@@ -3103,6 +3316,25 @@ async function inspectProductionReadiness(
3103
3316
  "@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.",
3104
3317
  });
3105
3318
  }
3319
+
3320
+ if (
3321
+ (await betterAuthRequiresIssuerSchema(targetDir, packageJson)) &&
3322
+ (await anyFileContains(
3323
+ targetDir,
3324
+ configFiles,
3325
+ "drizzleAdapter",
3326
+ sourceCache,
3327
+ )) &&
3328
+ !(await hasBetterAuthIssuerSchema(targetDir, files, sourceCache))
3329
+ ) {
3330
+ diagnostics.push({
3331
+ severity: "warning",
3332
+ code: "BEIGNET_BETTER_AUTH_ISSUER_SCHEMA_MISSING",
3333
+ file: schemaDir,
3334
+ message:
3335
+ "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.",
3336
+ });
3337
+ }
3106
3338
  }
3107
3339
 
3108
3340
  const installedReadinessProviders = [...installedPackages].filter(
@@ -3148,7 +3380,7 @@ async function uploadHasExplicitSizeLimit(
3148
3380
  );
3149
3381
  if (!importedName) continue;
3150
3382
 
3151
- const importedFile = resolveLocalTypeScriptModule(file, moduleName, files);
3383
+ const importedFile = sourceFileFromImport(moduleName, file, files);
3152
3384
  if (!importedFile) continue;
3153
3385
 
3154
3386
  const importedSource = await readCachedSource(
@@ -3203,20 +3435,6 @@ function exportedObjectHasProperty(
3203
3435
  return new RegExp(`\\b${escapeRegExp(propertyName)}\\s*:`).test(initializer);
3204
3436
  }
3205
3437
 
3206
- function resolveLocalTypeScriptModule(
3207
- importer: string,
3208
- moduleName: string,
3209
- files: string[],
3210
- ): string | undefined {
3211
- const base = normalizePath(
3212
- path.posix.normalize(
3213
- path.posix.join(path.posix.dirname(importer), moduleName),
3214
- ),
3215
- );
3216
- const candidates = [base, `${base}.ts`, `${base}.tsx`, `${base}/index.ts`];
3217
- return candidates.find((candidate) => files.includes(candidate));
3218
- }
3219
-
3220
3438
  async function hasReadinessCheck(
3221
3439
  targetDir: string,
3222
3440
  files: string[],
@@ -3224,10 +3442,18 @@ async function hasReadinessCheck(
3224
3442
  sourceCache: Map<string, string>,
3225
3443
  ): Promise<boolean> {
3226
3444
  const routesDir = directoryPath(config.paths.routes);
3227
- if (files.includes(`${routesDir}/ready/route.ts`)) return true;
3445
+ if (
3446
+ files.some(
3447
+ (file) =>
3448
+ path.posix.dirname(file) === `${routesDir}/ready` &&
3449
+ isNextRouteFile(file, config),
3450
+ )
3451
+ ) {
3452
+ return true;
3453
+ }
3228
3454
 
3229
3455
  for (const file of files) {
3230
- if (!file.endsWith(".ts") || file.endsWith(".test.ts")) continue;
3456
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file)) continue;
3231
3457
  if (!isRouteServerOrInfraFile(file, config)) continue;
3232
3458
 
3233
3459
  const source = await readCachedSource(targetDir, file, sourceCache);
@@ -3769,10 +3995,11 @@ function isFeatureUploadDefinition(
3769
3995
  file: string,
3770
3996
  config: ResolvedBeignetConfig,
3771
3997
  ): boolean {
3772
- const featuresPath = directoryPath(config.paths.features);
3773
- return new RegExp(
3774
- `^${escapeRegExp(featuresPath)}/[^/]+/uploads/[^/]+\\.ts$`,
3775
- ).test(file);
3998
+ return isFeatureArtifactFile(
3999
+ file,
4000
+ directoryPath(config.paths.features),
4001
+ "uploads",
4002
+ );
3776
4003
  }
3777
4004
 
3778
4005
  function operationalConfigFiles(
@@ -3782,17 +4009,263 @@ function operationalConfigFiles(
3782
4009
  return providerOperationalConfigFiles(files, config);
3783
4010
  }
3784
4011
 
3785
- async function readPackageJson(
4012
+ type InspectedPackageJson = {
4013
+ dependencies?: Record<string, string>;
4014
+ devDependencies?: Record<string, string>;
4015
+ peerDependencies?: Record<string, string>;
4016
+ };
4017
+
4018
+ function declaredBetterAuthMayRequireIssuerSchema(
4019
+ packageJson: InspectedPackageJson | undefined,
4020
+ ): boolean {
4021
+ const version =
4022
+ packageJson?.dependencies?.["better-auth"] ??
4023
+ packageJson?.devDependencies?.["better-auth"] ??
4024
+ packageJson?.peerDependencies?.["better-auth"];
4025
+ if (!version) return false;
4026
+
4027
+ const normalized = version.trim();
4028
+ const workspaceRange = normalized.startsWith("workspace:")
4029
+ ? normalized.slice("workspace:".length)
4030
+ : normalized;
4031
+ const npmAlias = /^npm:better-auth@(.+)$/.exec(workspaceRange);
4032
+ const candidate = npmAlias?.[1] ?? workspaceRange;
4033
+ const range = semver.validRange(candidate);
4034
+
4035
+ // Tags, catalogs, local paths, and other non-semver declarations can resolve
4036
+ // to 1.7. Prefer a migration warning until an installed version is readable.
4037
+ if (!range) return true;
4038
+
4039
+ return semver.intersects(range, ">=1.7.0");
4040
+ }
4041
+
4042
+ async function betterAuthRequiresIssuerSchema(
4043
+ targetDir: string,
4044
+ packageJson: InspectedPackageJson | undefined,
4045
+ ): Promise<boolean> {
4046
+ const installed = await readInstalledBetterAuthPackageJson(targetDir);
4047
+ if (installed?.version) {
4048
+ const version = semver.valid(installed.version.trim());
4049
+ if (version) return semver.gte(version, "1.7.0");
4050
+ }
4051
+
4052
+ return declaredBetterAuthMayRequireIssuerSchema(packageJson);
4053
+ }
4054
+
4055
+ async function readInstalledBetterAuthPackageJson(
4056
+ targetDir: string,
4057
+ ): Promise<{ name?: string; version?: string } | undefined> {
4058
+ const nodeModulesPackage =
4059
+ await readBetterAuthPackageFromNodeModules(targetDir);
4060
+ if (nodeModulesPackage) return nodeModulesPackage;
4061
+
4062
+ const requireFromTarget = createRequire(path.join(targetDir, "package.json"));
4063
+ const packageJsonPath = resolveModuleFromTarget(
4064
+ requireFromTarget,
4065
+ "better-auth/package.json",
4066
+ );
4067
+ if (packageJsonPath) {
4068
+ return readJsonIfExists(packageJsonPath);
4069
+ }
4070
+
4071
+ const entryPath = resolveModuleFromTarget(requireFromTarget, "better-auth");
4072
+ if (!entryPath) return undefined;
4073
+
4074
+ let directory = path.dirname(entryPath);
4075
+ while (true) {
4076
+ const candidate = await readJsonIfExists<{
4077
+ name?: string;
4078
+ version?: string;
4079
+ }>(path.join(directory, "package.json"));
4080
+ if (candidate?.name === "better-auth") return candidate;
4081
+
4082
+ const parent = path.dirname(directory);
4083
+ if (parent === directory) return undefined;
4084
+ directory = parent;
4085
+ }
4086
+ }
4087
+
4088
+ async function readBetterAuthPackageFromNodeModules(
4089
+ targetDir: string,
4090
+ ): Promise<{ name?: string; version?: string } | undefined> {
4091
+ let directory = targetDir;
4092
+ while (true) {
4093
+ const candidate = await readJsonIfExists<{
4094
+ name?: string;
4095
+ version?: string;
4096
+ }>(path.join(directory, "node_modules", "better-auth", "package.json"));
4097
+ if (candidate?.name === "better-auth") return candidate;
4098
+
4099
+ const parent = path.dirname(directory);
4100
+ if (parent === directory) return undefined;
4101
+ directory = parent;
4102
+ }
4103
+ }
4104
+
4105
+ function resolveModuleFromTarget(
4106
+ requireFromTarget: ReturnType<typeof createRequire>,
4107
+ request: string,
4108
+ ): string | undefined {
4109
+ try {
4110
+ return requireFromTarget.resolve(request);
4111
+ } catch {
4112
+ return undefined;
4113
+ }
4114
+ }
4115
+
4116
+ async function hasBetterAuthIssuerSchema(
3786
4117
  targetDir: string,
3787
4118
  files: string[],
3788
- ): Promise<
3789
- | {
3790
- dependencies?: Record<string, string>;
3791
- devDependencies?: Record<string, string>;
3792
- peerDependencies?: Record<string, string>;
4119
+ sourceCache: Map<string, string>,
4120
+ ): Promise<boolean> {
4121
+ const schemaFiles = files.filter(
4122
+ (file) =>
4123
+ !isTestSourceFile(file) &&
4124
+ /\.(?:ts|tsx|js|jsx|mts|mjs|cts|cjs)$/.test(file),
4125
+ );
4126
+
4127
+ for (const file of schemaFiles) {
4128
+ const source = await readCachedSource(targetDir, file, sourceCache);
4129
+ if (sourceHasBetterAuthIssuerSchema(file, source)) return true;
4130
+ }
4131
+
4132
+ return false;
4133
+ }
4134
+
4135
+ function sourceHasBetterAuthIssuerSchema(
4136
+ file: string,
4137
+ source: string,
4138
+ ): boolean {
4139
+ const sourceFile = ts.createSourceFile(
4140
+ file,
4141
+ source,
4142
+ ts.ScriptTarget.Latest,
4143
+ true,
4144
+ );
4145
+ let found = false;
4146
+ const visit = (node: ts.Node): void => {
4147
+ if (found) return;
4148
+ if (
4149
+ ts.isCallExpression(node) &&
4150
+ isBetterAuthAccountTableCall(node) &&
4151
+ betterAuthAccountTableHasIssuerIdentity(node)
4152
+ ) {
4153
+ found = true;
4154
+ return;
3793
4155
  }
3794
- | undefined
3795
- > {
4156
+ ts.forEachChild(node, visit);
4157
+ };
4158
+ visit(sourceFile);
4159
+ return found;
4160
+ }
4161
+
4162
+ function isBetterAuthAccountTableCall(call: ts.CallExpression): boolean {
4163
+ const callee = unwrapContractExpression(call.expression);
4164
+ const factoryName = ts.isIdentifier(callee)
4165
+ ? callee.text
4166
+ : ts.isPropertyAccessExpression(callee)
4167
+ ? callee.name.text
4168
+ : undefined;
4169
+ return (
4170
+ factoryName !== undefined &&
4171
+ ["mysqlTable", "pgTable", "sqliteTable"].includes(factoryName) &&
4172
+ staticString(call.arguments[0]) === "account"
4173
+ );
4174
+ }
4175
+
4176
+ function betterAuthAccountTableHasIssuerIdentity(
4177
+ call: ts.CallExpression,
4178
+ ): boolean {
4179
+ const columns = call.arguments[1];
4180
+ if (!columns || !ts.isObjectLiteralExpression(columns)) return false;
4181
+ const issuer = columns.properties.find(
4182
+ (property) =>
4183
+ ts.isPropertyAssignment(property) &&
4184
+ staticPropertyName(property.name) === "issuer",
4185
+ );
4186
+ if (!issuer || !ts.isPropertyAssignment(issuer)) return false;
4187
+ if (!isBetterAuthIssuerColumn(issuer.initializer)) return false;
4188
+
4189
+ const constraints = call.arguments[2];
4190
+ if (
4191
+ !constraints ||
4192
+ (!ts.isArrowFunction(constraints) &&
4193
+ !ts.isFunctionExpression(constraints)) ||
4194
+ constraints.parameters.length === 0 ||
4195
+ !ts.isIdentifier(constraints.parameters[0]?.name)
4196
+ ) {
4197
+ return false;
4198
+ }
4199
+
4200
+ const tableName = constraints.parameters[0].name.text;
4201
+ let found = false;
4202
+ const visit = (node: ts.Node): void => {
4203
+ if (found) return;
4204
+ if (
4205
+ ts.isCallExpression(node) &&
4206
+ isBetterAuthIssuerAccountUniqueConstraint(node, tableName)
4207
+ ) {
4208
+ found = true;
4209
+ return;
4210
+ }
4211
+ ts.forEachChild(node, visit);
4212
+ };
4213
+ visit(constraints.body);
4214
+ return found;
4215
+ }
4216
+
4217
+ function isBetterAuthIssuerColumn(expression: ts.Expression): boolean {
4218
+ let current = unwrapContractExpression(expression);
4219
+ while (ts.isCallExpression(current)) {
4220
+ const callee = unwrapContractExpression(current.expression);
4221
+ if (
4222
+ ts.isIdentifier(callee) &&
4223
+ ["text", "varchar"].includes(callee.text) &&
4224
+ staticString(current.arguments[0]) === "issuer"
4225
+ ) {
4226
+ return true;
4227
+ }
4228
+ if (!ts.isPropertyAccessExpression(callee)) return false;
4229
+ current = unwrapContractExpression(callee.expression);
4230
+ }
4231
+ return false;
4232
+ }
4233
+
4234
+ function isBetterAuthIssuerAccountUniqueConstraint(
4235
+ call: ts.CallExpression,
4236
+ tableName: string,
4237
+ ): boolean {
4238
+ const callee = unwrapContractExpression(call.expression);
4239
+ if (!ts.isPropertyAccessExpression(callee) || callee.name.text !== "on") {
4240
+ return false;
4241
+ }
4242
+
4243
+ const factoryCall = unwrapContractExpression(callee.expression);
4244
+ if (!ts.isCallExpression(factoryCall)) return false;
4245
+ const factory = unwrapContractExpression(factoryCall.expression);
4246
+ if (
4247
+ !ts.isIdentifier(factory) ||
4248
+ !["unique", "uniqueIndex"].includes(factory.text)
4249
+ ) {
4250
+ return false;
4251
+ }
4252
+
4253
+ if (call.arguments.length !== 2) return false;
4254
+ const columnNames = call.arguments.map((argument) => {
4255
+ const column = unwrapContractExpression(argument);
4256
+ return ts.isPropertyAccessExpression(column) &&
4257
+ ts.isIdentifier(column.expression) &&
4258
+ column.expression.text === tableName
4259
+ ? column.name.text
4260
+ : undefined;
4261
+ });
4262
+ return columnNames.includes("issuer") && columnNames.includes("accountId");
4263
+ }
4264
+
4265
+ async function readPackageJson(
4266
+ targetDir: string,
4267
+ files: string[],
4268
+ ): Promise<InspectedPackageJson | undefined> {
3796
4269
  if (!files.includes("package.json")) return undefined;
3797
4270
 
3798
4271
  return JSON.parse(
@@ -3953,7 +4426,7 @@ async function hasBillingEntitlementsProviderWiring(
3953
4426
  const serverPath = path.dirname(config.paths.server);
3954
4427
  const candidateFiles = files.filter(
3955
4428
  (file) =>
3956
- /\.(?:ts|tsx|mts|cts)$/.test(file) &&
4429
+ isAnalysisSourceFile(file) &&
3957
4430
  !isTestSourceFile(file) &&
3958
4431
  (file.startsWith(`${infraPath}/`) || file.startsWith(`${serverPath}/`)),
3959
4432
  );
@@ -3969,10 +4442,7 @@ async function hasBillingEntitlementsProviderWiring(
3969
4442
  }
3970
4443
 
3971
4444
  function isTestSourceFile(file: string): boolean {
3972
- return (
3973
- /(?:^|\/)tests\//.test(file) ||
3974
- /\.(?:test|spec)\.(?:ts|tsx|mts|cts)$/.test(file)
3975
- );
4445
+ return /(?:^|\/)tests\//.test(file) || isAppTestFile(file);
3976
4446
  }
3977
4447
 
3978
4448
  function exportedConstInitializer(
@@ -3999,8 +4469,8 @@ function featureNameFromContractFile(
3999
4469
  ): string | undefined {
4000
4470
  const featuresPath = directoryPath(config.paths.features);
4001
4471
  const match = new RegExp(
4002
- `^${escapeRegExp(featuresPath)}/([^/]+)/contracts\\.ts$`,
4003
- ).exec(file);
4472
+ `^${escapeRegExp(featuresPath)}/([^/]+)/contracts$`,
4473
+ ).exec(stripSourceFileExtension(file));
4004
4474
  return match?.[1];
4005
4475
  }
4006
4476
 
@@ -4017,7 +4487,7 @@ function featureRuntimeFiles(
4017
4487
  "use-cases",
4018
4488
  ];
4019
4489
  return files.filter((file) => {
4020
- if (!file.endsWith(".ts") || file.endsWith(".test.ts")) return false;
4490
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file)) return false;
4021
4491
  return runtimeFolders.some((folder) =>
4022
4492
  file.startsWith(`${featuresPath}/${feature}/${folder}/`),
4023
4493
  );
@@ -4033,11 +4503,16 @@ function featureTestFiles(
4033
4503
  return files.filter((file) => {
4034
4504
  if (
4035
4505
  !file.startsWith(`${featuresPath}/${feature}/`) ||
4036
- !file.endsWith(".test.ts")
4506
+ !isAppTestFile(file)
4037
4507
  ) {
4038
4508
  return false;
4039
4509
  }
4040
- return !options.policyOnly || file.endsWith("/policy.test.ts");
4510
+ return (
4511
+ !options.policyOnly ||
4512
+ ["policy.test", "policy.spec"].includes(
4513
+ path.posix.basename(stripSourceFileExtension(file)),
4514
+ )
4515
+ );
4041
4516
  });
4042
4517
  }
4043
4518
 
@@ -4204,17 +4679,12 @@ async function inspectFeatureArtifactDrift(
4204
4679
  severity: "warning",
4205
4680
  code: "BEIGNET_FEATURE_TEST_PLACEMENT",
4206
4681
  file,
4207
- 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.`,
4682
+ 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.`,
4208
4683
  });
4209
4684
  }
4210
4685
 
4211
4686
  for (const file of files) {
4212
- if (
4213
- !/\.(?:ts|tsx|mts|cts)$/.test(file) ||
4214
- /\.d\.(?:ts|mts|cts)$/.test(file)
4215
- ) {
4216
- continue;
4217
- }
4687
+ if (!isAnalysisSourceFile(file)) continue;
4218
4688
 
4219
4689
  const misplaced = directFeatureTestMatch(file, featuresPath)
4220
4690
  ? undefined
@@ -4229,7 +4699,6 @@ async function inspectFeatureArtifactDrift(
4229
4699
  }
4230
4700
 
4231
4701
  if (isTestSourceFile(file)) continue;
4232
- if (!file.endsWith(".ts")) continue;
4233
4702
 
4234
4703
  if (
4235
4704
  isNotificationDefinitionFile(file, featuresPath) ||
@@ -4255,9 +4724,10 @@ function directFeatureTestMatch(
4255
4724
  file: string,
4256
4725
  featuresPath: string,
4257
4726
  ): RegExpExecArray | null {
4727
+ if (!isAppTestFile(file)) return null;
4258
4728
  return new RegExp(
4259
- `^${escapeRegExp(featuresPath)}/([^/]+)/[^/]+\\.(?:test|spec)\\.(?:ts|tsx|mts|cts)$`,
4260
- ).exec(file);
4729
+ `^${escapeRegExp(featuresPath)}/([^/]+)/[^/]+\\.(?:test|spec)$`,
4730
+ ).exec(stripSourceFileExtension(file));
4261
4731
  }
4262
4732
 
4263
4733
  function isFeatureArtifactFile(
@@ -4265,9 +4735,10 @@ function isFeatureArtifactFile(
4265
4735
  featuresPath: string,
4266
4736
  folder: "uploads" | "notifications",
4267
4737
  ): boolean {
4738
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file)) return false;
4268
4739
  const match = new RegExp(
4269
- `^${escapeRegExp(featuresPath)}/[^/]+/${folder}/([^/]+)\\.ts$`,
4270
- ).exec(file);
4740
+ `^${escapeRegExp(featuresPath)}/[^/]+/${folder}/([^/]+)$`,
4741
+ ).exec(stripSourceFileExtension(file));
4271
4742
  return Boolean(match && match[1] !== "index");
4272
4743
  }
4273
4744
 
@@ -4277,7 +4748,7 @@ async function appHasUploadRoute(
4277
4748
  config: ResolvedBeignetConfig,
4278
4749
  ): Promise<boolean> {
4279
4750
  for (const file of files) {
4280
- if (!file.endsWith(".ts") || file.endsWith(".test.ts")) continue;
4751
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file)) continue;
4281
4752
  if (!isRouteServerOrInfraFile(file, config)) continue;
4282
4753
 
4283
4754
  const source = await readFile(path.join(targetDir, file), "utf8");
@@ -4831,11 +5302,17 @@ async function readFeatureWorkflowRegistries(
4831
5302
  for (const kind of kinds) {
4832
5303
  const folder = kind === "events" ? "domain/events" : kind;
4833
5304
  const pattern = new RegExp(
4834
- `^${escapeRegExp(featuresPath)}/[^/]+/${escapeRegExp(folder)}/index\\.ts$`,
5305
+ `^${escapeRegExp(featuresPath)}/[^/]+/${escapeRegExp(folder)}/index$`,
4835
5306
  );
4836
5307
 
4837
5308
  for (const file of files) {
4838
- if (!pattern.test(file)) continue;
5309
+ if (
5310
+ !isAnalysisSourceFile(file) ||
5311
+ isWorkflowTestFile(file) ||
5312
+ !pattern.test(stripSourceFileExtension(file))
5313
+ ) {
5314
+ continue;
5315
+ }
4839
5316
 
4840
5317
  const source = await readFile(path.join(targetDir, file), "utf8");
4841
5318
  const memberFiles = exportedMemberFiles(source, file, files);
@@ -4895,8 +5372,7 @@ async function readRuntimeManifestIdentifiers(
4895
5372
  const manifests: RuntimeManifestIdentifierSets[] = [];
4896
5373
 
4897
5374
  for (const file of files) {
4898
- if (!file.endsWith(".ts") && !file.endsWith(".tsx")) continue;
4899
- if (file.endsWith(".d.ts") || isWorkflowTestFile(file)) continue;
5375
+ if (!isAnalysisSourceFile(file) || isWorkflowTestFile(file)) continue;
4900
5376
 
4901
5377
  const source = await readFile(path.join(targetDir, file), "utf8");
4902
5378
  if (!source.includes("defineRuntimeManifest")) continue;
@@ -5033,12 +5509,18 @@ async function listenedEventNames(
5033
5509
  ): Promise<Set<string>> {
5034
5510
  const featuresPath = directoryPath(config.paths.features);
5035
5511
  const listenerPattern = new RegExp(
5036
- `^${escapeRegExp(featuresPath)}/[^/]+/listeners/[^/]+\\.ts$`,
5512
+ `^${escapeRegExp(featuresPath)}/[^/]+/listeners/[^/]+$`,
5037
5513
  );
5038
5514
  const listened = new Set<string>();
5039
5515
 
5040
5516
  for (const file of files) {
5041
- if (!listenerPattern.test(file) || isWorkflowTestFile(file)) continue;
5517
+ if (
5518
+ !isAnalysisSourceFile(file) ||
5519
+ isWorkflowTestFile(file) ||
5520
+ !listenerPattern.test(stripSourceFileExtension(file))
5521
+ ) {
5522
+ continue;
5523
+ }
5042
5524
 
5043
5525
  const source = await readFile(path.join(targetDir, file), "utf8");
5044
5526
  const callPattern = /\bdefineListener\s*\(/g;
@@ -5087,8 +5569,7 @@ async function listenerWiringReferences(
5087
5569
  const listenerRegistryFile = listenersFilePath(config);
5088
5570
 
5089
5571
  for (const file of files) {
5090
- if (!file.endsWith(".ts") && !file.endsWith(".tsx")) continue;
5091
- if (file.endsWith(".d.ts") || isWorkflowTestFile(file)) continue;
5572
+ if (!isAnalysisSourceFile(file) || isWorkflowTestFile(file)) continue;
5092
5573
 
5093
5574
  const source = await readFile(path.join(targetDir, file), "utf8");
5094
5575
  const namedImports = parseNamedImportSources(source);
@@ -5097,7 +5578,7 @@ async function listenerWiringReferences(
5097
5578
  source,
5098
5579
  ts.ScriptTarget.Latest,
5099
5580
  true,
5100
- file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
5581
+ sourceFileScriptKind(file),
5101
5582
  );
5102
5583
  const registerListenersBindings = importedRegisterListenersBindings(
5103
5584
  source,
@@ -5557,10 +6038,7 @@ function callReferencesCentralListenerRegistry(args: {
5557
6038
 
5558
6039
  function isWorkflowTestFile(file: string): boolean {
5559
6040
  return (
5560
- file.endsWith(".test.ts") ||
5561
- file.endsWith(".test.tsx") ||
5562
- file.startsWith("tests/") ||
5563
- file.includes("/tests/")
6041
+ isAppTestFile(file) || file.startsWith("tests/") || file.includes("/tests/")
5564
6042
  );
5565
6043
  }
5566
6044
 
@@ -5575,13 +6053,7 @@ async function inspectServerlessFootguns(
5575
6053
  const cronDirectory = path.posix.join(routeDirectory, "cron");
5576
6054
 
5577
6055
  for (const file of files) {
5578
- if (
5579
- !file.endsWith(".ts") ||
5580
- file.endsWith(".test.ts") ||
5581
- file.endsWith(".d.ts")
5582
- ) {
5583
- continue;
5584
- }
6056
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file)) continue;
5585
6057
 
5586
6058
  const source = await readFile(path.join(targetDir, file), "utf8");
5587
6059
  const isInfraOrServerFile = isRuntimeFile(file, config);
@@ -5727,9 +6199,10 @@ function fileWithinDirectory(file: string, directory: string): boolean {
5727
6199
  }
5728
6200
 
5729
6201
  function isProviderLifecycleFile(file: string, source?: string): boolean {
6202
+ const sourcePath = stripSourceFileExtension(file);
5730
6203
  return (
5731
- /(^|\/)provider(s)?\.ts$/.test(file) ||
5732
- /(^|\/)[^/]*provider[^/]*\.ts$/.test(file) ||
6204
+ /(^|\/)provider(s)?$/.test(sourcePath) ||
6205
+ /(^|\/)[^/]*provider[^/]*$/.test(sourcePath) ||
5733
6206
  /(^|\/)providers?\//.test(file) ||
5734
6207
  (source !== undefined && containsCallExpression(source, "createProvider"))
5735
6208
  );
@@ -5740,8 +6213,9 @@ function isNextRouteFile(file: string, config: ResolvedBeignetConfig): boolean {
5740
6213
 
5741
6214
  return (
5742
6215
  file.startsWith(`${routesPath}/`) &&
5743
- file.endsWith("/route.ts") &&
5744
- !file.endsWith(".test.ts")
6216
+ isSourceFileNamed(file, "route") &&
6217
+ [".js", ".jsx", ".ts", ".tsx"].includes(path.posix.extname(file)) &&
6218
+ !isAppTestFile(file)
5745
6219
  );
5746
6220
  }
5747
6221
 
@@ -7676,7 +8150,13 @@ async function inspectFeatureRouteRegistration(
7676
8150
  const feature = featureNameFromContractFile(contract.file, config);
7677
8151
  if (!feature) continue;
7678
8152
 
7679
- const routeFile = `${featuresPath}/${feature}/routes.ts`;
8153
+ const routeStem = `${featuresPath}/${feature}/routes`;
8154
+ const routeFile =
8155
+ files.find(
8156
+ (file) =>
8157
+ isAnalysisSourceFile(file) &&
8158
+ stripSourceFileExtension(file) === routeStem,
8159
+ ) ?? `${routeStem}${path.posix.extname(contract.file) || ".ts"}`;
7680
8160
  const featureRouteGroups = routeGroups.filter(
7681
8161
  (routeGroup) => routeGroup.file === routeFile,
7682
8162
  );
@@ -7766,13 +8246,14 @@ async function readFeatureRouteGroups(
7766
8246
  ): Promise<FeatureRouteGroup[]> {
7767
8247
  const featuresPath = `${directoryPath(config.paths.features)}/`;
7768
8248
  const routeFiles = files.filter(
7769
- (file) => file.startsWith(featuresPath) && file.endsWith("/routes.ts"),
8249
+ (file) =>
8250
+ file.startsWith(featuresPath) && isSourceFileNamed(file, "routes"),
7770
8251
  );
7771
8252
  const routeGroups: FeatureRouteGroup[] = [];
7772
8253
 
7773
8254
  for (const file of routeFiles) {
7774
8255
  const source = await readFile(path.join(targetDir, file), "utf8");
7775
- const imports = parseNamedImports(source, config, file);
8256
+ const imports = parseNamedImports(source, config, file, files);
7776
8257
  routeGroups.push(...parseFeatureRouteGroups(source, file, imports));
7777
8258
  }
7778
8259
 
@@ -7935,8 +8416,7 @@ function findRouteGroupContract(
7935
8416
  contract.exportName === routeGroupContract.exportName &&
7936
8417
  (!routeGroupContract.file ||
7937
8418
  contract.file === routeGroupContract.file ||
7938
- contract.file ===
7939
- routeGroupContract.file.replace(/\.ts$/, "/index.ts")),
8419
+ contract.file === directoryIndexSourceFile(routeGroupContract.file)),
7940
8420
  );
7941
8421
  }
7942
8422
 
@@ -9313,7 +9793,7 @@ async function readContractLists(
9313
9793
 
9314
9794
  for (const file of contractFiles) {
9315
9795
  const source = await readFile(path.join(targetDir, file), "utf8");
9316
- const imports = parseNamedImports(source, config, file);
9796
+ const imports = parseNamedImports(source, config, file, files);
9317
9797
  for (const list of parseContractLists(source, file, imports)) {
9318
9798
  lists.set(contractListKey(list.file, list.name), list);
9319
9799
  }
@@ -9330,18 +9810,18 @@ function contractSourceFiles(
9330
9810
  if (usesFeatureOwnedContracts(config)) {
9331
9811
  return files.filter(
9332
9812
  (file) =>
9333
- new RegExp(
9334
- `^${escapeRegExp(contractsPath)}/[^/]+/contracts\\.ts$`,
9335
- ).test(file) && !file.endsWith(".test.ts"),
9813
+ isAnalysisSourceFile(file) &&
9814
+ !isAppTestFile(file) &&
9815
+ new RegExp(`^${escapeRegExp(contractsPath)}/[^/]+/contracts$`).test(
9816
+ stripSourceFileExtension(file),
9817
+ ),
9336
9818
  );
9337
9819
  }
9338
9820
 
9339
- return files.filter(
9340
- (file) =>
9341
- file.startsWith(`${contractsPath}/`) &&
9342
- file.endsWith(".ts") &&
9343
- !file.endsWith(".test.ts"),
9344
- );
9821
+ return files.filter((file) => {
9822
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file)) return false;
9823
+ return stripSourceFileExtension(file).startsWith(`${contractsPath}/`);
9824
+ });
9345
9825
  }
9346
9826
 
9347
9827
  function contractListSourceFiles(
@@ -9351,22 +9831,23 @@ function contractListSourceFiles(
9351
9831
  const contractsPath = directoryPath(config.paths.contracts);
9352
9832
  if (usesFeatureOwnedContracts(config)) {
9353
9833
  return files.filter((file) => {
9354
- if (!file.endsWith(".ts") || file.endsWith(".test.ts")) return false;
9355
- if (file === `${contractsPath}/index.ts`) return true;
9356
- if (file === `${contractsPath}/contracts.ts`) return true;
9834
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file)) return false;
9835
+ const sourcePath = stripSourceFileExtension(file);
9836
+ if (sourcePath === `${contractsPath}/index`) return true;
9837
+ if (sourcePath === `${contractsPath}/contracts`) return true;
9357
9838
  return new RegExp(
9358
- `^${escapeRegExp(contractsPath)}/[^/]+/contracts\\.ts$`,
9359
- ).test(file);
9839
+ `^${escapeRegExp(contractsPath)}/[^/]+/contracts$`,
9840
+ ).test(sourcePath);
9360
9841
  });
9361
9842
  }
9362
9843
 
9363
- return files.filter(
9364
- (file) =>
9365
- (file === `${contractsPath}.ts` ||
9366
- file.startsWith(`${contractsPath}/`)) &&
9367
- file.endsWith(".ts") &&
9368
- !file.endsWith(".test.ts"),
9369
- );
9844
+ return files.filter((file) => {
9845
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file)) return false;
9846
+ const sourcePath = stripSourceFileExtension(file);
9847
+ return (
9848
+ sourcePath === contractsPath || sourcePath.startsWith(`${contractsPath}/`)
9849
+ );
9850
+ });
9370
9851
  }
9371
9852
 
9372
9853
  function parseContractLists(
@@ -9420,7 +9901,7 @@ function resolveContractList(
9420
9901
  if (seen.has(key)) return new Set();
9421
9902
  const list =
9422
9903
  lists.get(key) ??
9423
- lists.get(contractListKey(file.replace(/\.ts$/, "/index.ts"), name));
9904
+ lists.get(contractListKey(directoryIndexSourceFile(file), name));
9424
9905
  if (!list) return undefined;
9425
9906
 
9426
9907
  seen.add(key);
@@ -10010,7 +10491,7 @@ function nextRoutePath(file: string, routeRoot: string): string {
10010
10491
  const normalizedRouteRoot = directoryPath(routeRoot);
10011
10492
  const routeSegments = file
10012
10493
  .slice(normalizedRouteRoot.length + 1)
10013
- .replace(/\/route\.ts$/, "")
10494
+ .replace(/\/route\.(?:js|jsx|ts|tsx)$/, "")
10014
10495
  .split("/")
10015
10496
  .filter(Boolean);
10016
10497
  const apiPath = routeSegments.map(nextSegmentToContractSegment).join("/");