@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.
- package/CHANGELOG.md +46 -0
- package/README.md +182 -19
- package/dist/analysis/layers.d.ts +2 -0
- package/dist/analysis/layers.d.ts.map +1 -1
- package/dist/analysis/layers.js +9 -2
- package/dist/analysis/layers.js.map +1 -1
- package/dist/analysis/port-wiring.d.ts +4 -2
- package/dist/analysis/port-wiring.d.ts.map +1 -1
- package/dist/analysis/port-wiring.js +41 -11
- 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/source-index.d.ts +2 -1
- package/dist/analysis/source-index.d.ts.map +1 -1
- package/dist/analysis/source-index.js +34 -5
- package/dist/analysis/source-index.js.map +1 -1
- package/dist/analysis/workspace-routes.d.ts +8 -0
- package/dist/analysis/workspace-routes.d.ts.map +1 -0
- package/dist/analysis/workspace-routes.js +291 -0
- package/dist/analysis/workspace-routes.js.map +1 -0
- package/dist/analysis/workspace.d.ts +24 -3
- package/dist/analysis/workspace.d.ts.map +1 -1
- package/dist/analysis/workspace.js +184 -43
- package/dist/analysis/workspace.js.map +1 -1
- package/dist/app-map-changes.d.ts.map +1 -1
- package/dist/app-map-changes.js +24 -14
- package/dist/app-map-changes.js.map +1 -1
- package/dist/app-map.d.ts.map +1 -1
- package/dist/app-map.js +87 -81
- 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/config.d.ts +7 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +19 -0
- package/dist/config.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.map +1 -1
- package/dist/index.js +23 -3
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +424 -129
- package/dist/inspect.js.map +1 -1
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +108 -43
- package/dist/lint.js.map +1 -1
- package/dist/make/shared.js +1 -1
- package/dist/make.js +18 -1
- package/dist/make.js.map +1 -1
- package/dist/mcp.js +1 -1
- package/dist/mcp.js.map +1 -1
- package/dist/provider-add.js +2 -2
- package/dist/provider-add.js.map +1 -1
- package/dist/provider-audit.d.ts.map +1 -1
- package/dist/provider-audit.js +22 -6
- package/dist/provider-audit.js.map +1 -1
- package/dist/templates/agents.d.ts.map +1 -1
- package/dist/templates/agents.js +36 -13
- 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 +6 -6
- 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 +8 -5
- package/skills/app-structure/SKILL.md +74 -7
- package/src/analysis/layers.ts +13 -2
- package/src/analysis/port-wiring.ts +68 -11
- package/src/analysis/source-files.ts +61 -0
- package/src/analysis/source-index.ts +56 -4
- package/src/analysis/workspace-routes.ts +385 -0
- package/src/analysis/workspace.ts +302 -59
- package/src/app-map-changes.ts +41 -13
- package/src/app-map.ts +114 -103
- package/src/check.ts +115 -25
- package/src/config.ts +33 -0
- package/src/git-changes.ts +218 -86
- package/src/index.ts +33 -5
- package/src/inspect.ts +608 -156
- package/src/lint.ts +152 -48
- package/src/make/shared.ts +1 -1
- package/src/make.ts +31 -1
- package/src/mcp.ts +1 -1
- package/src/provider-add.ts +2 -2
- package/src/provider-audit.ts +30 -10
- package/src/templates/agents.ts +36 -13
- 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 +6 -6
- package/src/templates/testing.ts +12 -19
- 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 {
|
|
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
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
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
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
1141
|
-
if (!
|
|
1142
|
-
return
|
|
1143
|
-
|
|
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
|
|
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
|
|
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-
|
|
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
|
|
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) ||
|
|
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
|
|
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
|
|
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 =
|
|
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.
|
|
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
|
|
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
|
-
|
|
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) =>
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2817
|
+
!isAppTestFile(file)) {
|
|
2535
2818
|
return false;
|
|
2536
2819
|
}
|
|
2537
|
-
return !options.policyOnly ||
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
3294
|
+
const pattern = new RegExp(`^${escapeRegExp(featuresPath)}/[^/]+/${escapeRegExp(folder)}/index$`);
|
|
3011
3295
|
for (const file of files) {
|
|
3012
|
-
if (!
|
|
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
|
|
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/[^/]
|
|
3451
|
+
const listenerPattern = new RegExp(`^${escapeRegExp(featuresPath)}/[^/]+/listeners/[^/]+$`);
|
|
3167
3452
|
const listened = new Set();
|
|
3168
3453
|
for (const file of files) {
|
|
3169
|
-
if (!
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
3646
|
-
|
|
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
|
|
3654
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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) =>
|
|
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) =>
|
|
6012
|
-
file
|
|
6013
|
-
|
|
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
|
|
6311
|
+
if (!isAnalysisSourceFile(file) || isAppTestFile(file))
|
|
6020
6312
|
return false;
|
|
6021
|
-
|
|
6313
|
+
const sourcePath = stripSourceFileExtension(file);
|
|
6314
|
+
if (sourcePath === `${contractsPath}/index`)
|
|
6022
6315
|
return true;
|
|
6023
|
-
if (
|
|
6316
|
+
if (sourcePath === `${contractsPath}/contracts`)
|
|
6024
6317
|
return true;
|
|
6025
|
-
return new RegExp(`^${escapeRegExp(contractsPath)}/[^/]+/contracts
|
|
6318
|
+
return new RegExp(`^${escapeRegExp(contractsPath)}/[^/]+/contracts$`).test(sourcePath);
|
|
6026
6319
|
});
|
|
6027
6320
|
}
|
|
6028
|
-
return files.filter((file) =>
|
|
6029
|
-
file
|
|
6030
|
-
|
|
6031
|
-
|
|
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
|
|
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("/");
|