@beignet/cli 0.0.51 → 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 (87) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +51 -18
  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.map +1 -1
  31. package/dist/index.js +3 -3
  32. package/dist/index.js.map +1 -1
  33. package/dist/inspect.d.ts.map +1 -1
  34. package/dist/inspect.js +339 -112
  35. package/dist/inspect.js.map +1 -1
  36. package/dist/lint.d.ts.map +1 -1
  37. package/dist/lint.js +3 -6
  38. package/dist/lint.js.map +1 -1
  39. package/dist/mcp.js +1 -1
  40. package/dist/mcp.js.map +1 -1
  41. package/dist/templates/agents.d.ts.map +1 -1
  42. package/dist/templates/agents.js +36 -13
  43. package/dist/templates/agents.js.map +1 -1
  44. package/dist/templates/base.js +5 -5
  45. package/dist/templates/db/mysql.d.ts.map +1 -1
  46. package/dist/templates/db/mysql.js +51 -22
  47. package/dist/templates/db/mysql.js.map +1 -1
  48. package/dist/templates/db/postgres.d.ts.map +1 -1
  49. package/dist/templates/db/postgres.js +60 -20
  50. package/dist/templates/db/postgres.js.map +1 -1
  51. package/dist/templates/db/sqlite.d.ts.map +1 -1
  52. package/dist/templates/db/sqlite.js +51 -23
  53. package/dist/templates/db/sqlite.js.map +1 -1
  54. package/dist/templates/shadcn.js +1 -1
  55. package/dist/templates/shadcn.js.map +1 -1
  56. package/dist/templates/shared.js +4 -4
  57. package/dist/templates/shared.js.map +1 -1
  58. package/dist/templates/testing.d.ts.map +1 -1
  59. package/dist/templates/testing.js +7 -19
  60. package/dist/templates/testing.js.map +1 -1
  61. package/dist/test-discovery.d.ts +9 -0
  62. package/dist/test-discovery.d.ts.map +1 -0
  63. package/dist/test-discovery.js +40 -0
  64. package/dist/test-discovery.js.map +1 -0
  65. package/package.json +7 -5
  66. package/skills/app-structure/SKILL.md +32 -7
  67. package/src/analysis/layers.ts +4 -2
  68. package/src/analysis/port-wiring.ts +2 -1
  69. package/src/analysis/source-files.ts +61 -0
  70. package/src/analysis/workspace.ts +27 -31
  71. package/src/app-map-changes.ts +10 -8
  72. package/src/app-map.ts +43 -32
  73. package/src/check.ts +115 -25
  74. package/src/git-changes.ts +218 -86
  75. package/src/index.ts +8 -5
  76. package/src/inspect.ts +486 -141
  77. package/src/lint.ts +6 -5
  78. package/src/mcp.ts +1 -1
  79. package/src/templates/agents.ts +36 -13
  80. package/src/templates/base.ts +5 -5
  81. package/src/templates/db/mysql.ts +51 -22
  82. package/src/templates/db/postgres.ts +60 -20
  83. package/src/templates/db/sqlite.ts +51 -23
  84. package/src/templates/shadcn.ts +1 -1
  85. package/src/templates/shared.ts +4 -4
  86. package/src/templates/testing.ts +12 -19
  87. 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,6 +1590,7 @@ 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[] = [];
@@ -1581,9 +1599,14 @@ function parseRouteExports(
1581
1599
  source,
1582
1600
  ts.ScriptTarget.Latest,
1583
1601
  true,
1584
- ts.ScriptKind.TS,
1602
+ sourceFileScriptKind(handlerFile),
1603
+ );
1604
+ const imports = parseRouteNamedImports(
1605
+ sourceFile,
1606
+ config,
1607
+ handlerFile,
1608
+ files,
1585
1609
  );
1586
- const imports = parseRouteNamedImports(sourceFile, config, handlerFile);
1587
1610
 
1588
1611
  for (const statement of sourceFile.statements) {
1589
1612
  if (
@@ -1656,6 +1679,7 @@ function parseRouteNamedImports(
1656
1679
  sourceFile: ts.SourceFile,
1657
1680
  config: ResolvedBeignetConfig,
1658
1681
  handlerFile: string,
1682
+ files: string[],
1659
1683
  ): Map<string, { importedName: string; contractFile?: string }> {
1660
1684
  const imports = new Map<
1661
1685
  string,
@@ -1677,6 +1701,7 @@ function parseRouteNamedImports(
1677
1701
  statement.moduleSpecifier.text,
1678
1702
  config,
1679
1703
  handlerFile,
1704
+ files,
1680
1705
  );
1681
1706
  for (const element of statement.importClause.namedBindings.elements) {
1682
1707
  if (element.isTypeOnly) continue;
@@ -1757,6 +1782,7 @@ function parseNamedImports(
1757
1782
  source: string,
1758
1783
  config: ResolvedBeignetConfig,
1759
1784
  importerFile?: string,
1785
+ files?: string[],
1760
1786
  ): Map<
1761
1787
  string,
1762
1788
  {
@@ -1779,6 +1805,7 @@ function parseNamedImports(
1779
1805
  sourcePath,
1780
1806
  config,
1781
1807
  importerFile,
1808
+ files,
1782
1809
  );
1783
1810
  for (const member of match[1].split(",")) {
1784
1811
  const parsed = parseImportMember(member);
@@ -1849,9 +1876,7 @@ function sourceFileFromImport(
1849
1876
  files?: string[],
1850
1877
  ): string | undefined {
1851
1878
  const resolveCandidate = (candidate: string) => {
1852
- if (path.extname(candidate)) return candidate;
1853
-
1854
- const candidates = [`${candidate}.ts`, `${candidate}/index.ts`];
1879
+ const candidates = sourceFileSpecifierCandidates(candidate, importerFile);
1855
1880
  return files
1856
1881
  ? candidates.find((file) => files.includes(file))
1857
1882
  : candidates[0];
@@ -1870,33 +1895,76 @@ function sourceFileFromImport(
1870
1895
  return undefined;
1871
1896
  }
1872
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
+
1873
1958
  function contractFileFromImport(
1874
1959
  sourcePath: string,
1875
1960
  config: ResolvedBeignetConfig,
1876
1961
  importerFile?: string,
1962
+ files?: string[],
1877
1963
  ): string | undefined {
1878
1964
  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;
1965
+ const candidates = sourceFileSpecifierCandidates(candidate, importerFile);
1966
+ if (!files) return candidates[0];
1967
+ return candidates.find((file) => files.includes(file)) ?? candidates[0];
1900
1968
  };
1901
1969
  const contractsPath = directoryPath(config.paths.contracts);
1902
1970
  const aliasPrefix = `@/${contractsPath}/`;
@@ -1905,7 +1973,7 @@ function contractFileFromImport(
1905
1973
  const relativeExact = contractsPath;
1906
1974
 
1907
1975
  if (sourcePath === aliasExact || sourcePath === relativeExact) {
1908
- return `${contractsPath}/index.ts`;
1976
+ return resolveCandidate(`${contractsPath}/index`);
1909
1977
  }
1910
1978
  if (sourcePath.startsWith(aliasPrefix)) {
1911
1979
  return resolveCandidate(sourcePath.slice("@/".length));
@@ -2002,9 +2070,14 @@ async function inspectRouteSurface(
2002
2070
  const routeFiles = files.filter(
2003
2071
  (file) =>
2004
2072
  file.startsWith(`${directoryPath(config.paths.routes)}/`) &&
2005
- file.endsWith("/route.ts"),
2073
+ isNextRouteFile(file, config),
2074
+ );
2075
+ const routeExports = await readRouteExports(
2076
+ targetDir,
2077
+ routeFiles,
2078
+ files,
2079
+ config,
2006
2080
  );
2007
- const routeExports = await readRouteExports(targetDir, routeFiles, config);
2008
2081
  return matchRoutes(contracts, routeExports);
2009
2082
  }
2010
2083
 
@@ -2076,8 +2149,7 @@ function findContracts(
2076
2149
  contract.exportName === routeExport.contractRef &&
2077
2150
  (!routeExport.contractFile ||
2078
2151
  contract.file === routeExport.contractFile ||
2079
- contract.file ===
2080
- routeExport.contractFile.replace(/\.ts$/, "/index.ts")),
2152
+ contract.file === directoryIndexSourceFile(routeExport.contractFile)),
2081
2153
  );
2082
2154
  return contract ? [contract] : [];
2083
2155
  }
@@ -2521,7 +2593,12 @@ async function inspectCanonicalConformance(
2521
2593
 
2522
2594
  const featureRoutesPath = `${featuresPath}/`;
2523
2595
  for (const file of files) {
2524
- 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
+ ) {
2525
2602
  continue;
2526
2603
  }
2527
2604
 
@@ -2910,7 +2987,7 @@ async function inspectProductionReadiness(
2910
2987
  let hasSecurityHeadersHook = false;
2911
2988
 
2912
2989
  for (const file of files) {
2913
- if (!file.endsWith(".ts") || file.endsWith(".test.ts")) continue;
2990
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file)) continue;
2914
2991
 
2915
2992
  const source = await readCachedSource(targetDir, file, sourceCache);
2916
2993
  const routeServerOrInfra = isRouteServerOrInfraFile(file, config);
@@ -2937,7 +3014,7 @@ async function inspectProductionReadiness(
2937
3014
 
2938
3015
  if (
2939
3016
  fileWithinDirectory(file, cronDirectory) &&
2940
- file.endsWith("/route.ts") &&
3017
+ isNextRouteFile(file, config) &&
2941
3018
  !/\bCRON_SECRET\b/.test(source) &&
2942
3019
  !/\bcreateOutboxDrainRoute\s*\(/.test(source) &&
2943
3020
  !/\bcreateScheduleRoute\s*\(/.test(source)
@@ -3239,6 +3316,25 @@ async function inspectProductionReadiness(
3239
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.",
3240
3317
  });
3241
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
+ }
3242
3338
  }
3243
3339
 
3244
3340
  const installedReadinessProviders = [...installedPackages].filter(
@@ -3284,7 +3380,7 @@ async function uploadHasExplicitSizeLimit(
3284
3380
  );
3285
3381
  if (!importedName) continue;
3286
3382
 
3287
- const importedFile = resolveLocalTypeScriptModule(file, moduleName, files);
3383
+ const importedFile = sourceFileFromImport(moduleName, file, files);
3288
3384
  if (!importedFile) continue;
3289
3385
 
3290
3386
  const importedSource = await readCachedSource(
@@ -3339,20 +3435,6 @@ function exportedObjectHasProperty(
3339
3435
  return new RegExp(`\\b${escapeRegExp(propertyName)}\\s*:`).test(initializer);
3340
3436
  }
3341
3437
 
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
3438
  async function hasReadinessCheck(
3357
3439
  targetDir: string,
3358
3440
  files: string[],
@@ -3360,10 +3442,18 @@ async function hasReadinessCheck(
3360
3442
  sourceCache: Map<string, string>,
3361
3443
  ): Promise<boolean> {
3362
3444
  const routesDir = directoryPath(config.paths.routes);
3363
- 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
+ }
3364
3454
 
3365
3455
  for (const file of files) {
3366
- if (!file.endsWith(".ts") || file.endsWith(".test.ts")) continue;
3456
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file)) continue;
3367
3457
  if (!isRouteServerOrInfraFile(file, config)) continue;
3368
3458
 
3369
3459
  const source = await readCachedSource(targetDir, file, sourceCache);
@@ -3905,10 +3995,11 @@ function isFeatureUploadDefinition(
3905
3995
  file: string,
3906
3996
  config: ResolvedBeignetConfig,
3907
3997
  ): boolean {
3908
- const featuresPath = directoryPath(config.paths.features);
3909
- return new RegExp(
3910
- `^${escapeRegExp(featuresPath)}/[^/]+/uploads/[^/]+\\.ts$`,
3911
- ).test(file);
3998
+ return isFeatureArtifactFile(
3999
+ file,
4000
+ directoryPath(config.paths.features),
4001
+ "uploads",
4002
+ );
3912
4003
  }
3913
4004
 
3914
4005
  function operationalConfigFiles(
@@ -3918,17 +4009,263 @@ function operationalConfigFiles(
3918
4009
  return providerOperationalConfigFiles(files, config);
3919
4010
  }
3920
4011
 
3921
- 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(
3922
4117
  targetDir: string,
3923
4118
  files: string[],
3924
- ): Promise<
3925
- | {
3926
- dependencies?: Record<string, string>;
3927
- devDependencies?: Record<string, string>;
3928
- 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;
3929
4155
  }
3930
- | undefined
3931
- > {
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> {
3932
4269
  if (!files.includes("package.json")) return undefined;
3933
4270
 
3934
4271
  return JSON.parse(
@@ -4089,7 +4426,7 @@ async function hasBillingEntitlementsProviderWiring(
4089
4426
  const serverPath = path.dirname(config.paths.server);
4090
4427
  const candidateFiles = files.filter(
4091
4428
  (file) =>
4092
- /\.(?:ts|tsx|mts|cts)$/.test(file) &&
4429
+ isAnalysisSourceFile(file) &&
4093
4430
  !isTestSourceFile(file) &&
4094
4431
  (file.startsWith(`${infraPath}/`) || file.startsWith(`${serverPath}/`)),
4095
4432
  );
@@ -4105,10 +4442,7 @@ async function hasBillingEntitlementsProviderWiring(
4105
4442
  }
4106
4443
 
4107
4444
  function isTestSourceFile(file: string): boolean {
4108
- return (
4109
- /(?:^|\/)tests\//.test(file) ||
4110
- /\.(?:test|spec)\.(?:ts|tsx|mts|cts)$/.test(file)
4111
- );
4445
+ return /(?:^|\/)tests\//.test(file) || isAppTestFile(file);
4112
4446
  }
4113
4447
 
4114
4448
  function exportedConstInitializer(
@@ -4135,8 +4469,8 @@ function featureNameFromContractFile(
4135
4469
  ): string | undefined {
4136
4470
  const featuresPath = directoryPath(config.paths.features);
4137
4471
  const match = new RegExp(
4138
- `^${escapeRegExp(featuresPath)}/([^/]+)/contracts\\.ts$`,
4139
- ).exec(file);
4472
+ `^${escapeRegExp(featuresPath)}/([^/]+)/contracts$`,
4473
+ ).exec(stripSourceFileExtension(file));
4140
4474
  return match?.[1];
4141
4475
  }
4142
4476
 
@@ -4153,7 +4487,7 @@ function featureRuntimeFiles(
4153
4487
  "use-cases",
4154
4488
  ];
4155
4489
  return files.filter((file) => {
4156
- if (!file.endsWith(".ts") || file.endsWith(".test.ts")) return false;
4490
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file)) return false;
4157
4491
  return runtimeFolders.some((folder) =>
4158
4492
  file.startsWith(`${featuresPath}/${feature}/${folder}/`),
4159
4493
  );
@@ -4169,11 +4503,16 @@ function featureTestFiles(
4169
4503
  return files.filter((file) => {
4170
4504
  if (
4171
4505
  !file.startsWith(`${featuresPath}/${feature}/`) ||
4172
- !file.endsWith(".test.ts")
4506
+ !isAppTestFile(file)
4173
4507
  ) {
4174
4508
  return false;
4175
4509
  }
4176
- 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
+ );
4177
4516
  });
4178
4517
  }
4179
4518
 
@@ -4340,17 +4679,12 @@ async function inspectFeatureArtifactDrift(
4340
4679
  severity: "warning",
4341
4680
  code: "BEIGNET_FEATURE_TEST_PLACEMENT",
4342
4681
  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.`,
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.`,
4344
4683
  });
4345
4684
  }
4346
4685
 
4347
4686
  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
- }
4687
+ if (!isAnalysisSourceFile(file)) continue;
4354
4688
 
4355
4689
  const misplaced = directFeatureTestMatch(file, featuresPath)
4356
4690
  ? undefined
@@ -4365,7 +4699,6 @@ async function inspectFeatureArtifactDrift(
4365
4699
  }
4366
4700
 
4367
4701
  if (isTestSourceFile(file)) continue;
4368
- if (!file.endsWith(".ts")) continue;
4369
4702
 
4370
4703
  if (
4371
4704
  isNotificationDefinitionFile(file, featuresPath) ||
@@ -4391,9 +4724,10 @@ function directFeatureTestMatch(
4391
4724
  file: string,
4392
4725
  featuresPath: string,
4393
4726
  ): RegExpExecArray | null {
4727
+ if (!isAppTestFile(file)) return null;
4394
4728
  return new RegExp(
4395
- `^${escapeRegExp(featuresPath)}/([^/]+)/[^/]+\\.(?:test|spec)\\.(?:ts|tsx|mts|cts)$`,
4396
- ).exec(file);
4729
+ `^${escapeRegExp(featuresPath)}/([^/]+)/[^/]+\\.(?:test|spec)$`,
4730
+ ).exec(stripSourceFileExtension(file));
4397
4731
  }
4398
4732
 
4399
4733
  function isFeatureArtifactFile(
@@ -4401,9 +4735,10 @@ function isFeatureArtifactFile(
4401
4735
  featuresPath: string,
4402
4736
  folder: "uploads" | "notifications",
4403
4737
  ): boolean {
4738
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file)) return false;
4404
4739
  const match = new RegExp(
4405
- `^${escapeRegExp(featuresPath)}/[^/]+/${folder}/([^/]+)\\.ts$`,
4406
- ).exec(file);
4740
+ `^${escapeRegExp(featuresPath)}/[^/]+/${folder}/([^/]+)$`,
4741
+ ).exec(stripSourceFileExtension(file));
4407
4742
  return Boolean(match && match[1] !== "index");
4408
4743
  }
4409
4744
 
@@ -4413,7 +4748,7 @@ async function appHasUploadRoute(
4413
4748
  config: ResolvedBeignetConfig,
4414
4749
  ): Promise<boolean> {
4415
4750
  for (const file of files) {
4416
- if (!file.endsWith(".ts") || file.endsWith(".test.ts")) continue;
4751
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file)) continue;
4417
4752
  if (!isRouteServerOrInfraFile(file, config)) continue;
4418
4753
 
4419
4754
  const source = await readFile(path.join(targetDir, file), "utf8");
@@ -4967,11 +5302,17 @@ async function readFeatureWorkflowRegistries(
4967
5302
  for (const kind of kinds) {
4968
5303
  const folder = kind === "events" ? "domain/events" : kind;
4969
5304
  const pattern = new RegExp(
4970
- `^${escapeRegExp(featuresPath)}/[^/]+/${escapeRegExp(folder)}/index\\.ts$`,
5305
+ `^${escapeRegExp(featuresPath)}/[^/]+/${escapeRegExp(folder)}/index$`,
4971
5306
  );
4972
5307
 
4973
5308
  for (const file of files) {
4974
- if (!pattern.test(file)) continue;
5309
+ if (
5310
+ !isAnalysisSourceFile(file) ||
5311
+ isWorkflowTestFile(file) ||
5312
+ !pattern.test(stripSourceFileExtension(file))
5313
+ ) {
5314
+ continue;
5315
+ }
4975
5316
 
4976
5317
  const source = await readFile(path.join(targetDir, file), "utf8");
4977
5318
  const memberFiles = exportedMemberFiles(source, file, files);
@@ -5031,8 +5372,7 @@ async function readRuntimeManifestIdentifiers(
5031
5372
  const manifests: RuntimeManifestIdentifierSets[] = [];
5032
5373
 
5033
5374
  for (const file of files) {
5034
- if (!file.endsWith(".ts") && !file.endsWith(".tsx")) continue;
5035
- if (file.endsWith(".d.ts") || isWorkflowTestFile(file)) continue;
5375
+ if (!isAnalysisSourceFile(file) || isWorkflowTestFile(file)) continue;
5036
5376
 
5037
5377
  const source = await readFile(path.join(targetDir, file), "utf8");
5038
5378
  if (!source.includes("defineRuntimeManifest")) continue;
@@ -5169,12 +5509,18 @@ async function listenedEventNames(
5169
5509
  ): Promise<Set<string>> {
5170
5510
  const featuresPath = directoryPath(config.paths.features);
5171
5511
  const listenerPattern = new RegExp(
5172
- `^${escapeRegExp(featuresPath)}/[^/]+/listeners/[^/]+\\.ts$`,
5512
+ `^${escapeRegExp(featuresPath)}/[^/]+/listeners/[^/]+$`,
5173
5513
  );
5174
5514
  const listened = new Set<string>();
5175
5515
 
5176
5516
  for (const file of files) {
5177
- if (!listenerPattern.test(file) || isWorkflowTestFile(file)) continue;
5517
+ if (
5518
+ !isAnalysisSourceFile(file) ||
5519
+ isWorkflowTestFile(file) ||
5520
+ !listenerPattern.test(stripSourceFileExtension(file))
5521
+ ) {
5522
+ continue;
5523
+ }
5178
5524
 
5179
5525
  const source = await readFile(path.join(targetDir, file), "utf8");
5180
5526
  const callPattern = /\bdefineListener\s*\(/g;
@@ -5223,8 +5569,7 @@ async function listenerWiringReferences(
5223
5569
  const listenerRegistryFile = listenersFilePath(config);
5224
5570
 
5225
5571
  for (const file of files) {
5226
- if (!file.endsWith(".ts") && !file.endsWith(".tsx")) continue;
5227
- if (file.endsWith(".d.ts") || isWorkflowTestFile(file)) continue;
5572
+ if (!isAnalysisSourceFile(file) || isWorkflowTestFile(file)) continue;
5228
5573
 
5229
5574
  const source = await readFile(path.join(targetDir, file), "utf8");
5230
5575
  const namedImports = parseNamedImportSources(source);
@@ -5233,7 +5578,7 @@ async function listenerWiringReferences(
5233
5578
  source,
5234
5579
  ts.ScriptTarget.Latest,
5235
5580
  true,
5236
- file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
5581
+ sourceFileScriptKind(file),
5237
5582
  );
5238
5583
  const registerListenersBindings = importedRegisterListenersBindings(
5239
5584
  source,
@@ -5693,10 +6038,7 @@ function callReferencesCentralListenerRegistry(args: {
5693
6038
 
5694
6039
  function isWorkflowTestFile(file: string): boolean {
5695
6040
  return (
5696
- file.endsWith(".test.ts") ||
5697
- file.endsWith(".test.tsx") ||
5698
- file.startsWith("tests/") ||
5699
- file.includes("/tests/")
6041
+ isAppTestFile(file) || file.startsWith("tests/") || file.includes("/tests/")
5700
6042
  );
5701
6043
  }
5702
6044
 
@@ -5711,13 +6053,7 @@ async function inspectServerlessFootguns(
5711
6053
  const cronDirectory = path.posix.join(routeDirectory, "cron");
5712
6054
 
5713
6055
  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
- }
6056
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file)) continue;
5721
6057
 
5722
6058
  const source = await readFile(path.join(targetDir, file), "utf8");
5723
6059
  const isInfraOrServerFile = isRuntimeFile(file, config);
@@ -5863,9 +6199,10 @@ function fileWithinDirectory(file: string, directory: string): boolean {
5863
6199
  }
5864
6200
 
5865
6201
  function isProviderLifecycleFile(file: string, source?: string): boolean {
6202
+ const sourcePath = stripSourceFileExtension(file);
5866
6203
  return (
5867
- /(^|\/)provider(s)?\.ts$/.test(file) ||
5868
- /(^|\/)[^/]*provider[^/]*\.ts$/.test(file) ||
6204
+ /(^|\/)provider(s)?$/.test(sourcePath) ||
6205
+ /(^|\/)[^/]*provider[^/]*$/.test(sourcePath) ||
5869
6206
  /(^|\/)providers?\//.test(file) ||
5870
6207
  (source !== undefined && containsCallExpression(source, "createProvider"))
5871
6208
  );
@@ -5876,8 +6213,9 @@ function isNextRouteFile(file: string, config: ResolvedBeignetConfig): boolean {
5876
6213
 
5877
6214
  return (
5878
6215
  file.startsWith(`${routesPath}/`) &&
5879
- file.endsWith("/route.ts") &&
5880
- !file.endsWith(".test.ts")
6216
+ isSourceFileNamed(file, "route") &&
6217
+ [".js", ".jsx", ".ts", ".tsx"].includes(path.posix.extname(file)) &&
6218
+ !isAppTestFile(file)
5881
6219
  );
5882
6220
  }
5883
6221
 
@@ -7812,7 +8150,13 @@ async function inspectFeatureRouteRegistration(
7812
8150
  const feature = featureNameFromContractFile(contract.file, config);
7813
8151
  if (!feature) continue;
7814
8152
 
7815
- 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"}`;
7816
8160
  const featureRouteGroups = routeGroups.filter(
7817
8161
  (routeGroup) => routeGroup.file === routeFile,
7818
8162
  );
@@ -7902,13 +8246,14 @@ async function readFeatureRouteGroups(
7902
8246
  ): Promise<FeatureRouteGroup[]> {
7903
8247
  const featuresPath = `${directoryPath(config.paths.features)}/`;
7904
8248
  const routeFiles = files.filter(
7905
- (file) => file.startsWith(featuresPath) && file.endsWith("/routes.ts"),
8249
+ (file) =>
8250
+ file.startsWith(featuresPath) && isSourceFileNamed(file, "routes"),
7906
8251
  );
7907
8252
  const routeGroups: FeatureRouteGroup[] = [];
7908
8253
 
7909
8254
  for (const file of routeFiles) {
7910
8255
  const source = await readFile(path.join(targetDir, file), "utf8");
7911
- const imports = parseNamedImports(source, config, file);
8256
+ const imports = parseNamedImports(source, config, file, files);
7912
8257
  routeGroups.push(...parseFeatureRouteGroups(source, file, imports));
7913
8258
  }
7914
8259
 
@@ -8071,8 +8416,7 @@ function findRouteGroupContract(
8071
8416
  contract.exportName === routeGroupContract.exportName &&
8072
8417
  (!routeGroupContract.file ||
8073
8418
  contract.file === routeGroupContract.file ||
8074
- contract.file ===
8075
- routeGroupContract.file.replace(/\.ts$/, "/index.ts")),
8419
+ contract.file === directoryIndexSourceFile(routeGroupContract.file)),
8076
8420
  );
8077
8421
  }
8078
8422
 
@@ -9449,7 +9793,7 @@ async function readContractLists(
9449
9793
 
9450
9794
  for (const file of contractFiles) {
9451
9795
  const source = await readFile(path.join(targetDir, file), "utf8");
9452
- const imports = parseNamedImports(source, config, file);
9796
+ const imports = parseNamedImports(source, config, file, files);
9453
9797
  for (const list of parseContractLists(source, file, imports)) {
9454
9798
  lists.set(contractListKey(list.file, list.name), list);
9455
9799
  }
@@ -9466,18 +9810,18 @@ function contractSourceFiles(
9466
9810
  if (usesFeatureOwnedContracts(config)) {
9467
9811
  return files.filter(
9468
9812
  (file) =>
9469
- new RegExp(
9470
- `^${escapeRegExp(contractsPath)}/[^/]+/contracts\\.ts$`,
9471
- ).test(file) && !file.endsWith(".test.ts"),
9813
+ isAnalysisSourceFile(file) &&
9814
+ !isAppTestFile(file) &&
9815
+ new RegExp(`^${escapeRegExp(contractsPath)}/[^/]+/contracts$`).test(
9816
+ stripSourceFileExtension(file),
9817
+ ),
9472
9818
  );
9473
9819
  }
9474
9820
 
9475
- return files.filter(
9476
- (file) =>
9477
- file.startsWith(`${contractsPath}/`) &&
9478
- file.endsWith(".ts") &&
9479
- !file.endsWith(".test.ts"),
9480
- );
9821
+ return files.filter((file) => {
9822
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file)) return false;
9823
+ return stripSourceFileExtension(file).startsWith(`${contractsPath}/`);
9824
+ });
9481
9825
  }
9482
9826
 
9483
9827
  function contractListSourceFiles(
@@ -9487,22 +9831,23 @@ function contractListSourceFiles(
9487
9831
  const contractsPath = directoryPath(config.paths.contracts);
9488
9832
  if (usesFeatureOwnedContracts(config)) {
9489
9833
  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;
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;
9493
9838
  return new RegExp(
9494
- `^${escapeRegExp(contractsPath)}/[^/]+/contracts\\.ts$`,
9495
- ).test(file);
9839
+ `^${escapeRegExp(contractsPath)}/[^/]+/contracts$`,
9840
+ ).test(sourcePath);
9496
9841
  });
9497
9842
  }
9498
9843
 
9499
- return files.filter(
9500
- (file) =>
9501
- (file === `${contractsPath}.ts` ||
9502
- file.startsWith(`${contractsPath}/`)) &&
9503
- file.endsWith(".ts") &&
9504
- !file.endsWith(".test.ts"),
9505
- );
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
+ });
9506
9851
  }
9507
9852
 
9508
9853
  function parseContractLists(
@@ -9556,7 +9901,7 @@ function resolveContractList(
9556
9901
  if (seen.has(key)) return new Set();
9557
9902
  const list =
9558
9903
  lists.get(key) ??
9559
- lists.get(contractListKey(file.replace(/\.ts$/, "/index.ts"), name));
9904
+ lists.get(contractListKey(directoryIndexSourceFile(file), name));
9560
9905
  if (!list) return undefined;
9561
9906
 
9562
9907
  seen.add(key);
@@ -10146,7 +10491,7 @@ function nextRoutePath(file: string, routeRoot: string): string {
10146
10491
  const normalizedRouteRoot = directoryPath(routeRoot);
10147
10492
  const routeSegments = file
10148
10493
  .slice(normalizedRouteRoot.length + 1)
10149
- .replace(/\/route\.ts$/, "")
10494
+ .replace(/\/route\.(?:js|jsx|ts|tsx)$/, "")
10150
10495
  .split("/")
10151
10496
  .filter(Boolean);
10152
10497
  const apiPath = routeSegments.map(nextSegmentToContractSegment).join("/");