@beignet/cli 0.0.51 → 0.0.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +182 -19
  3. package/dist/analysis/layers.d.ts +2 -0
  4. package/dist/analysis/layers.d.ts.map +1 -1
  5. package/dist/analysis/layers.js +9 -2
  6. package/dist/analysis/layers.js.map +1 -1
  7. package/dist/analysis/port-wiring.d.ts +4 -2
  8. package/dist/analysis/port-wiring.d.ts.map +1 -1
  9. package/dist/analysis/port-wiring.js +41 -11
  10. package/dist/analysis/port-wiring.js.map +1 -1
  11. package/dist/analysis/source-files.d.ts +15 -0
  12. package/dist/analysis/source-files.d.ts.map +1 -0
  13. package/dist/analysis/source-files.js +52 -0
  14. package/dist/analysis/source-files.js.map +1 -0
  15. package/dist/analysis/source-index.d.ts +2 -1
  16. package/dist/analysis/source-index.d.ts.map +1 -1
  17. package/dist/analysis/source-index.js +34 -5
  18. package/dist/analysis/source-index.js.map +1 -1
  19. package/dist/analysis/workspace-routes.d.ts +8 -0
  20. package/dist/analysis/workspace-routes.d.ts.map +1 -0
  21. package/dist/analysis/workspace-routes.js +291 -0
  22. package/dist/analysis/workspace-routes.js.map +1 -0
  23. package/dist/analysis/workspace.d.ts +24 -3
  24. package/dist/analysis/workspace.d.ts.map +1 -1
  25. package/dist/analysis/workspace.js +184 -43
  26. package/dist/analysis/workspace.js.map +1 -1
  27. package/dist/app-map-changes.d.ts.map +1 -1
  28. package/dist/app-map-changes.js +24 -14
  29. package/dist/app-map-changes.js.map +1 -1
  30. package/dist/app-map.d.ts.map +1 -1
  31. package/dist/app-map.js +87 -81
  32. package/dist/app-map.js.map +1 -1
  33. package/dist/check.d.ts +12 -0
  34. package/dist/check.d.ts.map +1 -1
  35. package/dist/check.js +92 -25
  36. package/dist/check.js.map +1 -1
  37. package/dist/config.d.ts +7 -0
  38. package/dist/config.d.ts.map +1 -1
  39. package/dist/config.js +19 -0
  40. package/dist/config.js.map +1 -1
  41. package/dist/git-changes.d.ts +17 -1
  42. package/dist/git-changes.d.ts.map +1 -1
  43. package/dist/git-changes.js +118 -84
  44. package/dist/git-changes.js.map +1 -1
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +23 -3
  47. package/dist/index.js.map +1 -1
  48. package/dist/inspect.d.ts.map +1 -1
  49. package/dist/inspect.js +424 -129
  50. package/dist/inspect.js.map +1 -1
  51. package/dist/lint.d.ts.map +1 -1
  52. package/dist/lint.js +108 -43
  53. package/dist/lint.js.map +1 -1
  54. package/dist/make/shared.js +1 -1
  55. package/dist/make.js +18 -1
  56. package/dist/make.js.map +1 -1
  57. package/dist/mcp.js +1 -1
  58. package/dist/mcp.js.map +1 -1
  59. package/dist/provider-add.js +2 -2
  60. package/dist/provider-add.js.map +1 -1
  61. package/dist/provider-audit.d.ts.map +1 -1
  62. package/dist/provider-audit.js +22 -6
  63. package/dist/provider-audit.js.map +1 -1
  64. package/dist/templates/agents.d.ts.map +1 -1
  65. package/dist/templates/agents.js +36 -13
  66. package/dist/templates/agents.js.map +1 -1
  67. package/dist/templates/base.js +5 -5
  68. package/dist/templates/db/mysql.d.ts.map +1 -1
  69. package/dist/templates/db/mysql.js +51 -22
  70. package/dist/templates/db/mysql.js.map +1 -1
  71. package/dist/templates/db/postgres.d.ts.map +1 -1
  72. package/dist/templates/db/postgres.js +60 -20
  73. package/dist/templates/db/postgres.js.map +1 -1
  74. package/dist/templates/db/sqlite.d.ts.map +1 -1
  75. package/dist/templates/db/sqlite.js +51 -23
  76. package/dist/templates/db/sqlite.js.map +1 -1
  77. package/dist/templates/shadcn.js +1 -1
  78. package/dist/templates/shadcn.js.map +1 -1
  79. package/dist/templates/shared.js +6 -6
  80. package/dist/templates/shared.js.map +1 -1
  81. package/dist/templates/testing.d.ts.map +1 -1
  82. package/dist/templates/testing.js +7 -19
  83. package/dist/templates/testing.js.map +1 -1
  84. package/dist/test-discovery.d.ts +9 -0
  85. package/dist/test-discovery.d.ts.map +1 -0
  86. package/dist/test-discovery.js +40 -0
  87. package/dist/test-discovery.js.map +1 -0
  88. package/package.json +8 -5
  89. package/skills/app-structure/SKILL.md +74 -7
  90. package/src/analysis/layers.ts +13 -2
  91. package/src/analysis/port-wiring.ts +68 -11
  92. package/src/analysis/source-files.ts +61 -0
  93. package/src/analysis/source-index.ts +56 -4
  94. package/src/analysis/workspace-routes.ts +385 -0
  95. package/src/analysis/workspace.ts +302 -59
  96. package/src/app-map-changes.ts +41 -13
  97. package/src/app-map.ts +114 -103
  98. package/src/check.ts +115 -25
  99. package/src/config.ts +33 -0
  100. package/src/git-changes.ts +218 -86
  101. package/src/index.ts +33 -5
  102. package/src/inspect.ts +608 -156
  103. package/src/lint.ts +152 -48
  104. package/src/make/shared.ts +1 -1
  105. package/src/make.ts +31 -1
  106. package/src/mcp.ts +1 -1
  107. package/src/provider-add.ts +2 -2
  108. package/src/provider-audit.ts +30 -10
  109. package/src/templates/agents.ts +36 -13
  110. package/src/templates/base.ts +5 -5
  111. package/src/templates/db/mysql.ts +51 -22
  112. package/src/templates/db/postgres.ts +60 -20
  113. package/src/templates/db/sqlite.ts +51 -23
  114. package/src/templates/shadcn.ts +1 -1
  115. package/src/templates/shared.ts +6 -6
  116. package/src/templates/testing.ts +12 -19
  117. package/src/test-discovery.ts +53 -0
package/dist/inspect.js CHANGED
@@ -1,11 +1,16 @@
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";
8
- import { exportedVariableDeclarations } from "./analysis/source-index.js";
10
+ import { analysisSourceFileExtensions, isAnalysisSourceFile, isAnalysisSourceFileExtension, isJavaScriptSourceFile, javascriptAnalysisSourceFileExtensions, sourceFileScriptKind, stripSourceFileExtension, typescriptAnalysisSourceFileExtensions, } from "./analysis/source-files.js";
11
+ import { createSourceIndex, exportedVariableDeclarations, } from "./analysis/source-index.js";
12
+ import { createAnalysisWorkspace, projectPath } from "./analysis/workspace.js";
13
+ import { workspaceRouteContracts } from "./analysis/workspace-routes.js";
9
14
  import { createPainter } from "./ansi.js";
10
15
  import { clientDirPath, clientFormsPath, clientIndexPath, defaultBeignetConfig, directoryPath, loadBeignetConfig, normalizePath, resolveConfig, } from "./config.js";
11
16
  import { renderDatabaseSchema, resolveDatabaseSchemaDialect } from "./db.js";
@@ -16,19 +21,72 @@ import { detectedProviderVariants, diagnosticPackageJsonFile, formatProviderEnvA
16
21
  import { appendToArrayExpression, appendToNamedArray, appendToOutboxRegistryArray, arrayInitializerInfo, codeCharacterIndexes, identifiersFromArrayExpression, insertAfterImports, matchingDelimiterIndex, outboxRegistryEntryState, outboxRegistryIdentifiers, } from "./registry-edits.js";
17
22
  import { currentGeneratedPackageScripts, externalVersions, isLegacyGeneratedPackageScript, legacyGeneratedPackageScripts, } from "./templates/shared.js";
18
23
  import { testSupportTemplateFiles } from "./templates/testing.js";
24
+ import { isAppTestFile } from "./test-discovery.js";
19
25
  import { getCliVersion } from "./version.js";
20
26
  export async function inspectApp(options = {}) {
21
27
  const targetDir = path.resolve(options.cwd ?? process.cwd());
22
28
  await assertDirectory(targetDir);
23
- const files = await listFiles(targetDir);
24
- const config = options.config
25
- ? resolveConfig(options.config)
26
- : await loadBeignetConfig(targetDir, files);
27
- const convention = inspectConvention(files, config);
29
+ const workspace = await createAnalysisWorkspace(targetDir, {
30
+ config: options.config,
31
+ });
32
+ const { config } = workspace;
33
+ const files = workspace.projects[0].files;
28
34
  const inspectedContracts = await readContracts(targetDir, files, config);
35
+ for (const project of workspace.projects.slice(1)) {
36
+ const shared = await readContracts(project.targetDir, project.files, project.config);
37
+ inspectedContracts.contracts.push(...shared.contracts.map((contract) => ({
38
+ ...contract,
39
+ file: projectPath(project, contract.file),
40
+ })));
41
+ inspectedContracts.queryTransportDiagnostics.push(...shared.queryTransportDiagnostics.map((diagnostic) => ({
42
+ ...diagnostic,
43
+ ...(diagnostic.file
44
+ ? { file: projectPath(project, diagnostic.file) }
45
+ : {}),
46
+ ...(diagnostic.subject
47
+ ? {
48
+ subject: {
49
+ ...diagnostic.subject,
50
+ file: projectPath(project, diagnostic.subject.file),
51
+ },
52
+ }
53
+ : {}),
54
+ })));
55
+ }
29
56
  const { contracts } = inspectedContracts;
30
- const matchedRoutes = await inspectRouteSurface(targetDir, files, config, contracts);
57
+ contracts.sort(compareContracts);
58
+ const convention = inspectConvention(files, config, contracts.length > 0 && workspace.projects.length > 1);
59
+ const matchedRoutes = await inspectRouteSurface(targetDir, files, config, contracts, workspace.projects.length > 1 ? createSourceIndex(workspace) : undefined);
60
+ let workspaceRoutesUnresolved = false;
61
+ if (workspace.projects.length > 1) {
62
+ const registration = await workspaceRouteContracts(workspace);
63
+ workspaceRoutesUnresolved = registration.unresolved;
64
+ matchedRoutes.routes = matchedRoutes.routes.map((route) => {
65
+ if (route.handlerSource === "route-local")
66
+ return route;
67
+ if (!registration.contracts.has(`${route.file}#${route.exportName}`))
68
+ return {
69
+ ...route,
70
+ handlerFile: undefined,
71
+ handlerSource: "missing",
72
+ };
73
+ return config.framework === "web"
74
+ ? {
75
+ ...route,
76
+ handlerFile: config.paths.server,
77
+ handlerSource: "web-server",
78
+ }
79
+ : route;
80
+ });
81
+ }
31
82
  const diagnostics = await inspectDiagnostics(targetDir, files, config, convention, contracts, matchedRoutes, Boolean(options.strict), inspectedContracts.queryTransportDiagnostics);
83
+ if (workspaceRoutesUnresolved)
84
+ diagnostics.push({
85
+ severity: "warning",
86
+ code: "BEIGNET_WORKSPACE_ROUTES_UNRESOLVED",
87
+ file: config.paths.server,
88
+ message: "Could not fully resolve workspace route composition. Use explicit defineRoutes arrays and defineRouteGroup bindings reachable from the server routes option.",
89
+ });
32
90
  return {
33
91
  schemaVersion: 1,
34
92
  targetDir,
@@ -83,7 +141,10 @@ async function buildDoctorFixPlan(options = {}) {
83
141
  : await loadBeignetConfig(targetDir, files);
84
142
  const convention = inspectConvention(files, config);
85
143
  const { contracts } = await readContracts(targetDir, files, config);
86
- const matchedRoutes = await inspectRouteSurface(targetDir, files, config, contracts);
144
+ const sourceIndex = config.workspace.packages.length
145
+ ? createSourceIndex(await createAnalysisWorkspace(targetDir, { config }))
146
+ : undefined;
147
+ const matchedRoutes = await inspectRouteSurface(targetDir, files, config, contracts, sourceIndex);
87
148
  const operations = [];
88
149
  const packageFix = await planPackageScripts(targetDir, files, convention);
89
150
  if (packageFix)
@@ -437,14 +498,14 @@ async function readContracts(targetDir, files, config) {
437
498
  queryTransportDiagnostics,
438
499
  };
439
500
  }
440
- function inspectConvention(files, config) {
501
+ function inspectConvention(files, config, sharedContracts = false) {
441
502
  const missingNextLayout = missingRequirements(files, [
442
- `${directoryPath(config.paths.contracts)}/`,
503
+ ...(sharedContracts ? [] : [`${directoryPath(config.paths.contracts)}/`]),
443
504
  `${directoryPath(config.paths.routes)}/`,
444
505
  config.paths.server,
445
506
  ]);
446
507
  const missingWebLayout = missingRequirements(files, [
447
- `${directoryPath(config.paths.contracts)}/`,
508
+ ...(sharedContracts ? [] : [`${directoryPath(config.paths.contracts)}/`]),
448
509
  config.paths.server,
449
510
  ]);
450
511
  const missingResourceGenerator = missingRequirements(files, [
@@ -485,7 +546,7 @@ function hasRequirement(files, requirement) {
485
546
  }
486
547
  function parseContracts(source, file) {
487
548
  const contracts = [];
488
- const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
549
+ const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, sourceFileScriptKind(file));
489
550
  const assignments = variableAssignments(sourceFile);
490
551
  const assignmentsByName = new Map(assignments.map((assignment) => [assignment.name, assignment.expression]));
491
552
  const factoryBindings = contractFactoryBindings(sourceFile);
@@ -926,19 +987,19 @@ function normalizeContractPath(routePath) {
926
987
  const segments = routePath.split("/").filter(Boolean);
927
988
  return segments.length === 0 ? "/" : `/${segments.join("/")}`;
928
989
  }
929
- async function readRouteExports(targetDir, routeFiles, config) {
990
+ async function readRouteExports(targetDir, routeFiles, files, config, sourceIndex) {
930
991
  const routeExports = new Map();
931
992
  for (const file of routeFiles) {
932
993
  const routePath = nextRoutePath(file, config.paths.routes);
933
994
  const source = await readFile(path.join(targetDir, file), "utf8");
934
- routeExports.set(file, parseRouteExports(source, file, routePath, config));
995
+ routeExports.set(file, await parseRouteExports(source, file, routePath, files, config, sourceIndex));
935
996
  }
936
997
  return routeExports;
937
998
  }
938
- function parseRouteExports(source, handlerFile, routePath, config) {
999
+ async function parseRouteExports(source, handlerFile, routePath, files, config, sourceIndex) {
939
1000
  const exports = [];
940
- const sourceFile = ts.createSourceFile(handlerFile, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
941
- const imports = parseRouteNamedImports(sourceFile, config, handlerFile);
1001
+ const sourceFile = ts.createSourceFile(handlerFile, source, ts.ScriptTarget.Latest, true, sourceFileScriptKind(handlerFile));
1002
+ const imports = parseRouteNamedImports(sourceFile, config, handlerFile, files);
942
1003
  for (const statement of sourceFile.statements) {
943
1004
  if (!ts.isVariableStatement(statement) ||
944
1005
  !statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)) {
@@ -973,11 +1034,13 @@ function parseRouteExports(source, handlerFile, routePath, config) {
973
1034
  const localName = routeLocalContractIdentifier(declaration.initializer);
974
1035
  if (localName) {
975
1036
  const imported = imports.get(localName);
1037
+ const resolved = await sourceIndex?.resolveValue(handlerFile, ts.factory.createIdentifier(localName));
976
1038
  exports.push({
977
1039
  method,
978
1040
  handlerFile,
979
- contractRef: imported?.importedName ?? localName,
980
- contractFile: imported?.contractFile,
1041
+ contractRef: resolved?.exportName ?? imported?.importedName ?? localName,
1042
+ contractFile: resolved?.file ?? imported?.contractFile,
1043
+ ...(sourceIndex && !resolved ? { contractUnresolved: true } : {}),
981
1044
  source: "route-local",
982
1045
  });
983
1046
  continue;
@@ -994,7 +1057,7 @@ function parseRouteExports(source, handlerFile, routePath, config) {
994
1057
  }
995
1058
  return exports;
996
1059
  }
997
- function parseRouteNamedImports(sourceFile, config, handlerFile) {
1060
+ function parseRouteNamedImports(sourceFile, config, handlerFile, files) {
998
1061
  const imports = new Map();
999
1062
  for (const statement of sourceFile.statements) {
1000
1063
  if (!ts.isImportDeclaration(statement) ||
@@ -1004,7 +1067,7 @@ function parseRouteNamedImports(sourceFile, config, handlerFile) {
1004
1067
  !ts.isNamedImports(statement.importClause.namedBindings)) {
1005
1068
  continue;
1006
1069
  }
1007
- const contractFile = contractFileFromImport(statement.moduleSpecifier.text, config, handlerFile);
1070
+ const contractFile = contractFileFromImport(statement.moduleSpecifier.text, config, handlerFile, files);
1008
1071
  for (const element of statement.importClause.namedBindings.elements) {
1009
1072
  if (element.isTypeOnly)
1010
1073
  continue;
@@ -1065,12 +1128,12 @@ function catchAllRoutePrefix(routePath) {
1065
1128
  const prefixSegments = segments.slice(0, catchAllIndex);
1066
1129
  return prefixSegments.length === 0 ? "/" : `/${prefixSegments.join("/")}`;
1067
1130
  }
1068
- function parseNamedImports(source, config, importerFile) {
1131
+ function parseNamedImports(source, config, importerFile, files) {
1069
1132
  const imports = new Map();
1070
1133
  const importRegex = /import\s+\{([^}]+)\}\s+from\s+["']([^"']+)["']/g;
1071
1134
  for (const match of source.matchAll(importRegex)) {
1072
1135
  const sourcePath = match[2];
1073
- const contractFile = contractFileFromImport(sourcePath, config, importerFile);
1136
+ const contractFile = contractFileFromImport(sourcePath, config, importerFile, files);
1074
1137
  for (const member of match[1].split(",")) {
1075
1138
  const parsed = parseImportMember(member);
1076
1139
  if (!parsed)
@@ -1120,9 +1183,7 @@ function parseImportMember(member) {
1120
1183
  }
1121
1184
  function sourceFileFromImport(sourcePath, importerFile = "index.ts", files) {
1122
1185
  const resolveCandidate = (candidate) => {
1123
- if (path.extname(candidate))
1124
- return candidate;
1125
- const candidates = [`${candidate}.ts`, `${candidate}/index.ts`];
1186
+ const candidates = sourceFileSpecifierCandidates(candidate, importerFile);
1126
1187
  return files
1127
1188
  ? candidates.find((file) => files.includes(file))
1128
1189
  : candidates[0];
@@ -1135,29 +1196,58 @@ function sourceFileFromImport(sourcePath, importerFile = "index.ts", files) {
1135
1196
  }
1136
1197
  return undefined;
1137
1198
  }
1138
- function contractFileFromImport(sourcePath, config, importerFile) {
1199
+ function sourceFileCandidates(base, importerFile) {
1200
+ const extensions = importerFile && isJavaScriptSourceFile(importerFile)
1201
+ ? [
1202
+ ...javascriptAnalysisSourceFileExtensions,
1203
+ ...typescriptAnalysisSourceFileExtensions,
1204
+ ]
1205
+ : analysisSourceFileExtensions;
1206
+ return [
1207
+ ...extensions.map((extension) => `${base}${extension}`),
1208
+ ...extensions.map((extension) => `${base}/index${extension}`),
1209
+ ];
1210
+ }
1211
+ function sourceFileSpecifierCandidates(candidate, importerFile) {
1212
+ const extension = path.posix.extname(candidate).toLowerCase();
1213
+ if (!extension)
1214
+ return sourceFileCandidates(candidate, importerFile);
1215
+ if (!isAnalysisSourceFileExtension(extension))
1216
+ return [candidate];
1217
+ const sourceExtensions = extension === ".js"
1218
+ ? [".ts", ".tsx"]
1219
+ : extension === ".jsx"
1220
+ ? [".tsx"]
1221
+ : extension === ".mjs"
1222
+ ? [".mts"]
1223
+ : extension === ".cjs"
1224
+ ? [".cts"]
1225
+ : [];
1226
+ const sourceCandidates = sourceExtensions.map((sourceExtension) => `${candidate.slice(0, -extension.length)}${sourceExtension}`);
1227
+ const directCandidates = importerFile && isJavaScriptSourceFile(importerFile)
1228
+ ? [candidate, ...sourceCandidates]
1229
+ : [...sourceCandidates, candidate];
1230
+ return [
1231
+ ...new Set([
1232
+ ...directCandidates,
1233
+ ...directCandidates.map(directoryIndexSourceFile),
1234
+ ]),
1235
+ ];
1236
+ }
1237
+ function isSourceFileNamed(file, name) {
1238
+ return (isAnalysisSourceFile(file) &&
1239
+ path.posix.basename(stripSourceFileExtension(file)) === name);
1240
+ }
1241
+ function directoryIndexSourceFile(file) {
1242
+ const extension = path.posix.extname(file);
1243
+ return `${stripSourceFileExtension(file)}/index${extension}`;
1244
+ }
1245
+ function contractFileFromImport(sourcePath, config, importerFile, files) {
1139
1246
  const resolveCandidate = (candidate) => {
1140
- const extension = path.extname(candidate).toLowerCase();
1141
- if (!extension)
1142
- return `${candidate}.ts`;
1143
- let sourceExtension;
1144
- switch (extension) {
1145
- case ".js":
1146
- sourceExtension = ".ts";
1147
- break;
1148
- case ".jsx":
1149
- sourceExtension = ".tsx";
1150
- break;
1151
- case ".mjs":
1152
- sourceExtension = ".mts";
1153
- break;
1154
- case ".cjs":
1155
- sourceExtension = ".cts";
1156
- break;
1157
- }
1158
- return sourceExtension
1159
- ? `${candidate.slice(0, -extension.length)}${sourceExtension}`
1160
- : candidate;
1247
+ const candidates = sourceFileSpecifierCandidates(candidate, importerFile);
1248
+ if (!files)
1249
+ return candidates[0];
1250
+ return candidates.find((file) => files.includes(file)) ?? candidates[0];
1161
1251
  };
1162
1252
  const contractsPath = directoryPath(config.paths.contracts);
1163
1253
  const aliasPrefix = `@/${contractsPath}/`;
@@ -1165,7 +1255,7 @@ function contractFileFromImport(sourcePath, config, importerFile) {
1165
1255
  const relativePrefix = `${contractsPath}/`;
1166
1256
  const relativeExact = contractsPath;
1167
1257
  if (sourcePath === aliasExact || sourcePath === relativeExact) {
1168
- return `${contractsPath}/index.ts`;
1258
+ return resolveCandidate(`${contractsPath}/index`);
1169
1259
  }
1170
1260
  if (sourcePath.startsWith(aliasPrefix)) {
1171
1261
  return resolveCandidate(sourcePath.slice("@/".length));
@@ -1231,11 +1321,11 @@ function matchRoutes(contracts, routeExports) {
1231
1321
  unmatchedRouteHandlers,
1232
1322
  };
1233
1323
  }
1234
- async function inspectRouteSurface(targetDir, files, config, contracts) {
1324
+ async function inspectRouteSurface(targetDir, files, config, contracts, sourceIndex) {
1235
1325
  if (config.framework === "next") {
1236
1326
  const routeFiles = files.filter((file) => file.startsWith(`${directoryPath(config.paths.routes)}/`) &&
1237
- file.endsWith("/route.ts"));
1238
- const routeExports = await readRouteExports(targetDir, routeFiles, config);
1327
+ isNextRouteFile(file, config));
1328
+ const routeExports = await readRouteExports(targetDir, routeFiles, files, config, sourceIndex);
1239
1329
  return matchRoutes(contracts, routeExports);
1240
1330
  }
1241
1331
  if (!files.includes(config.paths.server)) {
@@ -1272,11 +1362,12 @@ function findContract(contracts, routeExport) {
1272
1362
  }
1273
1363
  function findContracts(contracts, routeExport) {
1274
1364
  if (routeExport.source === "route-local") {
1365
+ if (routeExport.contractUnresolved)
1366
+ return [];
1275
1367
  const contract = contracts.find((contract) => contract.exportName === routeExport.contractRef &&
1276
1368
  (!routeExport.contractFile ||
1277
1369
  contract.file === routeExport.contractFile ||
1278
- contract.file ===
1279
- routeExport.contractFile.replace(/\.ts$/, "/index.ts")));
1370
+ contract.file === directoryIndexSourceFile(routeExport.contractFile)));
1280
1371
  return contract ? [contract] : [];
1281
1372
  }
1282
1373
  if (routeExport.catchAllPrefix) {
@@ -1372,6 +1463,7 @@ async function inspectCanonicalConformance(targetDir, files, config, convention,
1372
1463
  files.some((file) => file.startsWith(`${clientDir}/`)) ||
1373
1464
  [
1374
1465
  "@beignet/react-query",
1466
+ "@beignet/react-form",
1375
1467
  "@beignet/react-hook-form",
1376
1468
  "@beignet/react-uploads",
1377
1469
  "@beignet/nuqs",
@@ -1408,11 +1500,12 @@ async function inspectCanonicalConformance(targetDir, files, config, convention,
1408
1500
  reason: `${clientIndex} should own the typed client and frontend adapter setup.`,
1409
1501
  });
1410
1502
  }
1411
- if (installedPackages.has("@beignet/react-hook-form") ||
1503
+ if (installedPackages.has("@beignet/react-form") ||
1504
+ installedPackages.has("@beignet/react-hook-form") ||
1412
1505
  files.includes(clientForms)) {
1413
1506
  requiredFiles.push({
1414
1507
  file: clientForms,
1415
- reason: `${clientForms} should own React Hook Form adapter setup when form helpers are used.`,
1508
+ reason: `${clientForms} should own form adapter setup (createReactForm or createReactHookForm) when form helpers are used.`,
1416
1509
  });
1417
1510
  }
1418
1511
  for (const required of requiredFiles) {
@@ -1513,7 +1606,10 @@ async function inspectCanonicalConformance(targetDir, files, config, convention,
1513
1606
  }
1514
1607
  const featureRoutesPath = `${featuresPath}/`;
1515
1608
  for (const file of files) {
1516
- if (!file.startsWith(featureRoutesPath) || !file.endsWith("/routes.ts")) {
1609
+ if (!file.startsWith(featureRoutesPath) ||
1610
+ !isAnalysisSourceFile(file) ||
1611
+ isAppTestFile(file) ||
1612
+ !stripSourceFileExtension(file).endsWith("/routes")) {
1517
1613
  continue;
1518
1614
  }
1519
1615
  const source = await readCachedSource(targetDir, file, sourceCache);
@@ -1783,7 +1879,7 @@ async function inspectProductionReadiness(targetDir, files, config, convention)
1783
1879
  const schemaDir = `${infrastructurePath}/db/schema`;
1784
1880
  let hasSecurityHeadersHook = false;
1785
1881
  for (const file of files) {
1786
- if (!file.endsWith(".ts") || file.endsWith(".test.ts"))
1882
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file))
1787
1883
  continue;
1788
1884
  const source = await readCachedSource(targetDir, file, sourceCache);
1789
1885
  const routeServerOrInfra = isRouteServerOrInfraFile(file, config);
@@ -1802,7 +1898,7 @@ async function inspectProductionReadiness(targetDir, files, config, convention)
1802
1898
  });
1803
1899
  }
1804
1900
  if (fileWithinDirectory(file, cronDirectory) &&
1805
- file.endsWith("/route.ts") &&
1901
+ isNextRouteFile(file, config) &&
1806
1902
  !/\bCRON_SECRET\b/.test(source) &&
1807
1903
  !/\bcreateOutboxDrainRoute\s*\(/.test(source) &&
1808
1904
  !/\bcreateScheduleRoute\s*\(/.test(source)) {
@@ -1968,6 +2064,16 @@ async function inspectProductionReadiness(targetDir, files, config, convention)
1968
2064
  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.",
1969
2065
  });
1970
2066
  }
2067
+ if ((await betterAuthRequiresIssuerSchema(targetDir, packageJson)) &&
2068
+ (await anyFileContains(targetDir, configFiles, "drizzleAdapter", sourceCache)) &&
2069
+ !(await hasBetterAuthIssuerSchema(targetDir, files, sourceCache))) {
2070
+ diagnostics.push({
2071
+ severity: "warning",
2072
+ code: "BEIGNET_BETTER_AUTH_ISSUER_SCHEMA_MISSING",
2073
+ file: schemaDir,
2074
+ 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.",
2075
+ });
2076
+ }
1971
2077
  }
1972
2078
  const installedReadinessProviders = [...installedPackages].filter((packageName) => readinessRelevantProviderPackages.has(packageName));
1973
2079
  if (installedReadinessProviders.length > 0 &&
@@ -1995,7 +2101,7 @@ async function uploadHasExplicitSizeLimit(targetDir, file, source, files, source
1995
2101
  const importedName = importedNameForLocalBinding(match[1] ?? "", fileReference);
1996
2102
  if (!importedName)
1997
2103
  continue;
1998
- const importedFile = resolveLocalTypeScriptModule(file, moduleName, files);
2104
+ const importedFile = sourceFileFromImport(moduleName, file, files);
1999
2105
  if (!importedFile)
2000
2106
  continue;
2001
2107
  const importedSource = await readCachedSource(targetDir, importedFile, sourceCache);
@@ -2029,17 +2135,14 @@ function exportedObjectHasProperty(source, exportName, propertyName) {
2029
2135
  const initializer = source.slice(openIndex, closeIndex + 1);
2030
2136
  return new RegExp(`\\b${escapeRegExp(propertyName)}\\s*:`).test(initializer);
2031
2137
  }
2032
- function resolveLocalTypeScriptModule(importer, moduleName, files) {
2033
- const base = normalizePath(path.posix.normalize(path.posix.join(path.posix.dirname(importer), moduleName)));
2034
- const candidates = [base, `${base}.ts`, `${base}.tsx`, `${base}/index.ts`];
2035
- return candidates.find((candidate) => files.includes(candidate));
2036
- }
2037
2138
  async function hasReadinessCheck(targetDir, files, config, sourceCache) {
2038
2139
  const routesDir = directoryPath(config.paths.routes);
2039
- if (files.includes(`${routesDir}/ready/route.ts`))
2140
+ if (files.some((file) => path.posix.dirname(file) === `${routesDir}/ready` &&
2141
+ isNextRouteFile(file, config))) {
2040
2142
  return true;
2143
+ }
2041
2144
  for (const file of files) {
2042
- if (!file.endsWith(".ts") || file.endsWith(".test.ts"))
2145
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file))
2043
2146
  continue;
2044
2147
  if (!isRouteServerOrInfraFile(file, config))
2045
2148
  continue;
@@ -2394,12 +2497,193 @@ async function inspectInngestProductionPath(targetDir, files, config) {
2394
2497
  return diagnostics;
2395
2498
  }
2396
2499
  function isFeatureUploadDefinition(file, config) {
2397
- const featuresPath = directoryPath(config.paths.features);
2398
- return new RegExp(`^${escapeRegExp(featuresPath)}/[^/]+/uploads/[^/]+\\.ts$`).test(file);
2500
+ return isFeatureArtifactFile(file, directoryPath(config.paths.features), "uploads");
2399
2501
  }
2400
2502
  function operationalConfigFiles(files, config) {
2401
2503
  return providerOperationalConfigFiles(files, config);
2402
2504
  }
2505
+ function declaredBetterAuthMayRequireIssuerSchema(packageJson) {
2506
+ const version = packageJson?.dependencies?.["better-auth"] ??
2507
+ packageJson?.devDependencies?.["better-auth"] ??
2508
+ packageJson?.peerDependencies?.["better-auth"];
2509
+ if (!version)
2510
+ return false;
2511
+ const normalized = version.trim();
2512
+ const workspaceRange = normalized.startsWith("workspace:")
2513
+ ? normalized.slice("workspace:".length)
2514
+ : normalized;
2515
+ const npmAlias = /^npm:better-auth@(.+)$/.exec(workspaceRange);
2516
+ const candidate = npmAlias?.[1] ?? workspaceRange;
2517
+ const range = semver.validRange(candidate);
2518
+ // Tags, catalogs, local paths, and other non-semver declarations can resolve
2519
+ // to 1.7. Prefer a migration warning until an installed version is readable.
2520
+ if (!range)
2521
+ return true;
2522
+ return semver.intersects(range, ">=1.7.0");
2523
+ }
2524
+ async function betterAuthRequiresIssuerSchema(targetDir, packageJson) {
2525
+ const installed = await readInstalledBetterAuthPackageJson(targetDir);
2526
+ if (installed?.version) {
2527
+ const version = semver.valid(installed.version.trim());
2528
+ if (version)
2529
+ return semver.gte(version, "1.7.0");
2530
+ }
2531
+ return declaredBetterAuthMayRequireIssuerSchema(packageJson);
2532
+ }
2533
+ async function readInstalledBetterAuthPackageJson(targetDir) {
2534
+ const nodeModulesPackage = await readBetterAuthPackageFromNodeModules(targetDir);
2535
+ if (nodeModulesPackage)
2536
+ return nodeModulesPackage;
2537
+ const requireFromTarget = createRequire(path.join(targetDir, "package.json"));
2538
+ const packageJsonPath = resolveModuleFromTarget(requireFromTarget, "better-auth/package.json");
2539
+ if (packageJsonPath) {
2540
+ return readJsonIfExists(packageJsonPath);
2541
+ }
2542
+ const entryPath = resolveModuleFromTarget(requireFromTarget, "better-auth");
2543
+ if (!entryPath)
2544
+ return undefined;
2545
+ let directory = path.dirname(entryPath);
2546
+ while (true) {
2547
+ const candidate = await readJsonIfExists(path.join(directory, "package.json"));
2548
+ if (candidate?.name === "better-auth")
2549
+ return candidate;
2550
+ const parent = path.dirname(directory);
2551
+ if (parent === directory)
2552
+ return undefined;
2553
+ directory = parent;
2554
+ }
2555
+ }
2556
+ async function readBetterAuthPackageFromNodeModules(targetDir) {
2557
+ let directory = targetDir;
2558
+ while (true) {
2559
+ const candidate = await readJsonIfExists(path.join(directory, "node_modules", "better-auth", "package.json"));
2560
+ if (candidate?.name === "better-auth")
2561
+ return candidate;
2562
+ const parent = path.dirname(directory);
2563
+ if (parent === directory)
2564
+ return undefined;
2565
+ directory = parent;
2566
+ }
2567
+ }
2568
+ function resolveModuleFromTarget(requireFromTarget, request) {
2569
+ try {
2570
+ return requireFromTarget.resolve(request);
2571
+ }
2572
+ catch {
2573
+ return undefined;
2574
+ }
2575
+ }
2576
+ async function hasBetterAuthIssuerSchema(targetDir, files, sourceCache) {
2577
+ const schemaFiles = files.filter((file) => !isTestSourceFile(file) &&
2578
+ /\.(?:ts|tsx|js|jsx|mts|mjs|cts|cjs)$/.test(file));
2579
+ for (const file of schemaFiles) {
2580
+ const source = await readCachedSource(targetDir, file, sourceCache);
2581
+ if (sourceHasBetterAuthIssuerSchema(file, source))
2582
+ return true;
2583
+ }
2584
+ return false;
2585
+ }
2586
+ function sourceHasBetterAuthIssuerSchema(file, source) {
2587
+ const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true);
2588
+ let found = false;
2589
+ const visit = (node) => {
2590
+ if (found)
2591
+ return;
2592
+ if (ts.isCallExpression(node) &&
2593
+ isBetterAuthAccountTableCall(node) &&
2594
+ betterAuthAccountTableHasIssuerIdentity(node)) {
2595
+ found = true;
2596
+ return;
2597
+ }
2598
+ ts.forEachChild(node, visit);
2599
+ };
2600
+ visit(sourceFile);
2601
+ return found;
2602
+ }
2603
+ function isBetterAuthAccountTableCall(call) {
2604
+ const callee = unwrapContractExpression(call.expression);
2605
+ const factoryName = ts.isIdentifier(callee)
2606
+ ? callee.text
2607
+ : ts.isPropertyAccessExpression(callee)
2608
+ ? callee.name.text
2609
+ : undefined;
2610
+ return (factoryName !== undefined &&
2611
+ ["mysqlTable", "pgTable", "sqliteTable"].includes(factoryName) &&
2612
+ staticString(call.arguments[0]) === "account");
2613
+ }
2614
+ function betterAuthAccountTableHasIssuerIdentity(call) {
2615
+ const columns = call.arguments[1];
2616
+ if (!columns || !ts.isObjectLiteralExpression(columns))
2617
+ return false;
2618
+ const issuer = columns.properties.find((property) => ts.isPropertyAssignment(property) &&
2619
+ staticPropertyName(property.name) === "issuer");
2620
+ if (!issuer || !ts.isPropertyAssignment(issuer))
2621
+ return false;
2622
+ if (!isBetterAuthIssuerColumn(issuer.initializer))
2623
+ return false;
2624
+ const constraints = call.arguments[2];
2625
+ if (!constraints ||
2626
+ (!ts.isArrowFunction(constraints) &&
2627
+ !ts.isFunctionExpression(constraints)) ||
2628
+ constraints.parameters.length === 0 ||
2629
+ !ts.isIdentifier(constraints.parameters[0]?.name)) {
2630
+ return false;
2631
+ }
2632
+ const tableName = constraints.parameters[0].name.text;
2633
+ let found = false;
2634
+ const visit = (node) => {
2635
+ if (found)
2636
+ return;
2637
+ if (ts.isCallExpression(node) &&
2638
+ isBetterAuthIssuerAccountUniqueConstraint(node, tableName)) {
2639
+ found = true;
2640
+ return;
2641
+ }
2642
+ ts.forEachChild(node, visit);
2643
+ };
2644
+ visit(constraints.body);
2645
+ return found;
2646
+ }
2647
+ function isBetterAuthIssuerColumn(expression) {
2648
+ let current = unwrapContractExpression(expression);
2649
+ while (ts.isCallExpression(current)) {
2650
+ const callee = unwrapContractExpression(current.expression);
2651
+ if (ts.isIdentifier(callee) &&
2652
+ ["text", "varchar"].includes(callee.text) &&
2653
+ staticString(current.arguments[0]) === "issuer") {
2654
+ return true;
2655
+ }
2656
+ if (!ts.isPropertyAccessExpression(callee))
2657
+ return false;
2658
+ current = unwrapContractExpression(callee.expression);
2659
+ }
2660
+ return false;
2661
+ }
2662
+ function isBetterAuthIssuerAccountUniqueConstraint(call, tableName) {
2663
+ const callee = unwrapContractExpression(call.expression);
2664
+ if (!ts.isPropertyAccessExpression(callee) || callee.name.text !== "on") {
2665
+ return false;
2666
+ }
2667
+ const factoryCall = unwrapContractExpression(callee.expression);
2668
+ if (!ts.isCallExpression(factoryCall))
2669
+ return false;
2670
+ const factory = unwrapContractExpression(factoryCall.expression);
2671
+ if (!ts.isIdentifier(factory) ||
2672
+ !["unique", "uniqueIndex"].includes(factory.text)) {
2673
+ return false;
2674
+ }
2675
+ if (call.arguments.length !== 2)
2676
+ return false;
2677
+ const columnNames = call.arguments.map((argument) => {
2678
+ const column = unwrapContractExpression(argument);
2679
+ return ts.isPropertyAccessExpression(column) &&
2680
+ ts.isIdentifier(column.expression) &&
2681
+ column.expression.text === tableName
2682
+ ? column.name.text
2683
+ : undefined;
2684
+ });
2685
+ return columnNames.includes("issuer") && columnNames.includes("accountId");
2686
+ }
2403
2687
  async function readPackageJson(targetDir, files) {
2404
2688
  if (!files.includes("package.json"))
2405
2689
  return undefined;
@@ -2484,7 +2768,7 @@ async function usesEntitlementChecks(targetDir, files, sourceCache) {
2484
2768
  async function hasBillingEntitlementsProviderWiring(targetDir, files, config, sourceCache) {
2485
2769
  const infraPath = path.dirname(config.paths.portWiring);
2486
2770
  const serverPath = path.dirname(config.paths.server);
2487
- const candidateFiles = files.filter((file) => /\.(?:ts|tsx|mts|cts)$/.test(file) &&
2771
+ const candidateFiles = files.filter((file) => isAnalysisSourceFile(file) &&
2488
2772
  !isTestSourceFile(file) &&
2489
2773
  (file.startsWith(`${infraPath}/`) || file.startsWith(`${serverPath}/`)));
2490
2774
  for (const file of candidateFiles) {
@@ -2496,8 +2780,7 @@ async function hasBillingEntitlementsProviderWiring(targetDir, files, config, so
2496
2780
  return false;
2497
2781
  }
2498
2782
  function isTestSourceFile(file) {
2499
- return (/(?:^|\/)tests\//.test(file) ||
2500
- /\.(?:test|spec)\.(?:ts|tsx|mts|cts)$/.test(file));
2783
+ return /(?:^|\/)tests\//.test(file) || isAppTestFile(file);
2501
2784
  }
2502
2785
  function exportedConstInitializer(source, exportName) {
2503
2786
  const declaration = new RegExp(`\\bexport\\s+const\\s+${escapeRegExp(exportName)}\\s*=`).exec(source);
@@ -2511,7 +2794,7 @@ function exportedConstInitializer(source, exportName) {
2511
2794
  }
2512
2795
  function featureNameFromContractFile(file, config) {
2513
2796
  const featuresPath = directoryPath(config.paths.features);
2514
- const match = new RegExp(`^${escapeRegExp(featuresPath)}/([^/]+)/contracts\\.ts$`).exec(file);
2797
+ const match = new RegExp(`^${escapeRegExp(featuresPath)}/([^/]+)/contracts$`).exec(stripSourceFileExtension(file));
2515
2798
  return match?.[1];
2516
2799
  }
2517
2800
  function featureRuntimeFiles(files, featuresPath, feature) {
@@ -2523,7 +2806,7 @@ function featureRuntimeFiles(files, featuresPath, feature) {
2523
2806
  "use-cases",
2524
2807
  ];
2525
2808
  return files.filter((file) => {
2526
- if (!file.endsWith(".ts") || file.endsWith(".test.ts"))
2809
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file))
2527
2810
  return false;
2528
2811
  return runtimeFolders.some((folder) => file.startsWith(`${featuresPath}/${feature}/${folder}/`));
2529
2812
  });
@@ -2531,10 +2814,11 @@ function featureRuntimeFiles(files, featuresPath, feature) {
2531
2814
  function featureTestFiles(files, featuresPath, feature, options = {}) {
2532
2815
  return files.filter((file) => {
2533
2816
  if (!file.startsWith(`${featuresPath}/${feature}/`) ||
2534
- !file.endsWith(".test.ts")) {
2817
+ !isAppTestFile(file)) {
2535
2818
  return false;
2536
2819
  }
2537
- return !options.policyOnly || file.endsWith("/policy.test.ts");
2820
+ return (!options.policyOnly ||
2821
+ ["policy.test", "policy.spec"].includes(path.posix.basename(stripSourceFileExtension(file))));
2538
2822
  });
2539
2823
  }
2540
2824
  function featureNamesFromContracts(contracts, config) {
@@ -2630,14 +2914,12 @@ async function inspectFeatureArtifactDrift(targetDir, files, config, convention)
2630
2914
  severity: "warning",
2631
2915
  code: "BEIGNET_FEATURE_TEST_PLACEMENT",
2632
2916
  file,
2633
- 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.`,
2917
+ 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.`,
2634
2918
  });
2635
2919
  }
2636
2920
  for (const file of files) {
2637
- if (!/\.(?:ts|tsx|mts|cts)$/.test(file) ||
2638
- /\.d\.(?:ts|mts|cts)$/.test(file)) {
2921
+ if (!isAnalysisSourceFile(file))
2639
2922
  continue;
2640
- }
2641
2923
  const misplaced = directFeatureTestMatch(file, featuresPath)
2642
2924
  ? undefined
2643
2925
  : misplacedFeatureLayer(file, config);
@@ -2651,8 +2933,6 @@ async function inspectFeatureArtifactDrift(targetDir, files, config, convention)
2651
2933
  }
2652
2934
  if (isTestSourceFile(file))
2653
2935
  continue;
2654
- if (!file.endsWith(".ts"))
2655
- continue;
2656
2936
  if (isNotificationDefinitionFile(file, featuresPath) ||
2657
2937
  isInfraOrServerFile(file, config)) {
2658
2938
  continue;
@@ -2670,15 +2950,19 @@ async function inspectFeatureArtifactDrift(targetDir, files, config, convention)
2670
2950
  return diagnostics;
2671
2951
  }
2672
2952
  function directFeatureTestMatch(file, featuresPath) {
2673
- return new RegExp(`^${escapeRegExp(featuresPath)}/([^/]+)/[^/]+\\.(?:test|spec)\\.(?:ts|tsx|mts|cts)$`).exec(file);
2953
+ if (!isAppTestFile(file))
2954
+ return null;
2955
+ return new RegExp(`^${escapeRegExp(featuresPath)}/([^/]+)/[^/]+\\.(?:test|spec)$`).exec(stripSourceFileExtension(file));
2674
2956
  }
2675
2957
  function isFeatureArtifactFile(file, featuresPath, folder) {
2676
- const match = new RegExp(`^${escapeRegExp(featuresPath)}/[^/]+/${folder}/([^/]+)\\.ts$`).exec(file);
2958
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file))
2959
+ return false;
2960
+ const match = new RegExp(`^${escapeRegExp(featuresPath)}/[^/]+/${folder}/([^/]+)$`).exec(stripSourceFileExtension(file));
2677
2961
  return Boolean(match && match[1] !== "index");
2678
2962
  }
2679
2963
  async function appHasUploadRoute(targetDir, files, config) {
2680
2964
  for (const file of files) {
2681
- if (!file.endsWith(".ts") || file.endsWith(".test.ts"))
2965
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file))
2682
2966
  continue;
2683
2967
  if (!isRouteServerOrInfraFile(file, config))
2684
2968
  continue;
@@ -3007,10 +3291,13 @@ async function readFeatureWorkflowRegistries(targetDir, files, config) {
3007
3291
  ];
3008
3292
  for (const kind of kinds) {
3009
3293
  const folder = kind === "events" ? "domain/events" : kind;
3010
- const pattern = new RegExp(`^${escapeRegExp(featuresPath)}/[^/]+/${escapeRegExp(folder)}/index\\.ts$`);
3294
+ const pattern = new RegExp(`^${escapeRegExp(featuresPath)}/[^/]+/${escapeRegExp(folder)}/index$`);
3011
3295
  for (const file of files) {
3012
- if (!pattern.test(file))
3296
+ if (!isAnalysisSourceFile(file) ||
3297
+ isWorkflowTestFile(file) ||
3298
+ !pattern.test(stripSourceFileExtension(file))) {
3013
3299
  continue;
3300
+ }
3014
3301
  const source = await readFile(path.join(targetDir, file), "utf8");
3015
3302
  const memberFiles = exportedMemberFiles(source, file, files);
3016
3303
  const exportRegex = new RegExp(`(?:^|\\n)export const\\s+([A-Za-z_$][\\w$]*${workflowRegistrySuffixes[kind]})\\s*=`, "g");
@@ -3052,9 +3339,7 @@ function exportedMemberFiles(source, indexFile, files) {
3052
3339
  async function readRuntimeManifestIdentifiers(targetDir, files) {
3053
3340
  const manifests = [];
3054
3341
  for (const file of files) {
3055
- if (!file.endsWith(".ts") && !file.endsWith(".tsx"))
3056
- continue;
3057
- if (file.endsWith(".d.ts") || isWorkflowTestFile(file))
3342
+ if (!isAnalysisSourceFile(file) || isWorkflowTestFile(file))
3058
3343
  continue;
3059
3344
  const source = await readFile(path.join(targetDir, file), "utf8");
3060
3345
  if (!source.includes("defineRuntimeManifest"))
@@ -3163,11 +3448,14 @@ function registeredWorkflowIdentifiers(source, kind) {
3163
3448
  }
3164
3449
  async function listenedEventNames(targetDir, files, config) {
3165
3450
  const featuresPath = directoryPath(config.paths.features);
3166
- const listenerPattern = new RegExp(`^${escapeRegExp(featuresPath)}/[^/]+/listeners/[^/]+\\.ts$`);
3451
+ const listenerPattern = new RegExp(`^${escapeRegExp(featuresPath)}/[^/]+/listeners/[^/]+$`);
3167
3452
  const listened = new Set();
3168
3453
  for (const file of files) {
3169
- if (!listenerPattern.test(file) || isWorkflowTestFile(file))
3454
+ if (!isAnalysisSourceFile(file) ||
3455
+ isWorkflowTestFile(file) ||
3456
+ !listenerPattern.test(stripSourceFileExtension(file))) {
3170
3457
  continue;
3458
+ }
3171
3459
  const source = await readFile(path.join(targetDir, file), "utf8");
3172
3460
  const callPattern = /\bdefineListener\s*\(/g;
3173
3461
  for (const match of source.matchAll(callPattern)) {
@@ -3197,13 +3485,11 @@ async function listenerWiringReferences(targetDir, files, config, listenerRegist
3197
3485
  let centralListenerRegistryReferenced = false;
3198
3486
  const listenerRegistryFile = listenersFilePath(config);
3199
3487
  for (const file of files) {
3200
- if (!file.endsWith(".ts") && !file.endsWith(".tsx"))
3201
- continue;
3202
- if (file.endsWith(".d.ts") || isWorkflowTestFile(file))
3488
+ if (!isAnalysisSourceFile(file) || isWorkflowTestFile(file))
3203
3489
  continue;
3204
3490
  const source = await readFile(path.join(targetDir, file), "utf8");
3205
3491
  const namedImports = parseNamedImportSources(source);
3206
- const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
3492
+ const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, sourceFileScriptKind(file));
3207
3493
  const registerListenersBindings = importedRegisterListenersBindings(source, namedImports);
3208
3494
  const calls = registerListenersCalls(sourceFile, registerListenersBindings);
3209
3495
  let foundRelevantCall = false;
@@ -3523,21 +3809,15 @@ function callReferencesCentralListenerRegistry(args) {
3523
3809
  return false;
3524
3810
  }
3525
3811
  function isWorkflowTestFile(file) {
3526
- return (file.endsWith(".test.ts") ||
3527
- file.endsWith(".test.tsx") ||
3528
- file.startsWith("tests/") ||
3529
- file.includes("/tests/"));
3812
+ return (isAppTestFile(file) || file.startsWith("tests/") || file.includes("/tests/"));
3530
3813
  }
3531
3814
  async function inspectServerlessFootguns(targetDir, files, config, convention) {
3532
3815
  const diagnostics = [];
3533
3816
  const routeDirectory = directoryPath(config.paths.routes);
3534
3817
  const cronDirectory = path.posix.join(routeDirectory, "cron");
3535
3818
  for (const file of files) {
3536
- if (!file.endsWith(".ts") ||
3537
- file.endsWith(".test.ts") ||
3538
- file.endsWith(".d.ts")) {
3819
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file))
3539
3820
  continue;
3540
- }
3541
3821
  const source = await readFile(path.join(targetDir, file), "utf8");
3542
3822
  const isInfraOrServerFile = isRuntimeFile(file, config);
3543
3823
  const isProviderFile = isProviderLifecycleFile(file, source);
@@ -3642,16 +3922,18 @@ function fileWithinDirectory(file, directory) {
3642
3922
  return directory === "." || file.startsWith(`${directory}/`);
3643
3923
  }
3644
3924
  function isProviderLifecycleFile(file, source) {
3645
- return (/(^|\/)provider(s)?\.ts$/.test(file) ||
3646
- /(^|\/)[^/]*provider[^/]*\.ts$/.test(file) ||
3925
+ const sourcePath = stripSourceFileExtension(file);
3926
+ return (/(^|\/)provider(s)?$/.test(sourcePath) ||
3927
+ /(^|\/)[^/]*provider[^/]*$/.test(sourcePath) ||
3647
3928
  /(^|\/)providers?\//.test(file) ||
3648
3929
  (source !== undefined && containsCallExpression(source, "createProvider")));
3649
3930
  }
3650
3931
  function isNextRouteFile(file, config) {
3651
3932
  const routesPath = directoryPath(config.paths.routes);
3652
3933
  return (file.startsWith(`${routesPath}/`) &&
3653
- file.endsWith("/route.ts") &&
3654
- !file.endsWith(".test.ts"));
3934
+ isSourceFileNamed(file, "route") &&
3935
+ [".js", ".jsx", ".ts", ".tsx"].includes(path.posix.extname(file)) &&
3936
+ !isAppTestFile(file));
3655
3937
  }
3656
3938
  function importsEagerServer(source, importerFile, files, config) {
3657
3939
  const importRegex = /import\s+\{([^}]+)\}\s+from\s+["']([^"']+)["']/g;
@@ -3737,6 +4019,9 @@ async function inspectPortProviderDrift(targetDir, files, config, convention) {
3737
4019
  const portsSource = files.includes(config.paths.ports)
3738
4020
  ? await readFile(path.join(targetDir, config.paths.ports), "utf8")
3739
4021
  : "";
4022
+ const sharedPorts = config.workspace.packages.length > 0
4023
+ ? await analyzePortWiringFiles({ targetDir, files, config })
4024
+ : undefined;
3740
4025
  const packageJson = files.includes("package.json")
3741
4026
  ? await readPackageJson(targetDir, files)
3742
4027
  : undefined;
@@ -3754,7 +4039,9 @@ async function inspectPortProviderDrift(targetDir, files, config, convention) {
3754
4039
  for (const expected of providerPortRequirements) {
3755
4040
  if (!installedPackages.has(expected.packageName))
3756
4041
  continue;
3757
- if (portsSource.includes(`${expected.portName}:`))
4042
+ if (sharedPorts
4043
+ ? sharedPorts.declared.has(expected.portName)
4044
+ : portsSource.includes(`${expected.portName}:`))
3758
4045
  continue;
3759
4046
  diagnostics.push({
3760
4047
  severity: "warning",
@@ -4882,7 +5169,9 @@ async function inspectFeatureRouteRegistration(targetDir, files, config, convent
4882
5169
  const feature = featureNameFromContractFile(contract.file, config);
4883
5170
  if (!feature)
4884
5171
  continue;
4885
- const routeFile = `${featuresPath}/${feature}/routes.ts`;
5172
+ const routeStem = `${featuresPath}/${feature}/routes`;
5173
+ const routeFile = files.find((file) => isAnalysisSourceFile(file) &&
5174
+ stripSourceFileExtension(file) === routeStem) ?? `${routeStem}${path.posix.extname(contract.file) || ".ts"}`;
4886
5175
  const featureRouteGroups = routeGroups.filter((routeGroup) => routeGroup.file === routeFile);
4887
5176
  if (featureRouteGroups.length === 0) {
4888
5177
  diagnostics.push({
@@ -4948,11 +5237,11 @@ function contractsForRegisteredRouteGroups(contracts, routeGroups, registeredGro
4948
5237
  }
4949
5238
  async function readFeatureRouteGroups(targetDir, files, config) {
4950
5239
  const featuresPath = `${directoryPath(config.paths.features)}/`;
4951
- const routeFiles = files.filter((file) => file.startsWith(featuresPath) && file.endsWith("/routes.ts"));
5240
+ const routeFiles = files.filter((file) => file.startsWith(featuresPath) && isSourceFileNamed(file, "routes"));
4952
5241
  const routeGroups = [];
4953
5242
  for (const file of routeFiles) {
4954
5243
  const source = await readFile(path.join(targetDir, file), "utf8");
4955
- const imports = parseNamedImports(source, config, file);
5244
+ const imports = parseNamedImports(source, config, file, files);
4956
5245
  routeGroups.push(...parseFeatureRouteGroups(source, file, imports));
4957
5246
  }
4958
5247
  return routeGroups;
@@ -5063,8 +5352,7 @@ function findRouteGroupContract(contracts, routeGroupContract) {
5063
5352
  return contracts.find((contract) => contract.exportName === routeGroupContract.exportName &&
5064
5353
  (!routeGroupContract.file ||
5065
5354
  contract.file === routeGroupContract.file ||
5066
- contract.file ===
5067
- routeGroupContract.file.replace(/\.ts$/, "/index.ts")));
5355
+ contract.file === directoryIndexSourceFile(routeGroupContract.file)));
5068
5356
  }
5069
5357
  const managedDatabaseSchemaMarker = "// Managed by beignet db schema sync.";
5070
5358
  const databaseSchemaExports = {
@@ -5996,7 +6284,7 @@ async function readContractLists(targetDir, files, config) {
5996
6284
  const contractFiles = contractListSourceFiles(files, config);
5997
6285
  for (const file of contractFiles) {
5998
6286
  const source = await readFile(path.join(targetDir, file), "utf8");
5999
- const imports = parseNamedImports(source, config, file);
6287
+ const imports = parseNamedImports(source, config, file, files);
6000
6288
  for (const list of parseContractLists(source, file, imports)) {
6001
6289
  lists.set(contractListKey(list.file, list.name), list);
6002
6290
  }
@@ -6006,29 +6294,36 @@ async function readContractLists(targetDir, files, config) {
6006
6294
  function contractSourceFiles(files, config) {
6007
6295
  const contractsPath = directoryPath(config.paths.contracts);
6008
6296
  if (usesFeatureOwnedContracts(config)) {
6009
- return files.filter((file) => new RegExp(`^${escapeRegExp(contractsPath)}/[^/]+/contracts\\.ts$`).test(file) && !file.endsWith(".test.ts"));
6297
+ return files.filter((file) => isAnalysisSourceFile(file) &&
6298
+ !isAppTestFile(file) &&
6299
+ new RegExp(`^${escapeRegExp(contractsPath)}/[^/]+/contracts$`).test(stripSourceFileExtension(file)));
6010
6300
  }
6011
- return files.filter((file) => file.startsWith(`${contractsPath}/`) &&
6012
- file.endsWith(".ts") &&
6013
- !file.endsWith(".test.ts"));
6301
+ return files.filter((file) => {
6302
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file))
6303
+ return false;
6304
+ return stripSourceFileExtension(file).startsWith(`${contractsPath}/`);
6305
+ });
6014
6306
  }
6015
6307
  function contractListSourceFiles(files, config) {
6016
6308
  const contractsPath = directoryPath(config.paths.contracts);
6017
6309
  if (usesFeatureOwnedContracts(config)) {
6018
6310
  return files.filter((file) => {
6019
- if (!file.endsWith(".ts") || file.endsWith(".test.ts"))
6311
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file))
6020
6312
  return false;
6021
- if (file === `${contractsPath}/index.ts`)
6313
+ const sourcePath = stripSourceFileExtension(file);
6314
+ if (sourcePath === `${contractsPath}/index`)
6022
6315
  return true;
6023
- if (file === `${contractsPath}/contracts.ts`)
6316
+ if (sourcePath === `${contractsPath}/contracts`)
6024
6317
  return true;
6025
- return new RegExp(`^${escapeRegExp(contractsPath)}/[^/]+/contracts\\.ts$`).test(file);
6318
+ return new RegExp(`^${escapeRegExp(contractsPath)}/[^/]+/contracts$`).test(sourcePath);
6026
6319
  });
6027
6320
  }
6028
- return files.filter((file) => (file === `${contractsPath}.ts` ||
6029
- file.startsWith(`${contractsPath}/`)) &&
6030
- file.endsWith(".ts") &&
6031
- !file.endsWith(".test.ts"));
6321
+ return files.filter((file) => {
6322
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file))
6323
+ return false;
6324
+ const sourcePath = stripSourceFileExtension(file);
6325
+ return (sourcePath === contractsPath || sourcePath.startsWith(`${contractsPath}/`));
6326
+ });
6032
6327
  }
6033
6328
  function parseContractLists(source, file, imports) {
6034
6329
  const lists = [];
@@ -6067,7 +6362,7 @@ function resolveContractList(lists, file, name, seen = new Set()) {
6067
6362
  if (seen.has(key))
6068
6363
  return new Set();
6069
6364
  const list = lists.get(key) ??
6070
- lists.get(contractListKey(file.replace(/\.ts$/, "/index.ts"), name));
6365
+ lists.get(contractListKey(directoryIndexSourceFile(file), name));
6071
6366
  if (!list)
6072
6367
  return undefined;
6073
6368
  seen.add(key);
@@ -6439,7 +6734,7 @@ function nextRoutePath(file, routeRoot) {
6439
6734
  const normalizedRouteRoot = directoryPath(routeRoot);
6440
6735
  const routeSegments = file
6441
6736
  .slice(normalizedRouteRoot.length + 1)
6442
- .replace(/\/route\.ts$/, "")
6737
+ .replace(/\/route\.(?:js|jsx|ts|tsx)$/, "")
6443
6738
  .split("/")
6444
6739
  .filter(Boolean);
6445
6740
  const apiPath = routeSegments.map(nextSegmentToContractSegment).join("/");