@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.
- package/CHANGELOG.md +32 -0
- package/README.md +64 -22
- package/dist/analysis/layers.d.ts.map +1 -1
- package/dist/analysis/layers.js +4 -2
- package/dist/analysis/layers.js.map +1 -1
- package/dist/analysis/port-wiring.d.ts.map +1 -1
- package/dist/analysis/port-wiring.js +2 -1
- package/dist/analysis/port-wiring.js.map +1 -1
- package/dist/analysis/source-files.d.ts +15 -0
- package/dist/analysis/source-files.d.ts.map +1 -0
- package/dist/analysis/source-files.js +52 -0
- package/dist/analysis/source-files.js.map +1 -0
- package/dist/analysis/workspace.d.ts.map +1 -1
- package/dist/analysis/workspace.js +21 -25
- package/dist/analysis/workspace.js.map +1 -1
- package/dist/app-map-changes.d.ts.map +1 -1
- package/dist/app-map-changes.js +5 -7
- package/dist/app-map-changes.js.map +1 -1
- package/dist/app-map.d.ts.map +1 -1
- package/dist/app-map.js +33 -30
- package/dist/app-map.js.map +1 -1
- package/dist/check.d.ts +12 -0
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +92 -25
- package/dist/check.js.map +1 -1
- package/dist/git-changes.d.ts +17 -1
- package/dist/git-changes.d.ts.map +1 -1
- package/dist/git-changes.js +118 -84
- package/dist/git-changes.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +35 -6
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +436 -112
- package/dist/inspect.js.map +1 -1
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +3 -6
- package/dist/lint.js.map +1 -1
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +0 -2
- package/dist/make.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +23 -5
- package/dist/mcp.js.map +1 -1
- package/dist/operational-process.d.ts +1 -0
- package/dist/operational-process.d.ts.map +1 -1
- package/dist/operational-process.js.map +1 -1
- package/dist/operational-runner.js +1 -0
- package/dist/operational-runner.js.map +1 -1
- package/dist/outbox.d.ts +1 -0
- package/dist/outbox.d.ts.map +1 -1
- package/dist/outbox.js +58 -12
- package/dist/outbox.js.map +1 -1
- package/dist/templates/agents.d.ts.map +1 -1
- package/dist/templates/agents.js +43 -15
- package/dist/templates/agents.js.map +1 -1
- package/dist/templates/base.js +5 -5
- package/dist/templates/db/mysql.d.ts.map +1 -1
- package/dist/templates/db/mysql.js +51 -22
- package/dist/templates/db/mysql.js.map +1 -1
- package/dist/templates/db/postgres.d.ts.map +1 -1
- package/dist/templates/db/postgres.js +60 -20
- package/dist/templates/db/postgres.js.map +1 -1
- package/dist/templates/db/sqlite.d.ts.map +1 -1
- package/dist/templates/db/sqlite.js +51 -23
- package/dist/templates/db/sqlite.js.map +1 -1
- package/dist/templates/shadcn.js +1 -1
- package/dist/templates/shadcn.js.map +1 -1
- package/dist/templates/shared.js +4 -4
- package/dist/templates/shared.js.map +1 -1
- package/dist/templates/testing.d.ts.map +1 -1
- package/dist/templates/testing.js +7 -19
- package/dist/templates/testing.js.map +1 -1
- package/dist/test-discovery.d.ts +9 -0
- package/dist/test-discovery.d.ts.map +1 -0
- package/dist/test-discovery.js +40 -0
- package/dist/test-discovery.js.map +1 -0
- package/package.json +7 -5
- package/skills/app-structure/SKILL.md +37 -10
- package/src/analysis/layers.ts +4 -2
- package/src/analysis/port-wiring.ts +2 -1
- package/src/analysis/source-files.ts +61 -0
- package/src/analysis/workspace.ts +27 -31
- package/src/app-map-changes.ts +10 -8
- package/src/app-map.ts +43 -32
- package/src/check.ts +115 -25
- package/src/git-changes.ts +218 -86
- package/src/index.ts +53 -8
- package/src/inspect.ts +627 -146
- package/src/lint.ts +6 -5
- package/src/make.ts +0 -2
- package/src/mcp.ts +34 -11
- package/src/operational-process.ts +1 -0
- package/src/operational-runner.ts +1 -0
- package/src/outbox.ts +63 -12
- package/src/templates/agents.ts +43 -15
- package/src/templates/base.ts +5 -5
- package/src/templates/db/mysql.ts +51 -22
- package/src/templates/db/postgres.ts +60 -20
- package/src/templates/db/sqlite.ts +51 -23
- package/src/templates/shadcn.ts +1 -1
- package/src/templates/shared.ts +4 -4
- package/src/templates/testing.ts +12 -19
- 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,
|
|
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
|
|
941
|
-
const
|
|
942
|
-
const
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
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
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
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
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
const
|
|
962
|
-
|
|
963
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
|
1199
|
+
return resolveCandidate(`${contractsPath}/index`);
|
|
1072
1200
|
}
|
|
1073
1201
|
if (sourcePath.startsWith(aliasPrefix)) {
|
|
1074
|
-
return
|
|
1202
|
+
return resolveCandidate(sourcePath.slice("@/".length));
|
|
1075
1203
|
}
|
|
1076
1204
|
if (sourcePath.startsWith(relativePrefix)) {
|
|
1077
|
-
return
|
|
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
|
|
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
|
|
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) ||
|
|
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
|
|
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
|
|
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 =
|
|
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.
|
|
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
|
|
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
|
-
|
|
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) =>
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2754
|
+
!isAppTestFile(file)) {
|
|
2438
2755
|
return false;
|
|
2439
2756
|
}
|
|
2440
|
-
return !options.policyOnly ||
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
3231
|
+
const pattern = new RegExp(`^${escapeRegExp(featuresPath)}/[^/]+/${escapeRegExp(folder)}/index$`);
|
|
2914
3232
|
for (const file of files) {
|
|
2915
|
-
if (!
|
|
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
|
|
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/[^/]
|
|
3388
|
+
const listenerPattern = new RegExp(`^${escapeRegExp(featuresPath)}/[^/]+/listeners/[^/]+$`);
|
|
3070
3389
|
const listened = new Set();
|
|
3071
3390
|
for (const file of files) {
|
|
3072
|
-
if (!
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
3549
|
-
|
|
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
|
|
3557
|
-
|
|
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
|
|
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
|
|
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) =>
|
|
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) =>
|
|
5915
|
-
file
|
|
5916
|
-
|
|
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
|
|
6243
|
+
if (!isAnalysisSourceFile(file) || isAppTestFile(file))
|
|
5923
6244
|
return false;
|
|
5924
|
-
|
|
6245
|
+
const sourcePath = stripSourceFileExtension(file);
|
|
6246
|
+
if (sourcePath === `${contractsPath}/index`)
|
|
5925
6247
|
return true;
|
|
5926
|
-
if (
|
|
6248
|
+
if (sourcePath === `${contractsPath}/contracts`)
|
|
5927
6249
|
return true;
|
|
5928
|
-
return new RegExp(`^${escapeRegExp(contractsPath)}/[^/]+/contracts
|
|
6250
|
+
return new RegExp(`^${escapeRegExp(contractsPath)}/[^/]+/contracts$`).test(sourcePath);
|
|
5929
6251
|
});
|
|
5930
6252
|
}
|
|
5931
|
-
return files.filter((file) =>
|
|
5932
|
-
file
|
|
5933
|
-
|
|
5934
|
-
|
|
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
|
|
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("/");
|