@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/dist/inspect.js CHANGED
@@ -1,10 +1,13 @@
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 { contractFactoryBindings, isImportedContractFactoryCall, } from "./analysis/contract-factories.js";
6
8
  import { canonicalFeatureLayerPath } from "./analysis/layers.js";
7
9
  import { analyzePortWiringFiles } from "./analysis/port-wiring.js";
10
+ import { analysisSourceFileExtensions, isAnalysisSourceFile, isAnalysisSourceFileExtension, isJavaScriptSourceFile, javascriptAnalysisSourceFileExtensions, sourceFileScriptKind, stripSourceFileExtension, typescriptAnalysisSourceFileExtensions, } from "./analysis/source-files.js";
8
11
  import { exportedVariableDeclarations } from "./analysis/source-index.js";
9
12
  import { createPainter } from "./ansi.js";
10
13
  import { clientDirPath, clientFormsPath, clientIndexPath, defaultBeignetConfig, directoryPath, loadBeignetConfig, normalizePath, resolveConfig, } from "./config.js";
@@ -16,6 +19,7 @@ import { detectedProviderVariants, diagnosticPackageJsonFile, formatProviderEnvA
16
19
  import { appendToArrayExpression, appendToNamedArray, appendToOutboxRegistryArray, arrayInitializerInfo, codeCharacterIndexes, identifiersFromArrayExpression, insertAfterImports, matchingDelimiterIndex, outboxRegistryEntryState, outboxRegistryIdentifiers, } from "./registry-edits.js";
17
20
  import { currentGeneratedPackageScripts, externalVersions, isLegacyGeneratedPackageScript, legacyGeneratedPackageScripts, } from "./templates/shared.js";
18
21
  import { testSupportTemplateFiles } from "./templates/testing.js";
22
+ import { isAppTestFile } from "./test-discovery.js";
19
23
  import { getCliVersion } from "./version.js";
20
24
  export async function inspectApp(options = {}) {
21
25
  const targetDir = path.resolve(options.cwd ?? process.cwd());
@@ -485,7 +489,7 @@ function hasRequirement(files, requirement) {
485
489
  }
486
490
  function parseContracts(source, file) {
487
491
  const contracts = [];
488
- const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
492
+ const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, sourceFileScriptKind(file));
489
493
  const assignments = variableAssignments(sourceFile);
490
494
  const assignmentsByName = new Map(assignments.map((assignment) => [assignment.name, assignment.expression]));
491
495
  const factoryBindings = contractFactoryBindings(sourceFile);
@@ -926,41 +930,52 @@ function normalizeContractPath(routePath) {
926
930
  const segments = routePath.split("/").filter(Boolean);
927
931
  return segments.length === 0 ? "/" : `/${segments.join("/")}`;
928
932
  }
929
- async function readRouteExports(targetDir, routeFiles, config) {
933
+ async function readRouteExports(targetDir, routeFiles, files, config) {
930
934
  const routeExports = new Map();
931
935
  for (const file of routeFiles) {
932
936
  const routePath = nextRoutePath(file, config.paths.routes);
933
937
  const source = await readFile(path.join(targetDir, file), "utf8");
934
- routeExports.set(file, parseRouteExports(source, file, routePath, config));
938
+ routeExports.set(file, parseRouteExports(source, file, routePath, files, config));
935
939
  }
936
940
  return routeExports;
937
941
  }
938
- function parseRouteExports(source, handlerFile, routePath, config) {
942
+ function parseRouteExports(source, handlerFile, routePath, files, config) {
939
943
  const exports = [];
940
- const imports = parseNamedImports(source, config);
941
- const exportRegex = /export const\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s*=\s*([^;\n]+)/g;
942
- const apiRouteExportRegex = /export const\s*\{([^}]+)\}\s*=\s*createApiRoute\s*\(/g;
943
- for (const match of source.matchAll(apiRouteExportRegex)) {
944
- for (const member of match[1].split(",")) {
945
- const parts = member.split(":");
946
- const method = (parts[1] ?? parts[0])?.trim();
947
- if (!method || !isHttpMethod(method))
944
+ const sourceFile = ts.createSourceFile(handlerFile, source, ts.ScriptTarget.Latest, true, sourceFileScriptKind(handlerFile));
945
+ const imports = parseRouteNamedImports(sourceFile, config, handlerFile, files);
946
+ for (const statement of sourceFile.statements) {
947
+ if (!ts.isVariableStatement(statement) ||
948
+ !statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)) {
949
+ continue;
950
+ }
951
+ for (const declaration of statement.declarationList.declarations) {
952
+ if (!declaration.initializer ||
953
+ !ts.isObjectBindingPattern(declaration.name) ||
954
+ !isNamedRouteFactoryCall(declaration.initializer, "createApiRoute")) {
948
955
  continue;
949
- exports.push({
950
- method,
951
- handlerFile,
952
- contractRef: routePath,
953
- catchAllPrefix: catchAllRoutePrefix(routePath),
954
- source: "next-route",
955
- });
956
+ }
957
+ for (const element of declaration.name.elements) {
958
+ if (!ts.isIdentifier(element.name))
959
+ continue;
960
+ const method = element.name.text;
961
+ if (!isHttpMethod(method))
962
+ continue;
963
+ exports.push({
964
+ method,
965
+ handlerFile,
966
+ contractRef: routePath,
967
+ catchAllPrefix: catchAllRoutePrefix(routePath),
968
+ source: "next-route",
969
+ });
970
+ }
956
971
  }
957
972
  }
958
- for (const match of source.matchAll(exportRegex)) {
959
- const method = match[1];
960
- const expression = match[2];
961
- const contractMatch = /server\.route\(\s*([A-Za-z_$][\w$]*)\s*\)\.handle/.exec(expression);
962
- if (contractMatch) {
963
- const localName = contractMatch[1];
973
+ for (const { exportName, declaration } of exportedVariableDeclarations(sourceFile)) {
974
+ if (!isHttpMethod(exportName) || !declaration.initializer)
975
+ continue;
976
+ const method = exportName;
977
+ const localName = routeLocalContractIdentifier(declaration.initializer);
978
+ if (localName) {
964
979
  const imported = imports.get(localName);
965
980
  exports.push({
966
981
  method,
@@ -971,7 +986,7 @@ function parseRouteExports(source, handlerFile, routePath, config) {
971
986
  });
972
987
  continue;
973
988
  }
974
- if (/server\.api\b/.test(expression)) {
989
+ if (isServerApiExpression(declaration.initializer)) {
975
990
  exports.push({
976
991
  method,
977
992
  handlerFile,
@@ -983,6 +998,69 @@ function parseRouteExports(source, handlerFile, routePath, config) {
983
998
  }
984
999
  return exports;
985
1000
  }
1001
+ function parseRouteNamedImports(sourceFile, config, handlerFile, files) {
1002
+ const imports = new Map();
1003
+ for (const statement of sourceFile.statements) {
1004
+ if (!ts.isImportDeclaration(statement) ||
1005
+ !ts.isStringLiteral(statement.moduleSpecifier) ||
1006
+ statement.importClause?.isTypeOnly ||
1007
+ !statement.importClause?.namedBindings ||
1008
+ !ts.isNamedImports(statement.importClause.namedBindings)) {
1009
+ continue;
1010
+ }
1011
+ const contractFile = contractFileFromImport(statement.moduleSpecifier.text, config, handlerFile, files);
1012
+ for (const element of statement.importClause.namedBindings.elements) {
1013
+ if (element.isTypeOnly)
1014
+ continue;
1015
+ imports.set(element.name.text, {
1016
+ importedName: element.propertyName?.text ?? element.name.text,
1017
+ contractFile,
1018
+ });
1019
+ }
1020
+ }
1021
+ return imports;
1022
+ }
1023
+ function isNamedRouteFactoryCall(expression, name) {
1024
+ const unwrapped = unwrapContractExpression(expression);
1025
+ if (!ts.isCallExpression(unwrapped))
1026
+ return false;
1027
+ const callee = unwrapContractExpression(unwrapped.expression);
1028
+ return ts.isIdentifier(callee) && callee.text === name;
1029
+ }
1030
+ function routeLocalContractIdentifier(expression) {
1031
+ const handleCall = unwrapContractExpression(expression);
1032
+ if (!ts.isCallExpression(handleCall))
1033
+ return undefined;
1034
+ const handleAccess = unwrapContractExpression(handleCall.expression);
1035
+ if (!ts.isPropertyAccessExpression(handleAccess) ||
1036
+ handleAccess.name.text !== "handle") {
1037
+ return undefined;
1038
+ }
1039
+ const routeCall = unwrapContractExpression(handleAccess.expression);
1040
+ if (!ts.isCallExpression(routeCall))
1041
+ return undefined;
1042
+ const routeAccess = unwrapContractExpression(routeCall.expression);
1043
+ if (!ts.isPropertyAccessExpression(routeAccess) ||
1044
+ routeAccess.name.text !== "route" ||
1045
+ !ts.isIdentifier(routeAccess.expression) ||
1046
+ routeAccess.expression.text !== "server") {
1047
+ return undefined;
1048
+ }
1049
+ const contract = routeCall.arguments[0];
1050
+ const unwrappedContract = contract
1051
+ ? unwrapContractExpression(contract)
1052
+ : undefined;
1053
+ return unwrappedContract && ts.isIdentifier(unwrappedContract)
1054
+ ? unwrappedContract.text
1055
+ : undefined;
1056
+ }
1057
+ function isServerApiExpression(expression) {
1058
+ const unwrapped = unwrapContractExpression(expression);
1059
+ return (ts.isPropertyAccessExpression(unwrapped) &&
1060
+ ts.isIdentifier(unwrapped.expression) &&
1061
+ unwrapped.expression.text === "server" &&
1062
+ unwrapped.name.text === "api");
1063
+ }
986
1064
  function catchAllRoutePrefix(routePath) {
987
1065
  const segments = routePath.split("/").filter(Boolean);
988
1066
  const catchAllIndex = segments.findIndex((segment) => segment.endsWith("*"));
@@ -991,12 +1069,12 @@ function catchAllRoutePrefix(routePath) {
991
1069
  const prefixSegments = segments.slice(0, catchAllIndex);
992
1070
  return prefixSegments.length === 0 ? "/" : `/${prefixSegments.join("/")}`;
993
1071
  }
994
- function parseNamedImports(source, config, importerFile) {
1072
+ function parseNamedImports(source, config, importerFile, files) {
995
1073
  const imports = new Map();
996
1074
  const importRegex = /import\s+\{([^}]+)\}\s+from\s+["']([^"']+)["']/g;
997
1075
  for (const match of source.matchAll(importRegex)) {
998
1076
  const sourcePath = match[2];
999
- const contractFile = contractFileFromImport(sourcePath, config, importerFile);
1077
+ const contractFile = contractFileFromImport(sourcePath, config, importerFile, files);
1000
1078
  for (const member of match[1].split(",")) {
1001
1079
  const parsed = parseImportMember(member);
1002
1080
  if (!parsed)
@@ -1046,9 +1124,7 @@ function parseImportMember(member) {
1046
1124
  }
1047
1125
  function sourceFileFromImport(sourcePath, importerFile = "index.ts", files) {
1048
1126
  const resolveCandidate = (candidate) => {
1049
- if (path.extname(candidate))
1050
- return candidate;
1051
- const candidates = [`${candidate}.ts`, `${candidate}/index.ts`];
1127
+ const candidates = sourceFileSpecifierCandidates(candidate, importerFile);
1052
1128
  return files
1053
1129
  ? candidates.find((file) => files.includes(file))
1054
1130
  : candidates[0];
@@ -1061,24 +1137,76 @@ function sourceFileFromImport(sourcePath, importerFile = "index.ts", files) {
1061
1137
  }
1062
1138
  return undefined;
1063
1139
  }
1064
- function contractFileFromImport(sourcePath, config, importerFile) {
1140
+ function sourceFileCandidates(base, importerFile) {
1141
+ const extensions = importerFile && isJavaScriptSourceFile(importerFile)
1142
+ ? [
1143
+ ...javascriptAnalysisSourceFileExtensions,
1144
+ ...typescriptAnalysisSourceFileExtensions,
1145
+ ]
1146
+ : analysisSourceFileExtensions;
1147
+ return [
1148
+ ...extensions.map((extension) => `${base}${extension}`),
1149
+ ...extensions.map((extension) => `${base}/index${extension}`),
1150
+ ];
1151
+ }
1152
+ function sourceFileSpecifierCandidates(candidate, importerFile) {
1153
+ const extension = path.posix.extname(candidate).toLowerCase();
1154
+ if (!extension)
1155
+ return sourceFileCandidates(candidate, importerFile);
1156
+ if (!isAnalysisSourceFileExtension(extension))
1157
+ return [candidate];
1158
+ const sourceExtensions = extension === ".js"
1159
+ ? [".ts", ".tsx"]
1160
+ : extension === ".jsx"
1161
+ ? [".tsx"]
1162
+ : extension === ".mjs"
1163
+ ? [".mts"]
1164
+ : extension === ".cjs"
1165
+ ? [".cts"]
1166
+ : [];
1167
+ const sourceCandidates = sourceExtensions.map((sourceExtension) => `${candidate.slice(0, -extension.length)}${sourceExtension}`);
1168
+ const directCandidates = importerFile && isJavaScriptSourceFile(importerFile)
1169
+ ? [candidate, ...sourceCandidates]
1170
+ : [...sourceCandidates, candidate];
1171
+ return [
1172
+ ...new Set([
1173
+ ...directCandidates,
1174
+ ...directCandidates.map(directoryIndexSourceFile),
1175
+ ]),
1176
+ ];
1177
+ }
1178
+ function isSourceFileNamed(file, name) {
1179
+ return (isAnalysisSourceFile(file) &&
1180
+ path.posix.basename(stripSourceFileExtension(file)) === name);
1181
+ }
1182
+ function directoryIndexSourceFile(file) {
1183
+ const extension = path.posix.extname(file);
1184
+ return `${stripSourceFileExtension(file)}/index${extension}`;
1185
+ }
1186
+ function contractFileFromImport(sourcePath, config, importerFile, files) {
1187
+ const resolveCandidate = (candidate) => {
1188
+ const candidates = sourceFileSpecifierCandidates(candidate, importerFile);
1189
+ if (!files)
1190
+ return candidates[0];
1191
+ return candidates.find((file) => files.includes(file)) ?? candidates[0];
1192
+ };
1065
1193
  const contractsPath = directoryPath(config.paths.contracts);
1066
1194
  const aliasPrefix = `@/${contractsPath}/`;
1067
1195
  const aliasExact = `@/${contractsPath}`;
1068
1196
  const relativePrefix = `${contractsPath}/`;
1069
1197
  const relativeExact = contractsPath;
1070
1198
  if (sourcePath === aliasExact || sourcePath === relativeExact) {
1071
- return `${contractsPath}/index.ts`;
1199
+ return resolveCandidate(`${contractsPath}/index`);
1072
1200
  }
1073
1201
  if (sourcePath.startsWith(aliasPrefix)) {
1074
- return `${sourcePath.slice("@/".length)}.ts`;
1202
+ return resolveCandidate(sourcePath.slice("@/".length));
1075
1203
  }
1076
1204
  if (sourcePath.startsWith(relativePrefix)) {
1077
- return `${sourcePath}.ts`;
1205
+ return resolveCandidate(sourcePath);
1078
1206
  }
1079
1207
  if (importerFile && sourcePath.startsWith(".")) {
1080
1208
  const resolved = normalizePath(path.join(path.dirname(importerFile), sourcePath));
1081
- return `${resolved}.ts`;
1209
+ return resolveCandidate(resolved);
1082
1210
  }
1083
1211
  return undefined;
1084
1212
  }
@@ -1137,8 +1265,8 @@ function matchRoutes(contracts, routeExports) {
1137
1265
  async function inspectRouteSurface(targetDir, files, config, contracts) {
1138
1266
  if (config.framework === "next") {
1139
1267
  const routeFiles = files.filter((file) => file.startsWith(`${directoryPath(config.paths.routes)}/`) &&
1140
- file.endsWith("/route.ts"));
1141
- const routeExports = await readRouteExports(targetDir, routeFiles, config);
1268
+ isNextRouteFile(file, config));
1269
+ const routeExports = await readRouteExports(targetDir, routeFiles, files, config);
1142
1270
  return matchRoutes(contracts, routeExports);
1143
1271
  }
1144
1272
  if (!files.includes(config.paths.server)) {
@@ -1178,8 +1306,7 @@ function findContracts(contracts, routeExport) {
1178
1306
  const contract = contracts.find((contract) => contract.exportName === routeExport.contractRef &&
1179
1307
  (!routeExport.contractFile ||
1180
1308
  contract.file === routeExport.contractFile ||
1181
- contract.file ===
1182
- routeExport.contractFile.replace(/\.ts$/, "/index.ts")));
1309
+ contract.file === directoryIndexSourceFile(routeExport.contractFile)));
1183
1310
  return contract ? [contract] : [];
1184
1311
  }
1185
1312
  if (routeExport.catchAllPrefix) {
@@ -1416,7 +1543,10 @@ async function inspectCanonicalConformance(targetDir, files, config, convention,
1416
1543
  }
1417
1544
  const featureRoutesPath = `${featuresPath}/`;
1418
1545
  for (const file of files) {
1419
- if (!file.startsWith(featureRoutesPath) || !file.endsWith("/routes.ts")) {
1546
+ if (!file.startsWith(featureRoutesPath) ||
1547
+ !isAnalysisSourceFile(file) ||
1548
+ isAppTestFile(file) ||
1549
+ !stripSourceFileExtension(file).endsWith("/routes")) {
1420
1550
  continue;
1421
1551
  }
1422
1552
  const source = await readCachedSource(targetDir, file, sourceCache);
@@ -1686,7 +1816,7 @@ async function inspectProductionReadiness(targetDir, files, config, convention)
1686
1816
  const schemaDir = `${infrastructurePath}/db/schema`;
1687
1817
  let hasSecurityHeadersHook = false;
1688
1818
  for (const file of files) {
1689
- if (!file.endsWith(".ts") || file.endsWith(".test.ts"))
1819
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file))
1690
1820
  continue;
1691
1821
  const source = await readCachedSource(targetDir, file, sourceCache);
1692
1822
  const routeServerOrInfra = isRouteServerOrInfraFile(file, config);
@@ -1705,7 +1835,7 @@ async function inspectProductionReadiness(targetDir, files, config, convention)
1705
1835
  });
1706
1836
  }
1707
1837
  if (fileWithinDirectory(file, cronDirectory) &&
1708
- file.endsWith("/route.ts") &&
1838
+ isNextRouteFile(file, config) &&
1709
1839
  !/\bCRON_SECRET\b/.test(source) &&
1710
1840
  !/\bcreateOutboxDrainRoute\s*\(/.test(source) &&
1711
1841
  !/\bcreateScheduleRoute\s*\(/.test(source)) {
@@ -1871,6 +2001,16 @@ async function inspectProductionReadiness(targetDir, files, config, convention)
1871
2001
  message: "@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.",
1872
2002
  });
1873
2003
  }
2004
+ if ((await betterAuthRequiresIssuerSchema(targetDir, packageJson)) &&
2005
+ (await anyFileContains(targetDir, configFiles, "drizzleAdapter", sourceCache)) &&
2006
+ !(await hasBetterAuthIssuerSchema(targetDir, files, sourceCache))) {
2007
+ diagnostics.push({
2008
+ severity: "warning",
2009
+ code: "BEIGNET_BETTER_AUTH_ISSUER_SCHEMA_MISSING",
2010
+ file: schemaDir,
2011
+ message: "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.",
2012
+ });
2013
+ }
1874
2014
  }
1875
2015
  const installedReadinessProviders = [...installedPackages].filter((packageName) => readinessRelevantProviderPackages.has(packageName));
1876
2016
  if (installedReadinessProviders.length > 0 &&
@@ -1898,7 +2038,7 @@ async function uploadHasExplicitSizeLimit(targetDir, file, source, files, source
1898
2038
  const importedName = importedNameForLocalBinding(match[1] ?? "", fileReference);
1899
2039
  if (!importedName)
1900
2040
  continue;
1901
- const importedFile = resolveLocalTypeScriptModule(file, moduleName, files);
2041
+ const importedFile = sourceFileFromImport(moduleName, file, files);
1902
2042
  if (!importedFile)
1903
2043
  continue;
1904
2044
  const importedSource = await readCachedSource(targetDir, importedFile, sourceCache);
@@ -1932,17 +2072,14 @@ function exportedObjectHasProperty(source, exportName, propertyName) {
1932
2072
  const initializer = source.slice(openIndex, closeIndex + 1);
1933
2073
  return new RegExp(`\\b${escapeRegExp(propertyName)}\\s*:`).test(initializer);
1934
2074
  }
1935
- function resolveLocalTypeScriptModule(importer, moduleName, files) {
1936
- const base = normalizePath(path.posix.normalize(path.posix.join(path.posix.dirname(importer), moduleName)));
1937
- const candidates = [base, `${base}.ts`, `${base}.tsx`, `${base}/index.ts`];
1938
- return candidates.find((candidate) => files.includes(candidate));
1939
- }
1940
2075
  async function hasReadinessCheck(targetDir, files, config, sourceCache) {
1941
2076
  const routesDir = directoryPath(config.paths.routes);
1942
- if (files.includes(`${routesDir}/ready/route.ts`))
2077
+ if (files.some((file) => path.posix.dirname(file) === `${routesDir}/ready` &&
2078
+ isNextRouteFile(file, config))) {
1943
2079
  return true;
2080
+ }
1944
2081
  for (const file of files) {
1945
- if (!file.endsWith(".ts") || file.endsWith(".test.ts"))
2082
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file))
1946
2083
  continue;
1947
2084
  if (!isRouteServerOrInfraFile(file, config))
1948
2085
  continue;
@@ -2297,12 +2434,193 @@ async function inspectInngestProductionPath(targetDir, files, config) {
2297
2434
  return diagnostics;
2298
2435
  }
2299
2436
  function isFeatureUploadDefinition(file, config) {
2300
- const featuresPath = directoryPath(config.paths.features);
2301
- return new RegExp(`^${escapeRegExp(featuresPath)}/[^/]+/uploads/[^/]+\\.ts$`).test(file);
2437
+ return isFeatureArtifactFile(file, directoryPath(config.paths.features), "uploads");
2302
2438
  }
2303
2439
  function operationalConfigFiles(files, config) {
2304
2440
  return providerOperationalConfigFiles(files, config);
2305
2441
  }
2442
+ function declaredBetterAuthMayRequireIssuerSchema(packageJson) {
2443
+ const version = packageJson?.dependencies?.["better-auth"] ??
2444
+ packageJson?.devDependencies?.["better-auth"] ??
2445
+ packageJson?.peerDependencies?.["better-auth"];
2446
+ if (!version)
2447
+ return false;
2448
+ const normalized = version.trim();
2449
+ const workspaceRange = normalized.startsWith("workspace:")
2450
+ ? normalized.slice("workspace:".length)
2451
+ : normalized;
2452
+ const npmAlias = /^npm:better-auth@(.+)$/.exec(workspaceRange);
2453
+ const candidate = npmAlias?.[1] ?? workspaceRange;
2454
+ const range = semver.validRange(candidate);
2455
+ // Tags, catalogs, local paths, and other non-semver declarations can resolve
2456
+ // to 1.7. Prefer a migration warning until an installed version is readable.
2457
+ if (!range)
2458
+ return true;
2459
+ return semver.intersects(range, ">=1.7.0");
2460
+ }
2461
+ async function betterAuthRequiresIssuerSchema(targetDir, packageJson) {
2462
+ const installed = await readInstalledBetterAuthPackageJson(targetDir);
2463
+ if (installed?.version) {
2464
+ const version = semver.valid(installed.version.trim());
2465
+ if (version)
2466
+ return semver.gte(version, "1.7.0");
2467
+ }
2468
+ return declaredBetterAuthMayRequireIssuerSchema(packageJson);
2469
+ }
2470
+ async function readInstalledBetterAuthPackageJson(targetDir) {
2471
+ const nodeModulesPackage = await readBetterAuthPackageFromNodeModules(targetDir);
2472
+ if (nodeModulesPackage)
2473
+ return nodeModulesPackage;
2474
+ const requireFromTarget = createRequire(path.join(targetDir, "package.json"));
2475
+ const packageJsonPath = resolveModuleFromTarget(requireFromTarget, "better-auth/package.json");
2476
+ if (packageJsonPath) {
2477
+ return readJsonIfExists(packageJsonPath);
2478
+ }
2479
+ const entryPath = resolveModuleFromTarget(requireFromTarget, "better-auth");
2480
+ if (!entryPath)
2481
+ return undefined;
2482
+ let directory = path.dirname(entryPath);
2483
+ while (true) {
2484
+ const candidate = await readJsonIfExists(path.join(directory, "package.json"));
2485
+ if (candidate?.name === "better-auth")
2486
+ return candidate;
2487
+ const parent = path.dirname(directory);
2488
+ if (parent === directory)
2489
+ return undefined;
2490
+ directory = parent;
2491
+ }
2492
+ }
2493
+ async function readBetterAuthPackageFromNodeModules(targetDir) {
2494
+ let directory = targetDir;
2495
+ while (true) {
2496
+ const candidate = await readJsonIfExists(path.join(directory, "node_modules", "better-auth", "package.json"));
2497
+ if (candidate?.name === "better-auth")
2498
+ return candidate;
2499
+ const parent = path.dirname(directory);
2500
+ if (parent === directory)
2501
+ return undefined;
2502
+ directory = parent;
2503
+ }
2504
+ }
2505
+ function resolveModuleFromTarget(requireFromTarget, request) {
2506
+ try {
2507
+ return requireFromTarget.resolve(request);
2508
+ }
2509
+ catch {
2510
+ return undefined;
2511
+ }
2512
+ }
2513
+ async function hasBetterAuthIssuerSchema(targetDir, files, sourceCache) {
2514
+ const schemaFiles = files.filter((file) => !isTestSourceFile(file) &&
2515
+ /\.(?:ts|tsx|js|jsx|mts|mjs|cts|cjs)$/.test(file));
2516
+ for (const file of schemaFiles) {
2517
+ const source = await readCachedSource(targetDir, file, sourceCache);
2518
+ if (sourceHasBetterAuthIssuerSchema(file, source))
2519
+ return true;
2520
+ }
2521
+ return false;
2522
+ }
2523
+ function sourceHasBetterAuthIssuerSchema(file, source) {
2524
+ const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true);
2525
+ let found = false;
2526
+ const visit = (node) => {
2527
+ if (found)
2528
+ return;
2529
+ if (ts.isCallExpression(node) &&
2530
+ isBetterAuthAccountTableCall(node) &&
2531
+ betterAuthAccountTableHasIssuerIdentity(node)) {
2532
+ found = true;
2533
+ return;
2534
+ }
2535
+ ts.forEachChild(node, visit);
2536
+ };
2537
+ visit(sourceFile);
2538
+ return found;
2539
+ }
2540
+ function isBetterAuthAccountTableCall(call) {
2541
+ const callee = unwrapContractExpression(call.expression);
2542
+ const factoryName = ts.isIdentifier(callee)
2543
+ ? callee.text
2544
+ : ts.isPropertyAccessExpression(callee)
2545
+ ? callee.name.text
2546
+ : undefined;
2547
+ return (factoryName !== undefined &&
2548
+ ["mysqlTable", "pgTable", "sqliteTable"].includes(factoryName) &&
2549
+ staticString(call.arguments[0]) === "account");
2550
+ }
2551
+ function betterAuthAccountTableHasIssuerIdentity(call) {
2552
+ const columns = call.arguments[1];
2553
+ if (!columns || !ts.isObjectLiteralExpression(columns))
2554
+ return false;
2555
+ const issuer = columns.properties.find((property) => ts.isPropertyAssignment(property) &&
2556
+ staticPropertyName(property.name) === "issuer");
2557
+ if (!issuer || !ts.isPropertyAssignment(issuer))
2558
+ return false;
2559
+ if (!isBetterAuthIssuerColumn(issuer.initializer))
2560
+ return false;
2561
+ const constraints = call.arguments[2];
2562
+ if (!constraints ||
2563
+ (!ts.isArrowFunction(constraints) &&
2564
+ !ts.isFunctionExpression(constraints)) ||
2565
+ constraints.parameters.length === 0 ||
2566
+ !ts.isIdentifier(constraints.parameters[0]?.name)) {
2567
+ return false;
2568
+ }
2569
+ const tableName = constraints.parameters[0].name.text;
2570
+ let found = false;
2571
+ const visit = (node) => {
2572
+ if (found)
2573
+ return;
2574
+ if (ts.isCallExpression(node) &&
2575
+ isBetterAuthIssuerAccountUniqueConstraint(node, tableName)) {
2576
+ found = true;
2577
+ return;
2578
+ }
2579
+ ts.forEachChild(node, visit);
2580
+ };
2581
+ visit(constraints.body);
2582
+ return found;
2583
+ }
2584
+ function isBetterAuthIssuerColumn(expression) {
2585
+ let current = unwrapContractExpression(expression);
2586
+ while (ts.isCallExpression(current)) {
2587
+ const callee = unwrapContractExpression(current.expression);
2588
+ if (ts.isIdentifier(callee) &&
2589
+ ["text", "varchar"].includes(callee.text) &&
2590
+ staticString(current.arguments[0]) === "issuer") {
2591
+ return true;
2592
+ }
2593
+ if (!ts.isPropertyAccessExpression(callee))
2594
+ return false;
2595
+ current = unwrapContractExpression(callee.expression);
2596
+ }
2597
+ return false;
2598
+ }
2599
+ function isBetterAuthIssuerAccountUniqueConstraint(call, tableName) {
2600
+ const callee = unwrapContractExpression(call.expression);
2601
+ if (!ts.isPropertyAccessExpression(callee) || callee.name.text !== "on") {
2602
+ return false;
2603
+ }
2604
+ const factoryCall = unwrapContractExpression(callee.expression);
2605
+ if (!ts.isCallExpression(factoryCall))
2606
+ return false;
2607
+ const factory = unwrapContractExpression(factoryCall.expression);
2608
+ if (!ts.isIdentifier(factory) ||
2609
+ !["unique", "uniqueIndex"].includes(factory.text)) {
2610
+ return false;
2611
+ }
2612
+ if (call.arguments.length !== 2)
2613
+ return false;
2614
+ const columnNames = call.arguments.map((argument) => {
2615
+ const column = unwrapContractExpression(argument);
2616
+ return ts.isPropertyAccessExpression(column) &&
2617
+ ts.isIdentifier(column.expression) &&
2618
+ column.expression.text === tableName
2619
+ ? column.name.text
2620
+ : undefined;
2621
+ });
2622
+ return columnNames.includes("issuer") && columnNames.includes("accountId");
2623
+ }
2306
2624
  async function readPackageJson(targetDir, files) {
2307
2625
  if (!files.includes("package.json"))
2308
2626
  return undefined;
@@ -2387,7 +2705,7 @@ async function usesEntitlementChecks(targetDir, files, sourceCache) {
2387
2705
  async function hasBillingEntitlementsProviderWiring(targetDir, files, config, sourceCache) {
2388
2706
  const infraPath = path.dirname(config.paths.portWiring);
2389
2707
  const serverPath = path.dirname(config.paths.server);
2390
- const candidateFiles = files.filter((file) => /\.(?:ts|tsx|mts|cts)$/.test(file) &&
2708
+ const candidateFiles = files.filter((file) => isAnalysisSourceFile(file) &&
2391
2709
  !isTestSourceFile(file) &&
2392
2710
  (file.startsWith(`${infraPath}/`) || file.startsWith(`${serverPath}/`)));
2393
2711
  for (const file of candidateFiles) {
@@ -2399,8 +2717,7 @@ async function hasBillingEntitlementsProviderWiring(targetDir, files, config, so
2399
2717
  return false;
2400
2718
  }
2401
2719
  function isTestSourceFile(file) {
2402
- return (/(?:^|\/)tests\//.test(file) ||
2403
- /\.(?:test|spec)\.(?:ts|tsx|mts|cts)$/.test(file));
2720
+ return /(?:^|\/)tests\//.test(file) || isAppTestFile(file);
2404
2721
  }
2405
2722
  function exportedConstInitializer(source, exportName) {
2406
2723
  const declaration = new RegExp(`\\bexport\\s+const\\s+${escapeRegExp(exportName)}\\s*=`).exec(source);
@@ -2414,7 +2731,7 @@ function exportedConstInitializer(source, exportName) {
2414
2731
  }
2415
2732
  function featureNameFromContractFile(file, config) {
2416
2733
  const featuresPath = directoryPath(config.paths.features);
2417
- const match = new RegExp(`^${escapeRegExp(featuresPath)}/([^/]+)/contracts\\.ts$`).exec(file);
2734
+ const match = new RegExp(`^${escapeRegExp(featuresPath)}/([^/]+)/contracts$`).exec(stripSourceFileExtension(file));
2418
2735
  return match?.[1];
2419
2736
  }
2420
2737
  function featureRuntimeFiles(files, featuresPath, feature) {
@@ -2426,7 +2743,7 @@ function featureRuntimeFiles(files, featuresPath, feature) {
2426
2743
  "use-cases",
2427
2744
  ];
2428
2745
  return files.filter((file) => {
2429
- if (!file.endsWith(".ts") || file.endsWith(".test.ts"))
2746
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file))
2430
2747
  return false;
2431
2748
  return runtimeFolders.some((folder) => file.startsWith(`${featuresPath}/${feature}/${folder}/`));
2432
2749
  });
@@ -2434,10 +2751,11 @@ function featureRuntimeFiles(files, featuresPath, feature) {
2434
2751
  function featureTestFiles(files, featuresPath, feature, options = {}) {
2435
2752
  return files.filter((file) => {
2436
2753
  if (!file.startsWith(`${featuresPath}/${feature}/`) ||
2437
- !file.endsWith(".test.ts")) {
2754
+ !isAppTestFile(file)) {
2438
2755
  return false;
2439
2756
  }
2440
- return !options.policyOnly || file.endsWith("/policy.test.ts");
2757
+ return (!options.policyOnly ||
2758
+ ["policy.test", "policy.spec"].includes(path.posix.basename(stripSourceFileExtension(file))));
2441
2759
  });
2442
2760
  }
2443
2761
  function featureNamesFromContracts(contracts, config) {
@@ -2533,14 +2851,12 @@ async function inspectFeatureArtifactDrift(targetDir, files, config, convention)
2533
2851
  severity: "warning",
2534
2852
  code: "BEIGNET_FEATURE_TEST_PLACEMENT",
2535
2853
  file,
2536
- 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.`,
2854
+ 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.`,
2537
2855
  });
2538
2856
  }
2539
2857
  for (const file of files) {
2540
- if (!/\.(?:ts|tsx|mts|cts)$/.test(file) ||
2541
- /\.d\.(?:ts|mts|cts)$/.test(file)) {
2858
+ if (!isAnalysisSourceFile(file))
2542
2859
  continue;
2543
- }
2544
2860
  const misplaced = directFeatureTestMatch(file, featuresPath)
2545
2861
  ? undefined
2546
2862
  : misplacedFeatureLayer(file, config);
@@ -2554,8 +2870,6 @@ async function inspectFeatureArtifactDrift(targetDir, files, config, convention)
2554
2870
  }
2555
2871
  if (isTestSourceFile(file))
2556
2872
  continue;
2557
- if (!file.endsWith(".ts"))
2558
- continue;
2559
2873
  if (isNotificationDefinitionFile(file, featuresPath) ||
2560
2874
  isInfraOrServerFile(file, config)) {
2561
2875
  continue;
@@ -2573,15 +2887,19 @@ async function inspectFeatureArtifactDrift(targetDir, files, config, convention)
2573
2887
  return diagnostics;
2574
2888
  }
2575
2889
  function directFeatureTestMatch(file, featuresPath) {
2576
- return new RegExp(`^${escapeRegExp(featuresPath)}/([^/]+)/[^/]+\\.(?:test|spec)\\.(?:ts|tsx|mts|cts)$`).exec(file);
2890
+ if (!isAppTestFile(file))
2891
+ return null;
2892
+ return new RegExp(`^${escapeRegExp(featuresPath)}/([^/]+)/[^/]+\\.(?:test|spec)$`).exec(stripSourceFileExtension(file));
2577
2893
  }
2578
2894
  function isFeatureArtifactFile(file, featuresPath, folder) {
2579
- const match = new RegExp(`^${escapeRegExp(featuresPath)}/[^/]+/${folder}/([^/]+)\\.ts$`).exec(file);
2895
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file))
2896
+ return false;
2897
+ const match = new RegExp(`^${escapeRegExp(featuresPath)}/[^/]+/${folder}/([^/]+)$`).exec(stripSourceFileExtension(file));
2580
2898
  return Boolean(match && match[1] !== "index");
2581
2899
  }
2582
2900
  async function appHasUploadRoute(targetDir, files, config) {
2583
2901
  for (const file of files) {
2584
- if (!file.endsWith(".ts") || file.endsWith(".test.ts"))
2902
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file))
2585
2903
  continue;
2586
2904
  if (!isRouteServerOrInfraFile(file, config))
2587
2905
  continue;
@@ -2910,10 +3228,13 @@ async function readFeatureWorkflowRegistries(targetDir, files, config) {
2910
3228
  ];
2911
3229
  for (const kind of kinds) {
2912
3230
  const folder = kind === "events" ? "domain/events" : kind;
2913
- const pattern = new RegExp(`^${escapeRegExp(featuresPath)}/[^/]+/${escapeRegExp(folder)}/index\\.ts$`);
3231
+ const pattern = new RegExp(`^${escapeRegExp(featuresPath)}/[^/]+/${escapeRegExp(folder)}/index$`);
2914
3232
  for (const file of files) {
2915
- if (!pattern.test(file))
3233
+ if (!isAnalysisSourceFile(file) ||
3234
+ isWorkflowTestFile(file) ||
3235
+ !pattern.test(stripSourceFileExtension(file))) {
2916
3236
  continue;
3237
+ }
2917
3238
  const source = await readFile(path.join(targetDir, file), "utf8");
2918
3239
  const memberFiles = exportedMemberFiles(source, file, files);
2919
3240
  const exportRegex = new RegExp(`(?:^|\\n)export const\\s+([A-Za-z_$][\\w$]*${workflowRegistrySuffixes[kind]})\\s*=`, "g");
@@ -2955,9 +3276,7 @@ function exportedMemberFiles(source, indexFile, files) {
2955
3276
  async function readRuntimeManifestIdentifiers(targetDir, files) {
2956
3277
  const manifests = [];
2957
3278
  for (const file of files) {
2958
- if (!file.endsWith(".ts") && !file.endsWith(".tsx"))
2959
- continue;
2960
- if (file.endsWith(".d.ts") || isWorkflowTestFile(file))
3279
+ if (!isAnalysisSourceFile(file) || isWorkflowTestFile(file))
2961
3280
  continue;
2962
3281
  const source = await readFile(path.join(targetDir, file), "utf8");
2963
3282
  if (!source.includes("defineRuntimeManifest"))
@@ -3066,11 +3385,14 @@ function registeredWorkflowIdentifiers(source, kind) {
3066
3385
  }
3067
3386
  async function listenedEventNames(targetDir, files, config) {
3068
3387
  const featuresPath = directoryPath(config.paths.features);
3069
- const listenerPattern = new RegExp(`^${escapeRegExp(featuresPath)}/[^/]+/listeners/[^/]+\\.ts$`);
3388
+ const listenerPattern = new RegExp(`^${escapeRegExp(featuresPath)}/[^/]+/listeners/[^/]+$`);
3070
3389
  const listened = new Set();
3071
3390
  for (const file of files) {
3072
- if (!listenerPattern.test(file) || isWorkflowTestFile(file))
3391
+ if (!isAnalysisSourceFile(file) ||
3392
+ isWorkflowTestFile(file) ||
3393
+ !listenerPattern.test(stripSourceFileExtension(file))) {
3073
3394
  continue;
3395
+ }
3074
3396
  const source = await readFile(path.join(targetDir, file), "utf8");
3075
3397
  const callPattern = /\bdefineListener\s*\(/g;
3076
3398
  for (const match of source.matchAll(callPattern)) {
@@ -3100,13 +3422,11 @@ async function listenerWiringReferences(targetDir, files, config, listenerRegist
3100
3422
  let centralListenerRegistryReferenced = false;
3101
3423
  const listenerRegistryFile = listenersFilePath(config);
3102
3424
  for (const file of files) {
3103
- if (!file.endsWith(".ts") && !file.endsWith(".tsx"))
3104
- continue;
3105
- if (file.endsWith(".d.ts") || isWorkflowTestFile(file))
3425
+ if (!isAnalysisSourceFile(file) || isWorkflowTestFile(file))
3106
3426
  continue;
3107
3427
  const source = await readFile(path.join(targetDir, file), "utf8");
3108
3428
  const namedImports = parseNamedImportSources(source);
3109
- const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
3429
+ const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, sourceFileScriptKind(file));
3110
3430
  const registerListenersBindings = importedRegisterListenersBindings(source, namedImports);
3111
3431
  const calls = registerListenersCalls(sourceFile, registerListenersBindings);
3112
3432
  let foundRelevantCall = false;
@@ -3426,21 +3746,15 @@ function callReferencesCentralListenerRegistry(args) {
3426
3746
  return false;
3427
3747
  }
3428
3748
  function isWorkflowTestFile(file) {
3429
- return (file.endsWith(".test.ts") ||
3430
- file.endsWith(".test.tsx") ||
3431
- file.startsWith("tests/") ||
3432
- file.includes("/tests/"));
3749
+ return (isAppTestFile(file) || file.startsWith("tests/") || file.includes("/tests/"));
3433
3750
  }
3434
3751
  async function inspectServerlessFootguns(targetDir, files, config, convention) {
3435
3752
  const diagnostics = [];
3436
3753
  const routeDirectory = directoryPath(config.paths.routes);
3437
3754
  const cronDirectory = path.posix.join(routeDirectory, "cron");
3438
3755
  for (const file of files) {
3439
- if (!file.endsWith(".ts") ||
3440
- file.endsWith(".test.ts") ||
3441
- file.endsWith(".d.ts")) {
3756
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file))
3442
3757
  continue;
3443
- }
3444
3758
  const source = await readFile(path.join(targetDir, file), "utf8");
3445
3759
  const isInfraOrServerFile = isRuntimeFile(file, config);
3446
3760
  const isProviderFile = isProviderLifecycleFile(file, source);
@@ -3545,16 +3859,18 @@ function fileWithinDirectory(file, directory) {
3545
3859
  return directory === "." || file.startsWith(`${directory}/`);
3546
3860
  }
3547
3861
  function isProviderLifecycleFile(file, source) {
3548
- return (/(^|\/)provider(s)?\.ts$/.test(file) ||
3549
- /(^|\/)[^/]*provider[^/]*\.ts$/.test(file) ||
3862
+ const sourcePath = stripSourceFileExtension(file);
3863
+ return (/(^|\/)provider(s)?$/.test(sourcePath) ||
3864
+ /(^|\/)[^/]*provider[^/]*$/.test(sourcePath) ||
3550
3865
  /(^|\/)providers?\//.test(file) ||
3551
3866
  (source !== undefined && containsCallExpression(source, "createProvider")));
3552
3867
  }
3553
3868
  function isNextRouteFile(file, config) {
3554
3869
  const routesPath = directoryPath(config.paths.routes);
3555
3870
  return (file.startsWith(`${routesPath}/`) &&
3556
- file.endsWith("/route.ts") &&
3557
- !file.endsWith(".test.ts"));
3871
+ isSourceFileNamed(file, "route") &&
3872
+ [".js", ".jsx", ".ts", ".tsx"].includes(path.posix.extname(file)) &&
3873
+ !isAppTestFile(file));
3558
3874
  }
3559
3875
  function importsEagerServer(source, importerFile, files, config) {
3560
3876
  const importRegex = /import\s+\{([^}]+)\}\s+from\s+["']([^"']+)["']/g;
@@ -4785,7 +5101,9 @@ async function inspectFeatureRouteRegistration(targetDir, files, config, convent
4785
5101
  const feature = featureNameFromContractFile(contract.file, config);
4786
5102
  if (!feature)
4787
5103
  continue;
4788
- const routeFile = `${featuresPath}/${feature}/routes.ts`;
5104
+ const routeStem = `${featuresPath}/${feature}/routes`;
5105
+ const routeFile = files.find((file) => isAnalysisSourceFile(file) &&
5106
+ stripSourceFileExtension(file) === routeStem) ?? `${routeStem}${path.posix.extname(contract.file) || ".ts"}`;
4789
5107
  const featureRouteGroups = routeGroups.filter((routeGroup) => routeGroup.file === routeFile);
4790
5108
  if (featureRouteGroups.length === 0) {
4791
5109
  diagnostics.push({
@@ -4851,11 +5169,11 @@ function contractsForRegisteredRouteGroups(contracts, routeGroups, registeredGro
4851
5169
  }
4852
5170
  async function readFeatureRouteGroups(targetDir, files, config) {
4853
5171
  const featuresPath = `${directoryPath(config.paths.features)}/`;
4854
- const routeFiles = files.filter((file) => file.startsWith(featuresPath) && file.endsWith("/routes.ts"));
5172
+ const routeFiles = files.filter((file) => file.startsWith(featuresPath) && isSourceFileNamed(file, "routes"));
4855
5173
  const routeGroups = [];
4856
5174
  for (const file of routeFiles) {
4857
5175
  const source = await readFile(path.join(targetDir, file), "utf8");
4858
- const imports = parseNamedImports(source, config, file);
5176
+ const imports = parseNamedImports(source, config, file, files);
4859
5177
  routeGroups.push(...parseFeatureRouteGroups(source, file, imports));
4860
5178
  }
4861
5179
  return routeGroups;
@@ -4966,8 +5284,7 @@ function findRouteGroupContract(contracts, routeGroupContract) {
4966
5284
  return contracts.find((contract) => contract.exportName === routeGroupContract.exportName &&
4967
5285
  (!routeGroupContract.file ||
4968
5286
  contract.file === routeGroupContract.file ||
4969
- contract.file ===
4970
- routeGroupContract.file.replace(/\.ts$/, "/index.ts")));
5287
+ contract.file === directoryIndexSourceFile(routeGroupContract.file)));
4971
5288
  }
4972
5289
  const managedDatabaseSchemaMarker = "// Managed by beignet db schema sync.";
4973
5290
  const databaseSchemaExports = {
@@ -5899,7 +6216,7 @@ async function readContractLists(targetDir, files, config) {
5899
6216
  const contractFiles = contractListSourceFiles(files, config);
5900
6217
  for (const file of contractFiles) {
5901
6218
  const source = await readFile(path.join(targetDir, file), "utf8");
5902
- const imports = parseNamedImports(source, config, file);
6219
+ const imports = parseNamedImports(source, config, file, files);
5903
6220
  for (const list of parseContractLists(source, file, imports)) {
5904
6221
  lists.set(contractListKey(list.file, list.name), list);
5905
6222
  }
@@ -5909,29 +6226,36 @@ async function readContractLists(targetDir, files, config) {
5909
6226
  function contractSourceFiles(files, config) {
5910
6227
  const contractsPath = directoryPath(config.paths.contracts);
5911
6228
  if (usesFeatureOwnedContracts(config)) {
5912
- return files.filter((file) => new RegExp(`^${escapeRegExp(contractsPath)}/[^/]+/contracts\\.ts$`).test(file) && !file.endsWith(".test.ts"));
6229
+ return files.filter((file) => isAnalysisSourceFile(file) &&
6230
+ !isAppTestFile(file) &&
6231
+ new RegExp(`^${escapeRegExp(contractsPath)}/[^/]+/contracts$`).test(stripSourceFileExtension(file)));
5913
6232
  }
5914
- return files.filter((file) => file.startsWith(`${contractsPath}/`) &&
5915
- file.endsWith(".ts") &&
5916
- !file.endsWith(".test.ts"));
6233
+ return files.filter((file) => {
6234
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file))
6235
+ return false;
6236
+ return stripSourceFileExtension(file).startsWith(`${contractsPath}/`);
6237
+ });
5917
6238
  }
5918
6239
  function contractListSourceFiles(files, config) {
5919
6240
  const contractsPath = directoryPath(config.paths.contracts);
5920
6241
  if (usesFeatureOwnedContracts(config)) {
5921
6242
  return files.filter((file) => {
5922
- if (!file.endsWith(".ts") || file.endsWith(".test.ts"))
6243
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file))
5923
6244
  return false;
5924
- if (file === `${contractsPath}/index.ts`)
6245
+ const sourcePath = stripSourceFileExtension(file);
6246
+ if (sourcePath === `${contractsPath}/index`)
5925
6247
  return true;
5926
- if (file === `${contractsPath}/contracts.ts`)
6248
+ if (sourcePath === `${contractsPath}/contracts`)
5927
6249
  return true;
5928
- return new RegExp(`^${escapeRegExp(contractsPath)}/[^/]+/contracts\\.ts$`).test(file);
6250
+ return new RegExp(`^${escapeRegExp(contractsPath)}/[^/]+/contracts$`).test(sourcePath);
5929
6251
  });
5930
6252
  }
5931
- return files.filter((file) => (file === `${contractsPath}.ts` ||
5932
- file.startsWith(`${contractsPath}/`)) &&
5933
- file.endsWith(".ts") &&
5934
- !file.endsWith(".test.ts"));
6253
+ return files.filter((file) => {
6254
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file))
6255
+ return false;
6256
+ const sourcePath = stripSourceFileExtension(file);
6257
+ return (sourcePath === contractsPath || sourcePath.startsWith(`${contractsPath}/`));
6258
+ });
5935
6259
  }
5936
6260
  function parseContractLists(source, file, imports) {
5937
6261
  const lists = [];
@@ -5970,7 +6294,7 @@ function resolveContractList(lists, file, name, seen = new Set()) {
5970
6294
  if (seen.has(key))
5971
6295
  return new Set();
5972
6296
  const list = lists.get(key) ??
5973
- lists.get(contractListKey(file.replace(/\.ts$/, "/index.ts"), name));
6297
+ lists.get(contractListKey(directoryIndexSourceFile(file), name));
5974
6298
  if (!list)
5975
6299
  return undefined;
5976
6300
  seen.add(key);
@@ -6342,7 +6666,7 @@ function nextRoutePath(file, routeRoot) {
6342
6666
  const normalizedRouteRoot = directoryPath(routeRoot);
6343
6667
  const routeSegments = file
6344
6668
  .slice(normalizedRouteRoot.length + 1)
6345
- .replace(/\/route\.ts$/, "")
6669
+ .replace(/\/route\.(?:js|jsx|ts|tsx)$/, "")
6346
6670
  .split("/")
6347
6671
  .filter(Boolean);
6348
6672
  const apiPath = routeSegments.map(nextSegmentToContractSegment).join("/");