@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/src/inspect.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { parseProviderPackageMetadata } from "@beignet/core/providers";
|
|
5
|
+
import semver from "semver";
|
|
4
6
|
import ts from "typescript";
|
|
5
7
|
import {
|
|
6
8
|
type ContractFactoryBindings,
|
|
@@ -10,6 +12,16 @@ import {
|
|
|
10
12
|
} from "./analysis/contract-factories.js";
|
|
11
13
|
import { canonicalFeatureLayerPath } from "./analysis/layers.js";
|
|
12
14
|
import { analyzePortWiringFiles } from "./analysis/port-wiring.js";
|
|
15
|
+
import {
|
|
16
|
+
analysisSourceFileExtensions,
|
|
17
|
+
isAnalysisSourceFile,
|
|
18
|
+
isAnalysisSourceFileExtension,
|
|
19
|
+
isJavaScriptSourceFile,
|
|
20
|
+
javascriptAnalysisSourceFileExtensions,
|
|
21
|
+
sourceFileScriptKind,
|
|
22
|
+
stripSourceFileExtension,
|
|
23
|
+
typescriptAnalysisSourceFileExtensions,
|
|
24
|
+
} from "./analysis/source-files.js";
|
|
13
25
|
import { exportedVariableDeclarations } from "./analysis/source-index.js";
|
|
14
26
|
import { createPainter } from "./ansi.js";
|
|
15
27
|
import type { DatabaseSchemaDialect, DatabaseSchemaTable } from "./choices.js";
|
|
@@ -86,6 +98,7 @@ import {
|
|
|
86
98
|
legacyGeneratedPackageScripts,
|
|
87
99
|
} from "./templates/shared.js";
|
|
88
100
|
import { testSupportTemplateFiles } from "./templates/testing.js";
|
|
101
|
+
import { isAppTestFile } from "./test-discovery.js";
|
|
89
102
|
import { getCliVersion } from "./version.js";
|
|
90
103
|
|
|
91
104
|
type InspectAppOptions = {
|
|
@@ -946,7 +959,7 @@ function parseContracts(source: string, file: string): ParsedContract[] {
|
|
|
946
959
|
source,
|
|
947
960
|
ts.ScriptTarget.Latest,
|
|
948
961
|
true,
|
|
949
|
-
|
|
962
|
+
sourceFileScriptKind(file),
|
|
950
963
|
);
|
|
951
964
|
const assignments = variableAssignments(sourceFile);
|
|
952
965
|
const assignmentsByName = new Map(
|
|
@@ -1556,6 +1569,7 @@ function normalizeContractPath(routePath: string): string {
|
|
|
1556
1569
|
async function readRouteExports(
|
|
1557
1570
|
targetDir: string,
|
|
1558
1571
|
routeFiles: string[],
|
|
1572
|
+
files: string[],
|
|
1559
1573
|
config: ResolvedBeignetConfig,
|
|
1560
1574
|
): Promise<Map<string, RouteExport[]>> {
|
|
1561
1575
|
const routeExports = new Map<string, RouteExport[]>();
|
|
@@ -1563,7 +1577,10 @@ async function readRouteExports(
|
|
|
1563
1577
|
for (const file of routeFiles) {
|
|
1564
1578
|
const routePath = nextRoutePath(file, config.paths.routes);
|
|
1565
1579
|
const source = await readFile(path.join(targetDir, file), "utf8");
|
|
1566
|
-
routeExports.set(
|
|
1580
|
+
routeExports.set(
|
|
1581
|
+
file,
|
|
1582
|
+
parseRouteExports(source, file, routePath, files, config),
|
|
1583
|
+
);
|
|
1567
1584
|
}
|
|
1568
1585
|
|
|
1569
1586
|
return routeExports;
|
|
@@ -1573,39 +1590,66 @@ function parseRouteExports(
|
|
|
1573
1590
|
source: string,
|
|
1574
1591
|
handlerFile: string,
|
|
1575
1592
|
routePath: string,
|
|
1593
|
+
files: string[],
|
|
1576
1594
|
config: ResolvedBeignetConfig,
|
|
1577
1595
|
): RouteExport[] {
|
|
1578
1596
|
const exports: RouteExport[] = [];
|
|
1579
|
-
const
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1597
|
+
const sourceFile = ts.createSourceFile(
|
|
1598
|
+
handlerFile,
|
|
1599
|
+
source,
|
|
1600
|
+
ts.ScriptTarget.Latest,
|
|
1601
|
+
true,
|
|
1602
|
+
sourceFileScriptKind(handlerFile),
|
|
1603
|
+
);
|
|
1604
|
+
const imports = parseRouteNamedImports(
|
|
1605
|
+
sourceFile,
|
|
1606
|
+
config,
|
|
1607
|
+
handlerFile,
|
|
1608
|
+
files,
|
|
1609
|
+
);
|
|
1584
1610
|
|
|
1585
|
-
for (const
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1611
|
+
for (const statement of sourceFile.statements) {
|
|
1612
|
+
if (
|
|
1613
|
+
!ts.isVariableStatement(statement) ||
|
|
1614
|
+
!statement.modifiers?.some(
|
|
1615
|
+
(modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword,
|
|
1616
|
+
)
|
|
1617
|
+
) {
|
|
1618
|
+
continue;
|
|
1619
|
+
}
|
|
1590
1620
|
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1621
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
1622
|
+
if (
|
|
1623
|
+
!declaration.initializer ||
|
|
1624
|
+
!ts.isObjectBindingPattern(declaration.name) ||
|
|
1625
|
+
!isNamedRouteFactoryCall(declaration.initializer, "createApiRoute")
|
|
1626
|
+
) {
|
|
1627
|
+
continue;
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
for (const element of declaration.name.elements) {
|
|
1631
|
+
if (!ts.isIdentifier(element.name)) continue;
|
|
1632
|
+
const method = element.name.text;
|
|
1633
|
+
if (!isHttpMethod(method)) continue;
|
|
1634
|
+
exports.push({
|
|
1635
|
+
method,
|
|
1636
|
+
handlerFile,
|
|
1637
|
+
contractRef: routePath,
|
|
1638
|
+
catchAllPrefix: catchAllRoutePrefix(routePath),
|
|
1639
|
+
source: "next-route",
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1598
1642
|
}
|
|
1599
1643
|
}
|
|
1600
1644
|
|
|
1601
|
-
for (const
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1645
|
+
for (const { exportName, declaration } of exportedVariableDeclarations(
|
|
1646
|
+
sourceFile,
|
|
1647
|
+
)) {
|
|
1648
|
+
if (!isHttpMethod(exportName) || !declaration.initializer) continue;
|
|
1649
|
+
const method = exportName;
|
|
1650
|
+
const localName = routeLocalContractIdentifier(declaration.initializer);
|
|
1606
1651
|
|
|
1607
|
-
if (
|
|
1608
|
-
const localName = contractMatch[1];
|
|
1652
|
+
if (localName) {
|
|
1609
1653
|
const imported = imports.get(localName);
|
|
1610
1654
|
exports.push({
|
|
1611
1655
|
method,
|
|
@@ -1617,7 +1661,7 @@ function parseRouteExports(
|
|
|
1617
1661
|
continue;
|
|
1618
1662
|
}
|
|
1619
1663
|
|
|
1620
|
-
if (
|
|
1664
|
+
if (isServerApiExpression(declaration.initializer)) {
|
|
1621
1665
|
exports.push({
|
|
1622
1666
|
method,
|
|
1623
1667
|
handlerFile,
|
|
@@ -1631,6 +1675,100 @@ function parseRouteExports(
|
|
|
1631
1675
|
return exports;
|
|
1632
1676
|
}
|
|
1633
1677
|
|
|
1678
|
+
function parseRouteNamedImports(
|
|
1679
|
+
sourceFile: ts.SourceFile,
|
|
1680
|
+
config: ResolvedBeignetConfig,
|
|
1681
|
+
handlerFile: string,
|
|
1682
|
+
files: string[],
|
|
1683
|
+
): Map<string, { importedName: string; contractFile?: string }> {
|
|
1684
|
+
const imports = new Map<
|
|
1685
|
+
string,
|
|
1686
|
+
{ importedName: string; contractFile?: string }
|
|
1687
|
+
>();
|
|
1688
|
+
|
|
1689
|
+
for (const statement of sourceFile.statements) {
|
|
1690
|
+
if (
|
|
1691
|
+
!ts.isImportDeclaration(statement) ||
|
|
1692
|
+
!ts.isStringLiteral(statement.moduleSpecifier) ||
|
|
1693
|
+
statement.importClause?.isTypeOnly ||
|
|
1694
|
+
!statement.importClause?.namedBindings ||
|
|
1695
|
+
!ts.isNamedImports(statement.importClause.namedBindings)
|
|
1696
|
+
) {
|
|
1697
|
+
continue;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
const contractFile = contractFileFromImport(
|
|
1701
|
+
statement.moduleSpecifier.text,
|
|
1702
|
+
config,
|
|
1703
|
+
handlerFile,
|
|
1704
|
+
files,
|
|
1705
|
+
);
|
|
1706
|
+
for (const element of statement.importClause.namedBindings.elements) {
|
|
1707
|
+
if (element.isTypeOnly) continue;
|
|
1708
|
+
imports.set(element.name.text, {
|
|
1709
|
+
importedName: element.propertyName?.text ?? element.name.text,
|
|
1710
|
+
contractFile,
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
return imports;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
function isNamedRouteFactoryCall(
|
|
1719
|
+
expression: ts.Expression,
|
|
1720
|
+
name: string,
|
|
1721
|
+
): boolean {
|
|
1722
|
+
const unwrapped = unwrapContractExpression(expression);
|
|
1723
|
+
if (!ts.isCallExpression(unwrapped)) return false;
|
|
1724
|
+
const callee = unwrapContractExpression(unwrapped.expression);
|
|
1725
|
+
return ts.isIdentifier(callee) && callee.text === name;
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
function routeLocalContractIdentifier(
|
|
1729
|
+
expression: ts.Expression,
|
|
1730
|
+
): string | undefined {
|
|
1731
|
+
const handleCall = unwrapContractExpression(expression);
|
|
1732
|
+
if (!ts.isCallExpression(handleCall)) return undefined;
|
|
1733
|
+
const handleAccess = unwrapContractExpression(handleCall.expression);
|
|
1734
|
+
if (
|
|
1735
|
+
!ts.isPropertyAccessExpression(handleAccess) ||
|
|
1736
|
+
handleAccess.name.text !== "handle"
|
|
1737
|
+
) {
|
|
1738
|
+
return undefined;
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
const routeCall = unwrapContractExpression(handleAccess.expression);
|
|
1742
|
+
if (!ts.isCallExpression(routeCall)) return undefined;
|
|
1743
|
+
const routeAccess = unwrapContractExpression(routeCall.expression);
|
|
1744
|
+
if (
|
|
1745
|
+
!ts.isPropertyAccessExpression(routeAccess) ||
|
|
1746
|
+
routeAccess.name.text !== "route" ||
|
|
1747
|
+
!ts.isIdentifier(routeAccess.expression) ||
|
|
1748
|
+
routeAccess.expression.text !== "server"
|
|
1749
|
+
) {
|
|
1750
|
+
return undefined;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
const contract = routeCall.arguments[0];
|
|
1754
|
+
const unwrappedContract = contract
|
|
1755
|
+
? unwrapContractExpression(contract)
|
|
1756
|
+
: undefined;
|
|
1757
|
+
return unwrappedContract && ts.isIdentifier(unwrappedContract)
|
|
1758
|
+
? unwrappedContract.text
|
|
1759
|
+
: undefined;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
function isServerApiExpression(expression: ts.Expression): boolean {
|
|
1763
|
+
const unwrapped = unwrapContractExpression(expression);
|
|
1764
|
+
return (
|
|
1765
|
+
ts.isPropertyAccessExpression(unwrapped) &&
|
|
1766
|
+
ts.isIdentifier(unwrapped.expression) &&
|
|
1767
|
+
unwrapped.expression.text === "server" &&
|
|
1768
|
+
unwrapped.name.text === "api"
|
|
1769
|
+
);
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1634
1772
|
function catchAllRoutePrefix(routePath: string): string | undefined {
|
|
1635
1773
|
const segments = routePath.split("/").filter(Boolean);
|
|
1636
1774
|
const catchAllIndex = segments.findIndex((segment) => segment.endsWith("*"));
|
|
@@ -1644,6 +1782,7 @@ function parseNamedImports(
|
|
|
1644
1782
|
source: string,
|
|
1645
1783
|
config: ResolvedBeignetConfig,
|
|
1646
1784
|
importerFile?: string,
|
|
1785
|
+
files?: string[],
|
|
1647
1786
|
): Map<
|
|
1648
1787
|
string,
|
|
1649
1788
|
{
|
|
@@ -1666,6 +1805,7 @@ function parseNamedImports(
|
|
|
1666
1805
|
sourcePath,
|
|
1667
1806
|
config,
|
|
1668
1807
|
importerFile,
|
|
1808
|
+
files,
|
|
1669
1809
|
);
|
|
1670
1810
|
for (const member of match[1].split(",")) {
|
|
1671
1811
|
const parsed = parseImportMember(member);
|
|
@@ -1736,9 +1876,7 @@ function sourceFileFromImport(
|
|
|
1736
1876
|
files?: string[],
|
|
1737
1877
|
): string | undefined {
|
|
1738
1878
|
const resolveCandidate = (candidate: string) => {
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
const candidates = [`${candidate}.ts`, `${candidate}/index.ts`];
|
|
1879
|
+
const candidates = sourceFileSpecifierCandidates(candidate, importerFile);
|
|
1742
1880
|
return files
|
|
1743
1881
|
? candidates.find((file) => files.includes(file))
|
|
1744
1882
|
: candidates[0];
|
|
@@ -1757,11 +1895,77 @@ function sourceFileFromImport(
|
|
|
1757
1895
|
return undefined;
|
|
1758
1896
|
}
|
|
1759
1897
|
|
|
1898
|
+
function sourceFileCandidates(base: string, importerFile?: string): string[] {
|
|
1899
|
+
const extensions =
|
|
1900
|
+
importerFile && isJavaScriptSourceFile(importerFile)
|
|
1901
|
+
? [
|
|
1902
|
+
...javascriptAnalysisSourceFileExtensions,
|
|
1903
|
+
...typescriptAnalysisSourceFileExtensions,
|
|
1904
|
+
]
|
|
1905
|
+
: analysisSourceFileExtensions;
|
|
1906
|
+
return [
|
|
1907
|
+
...extensions.map((extension) => `${base}${extension}`),
|
|
1908
|
+
...extensions.map((extension) => `${base}/index${extension}`),
|
|
1909
|
+
];
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
function sourceFileSpecifierCandidates(
|
|
1913
|
+
candidate: string,
|
|
1914
|
+
importerFile?: string,
|
|
1915
|
+
): string[] {
|
|
1916
|
+
const extension = path.posix.extname(candidate).toLowerCase();
|
|
1917
|
+
if (!extension) return sourceFileCandidates(candidate, importerFile);
|
|
1918
|
+
if (!isAnalysisSourceFileExtension(extension)) return [candidate];
|
|
1919
|
+
|
|
1920
|
+
const sourceExtensions =
|
|
1921
|
+
extension === ".js"
|
|
1922
|
+
? [".ts", ".tsx"]
|
|
1923
|
+
: extension === ".jsx"
|
|
1924
|
+
? [".tsx"]
|
|
1925
|
+
: extension === ".mjs"
|
|
1926
|
+
? [".mts"]
|
|
1927
|
+
: extension === ".cjs"
|
|
1928
|
+
? [".cts"]
|
|
1929
|
+
: [];
|
|
1930
|
+
const sourceCandidates = sourceExtensions.map(
|
|
1931
|
+
(sourceExtension) =>
|
|
1932
|
+
`${candidate.slice(0, -extension.length)}${sourceExtension}`,
|
|
1933
|
+
);
|
|
1934
|
+
const directCandidates =
|
|
1935
|
+
importerFile && isJavaScriptSourceFile(importerFile)
|
|
1936
|
+
? [candidate, ...sourceCandidates]
|
|
1937
|
+
: [...sourceCandidates, candidate];
|
|
1938
|
+
return [
|
|
1939
|
+
...new Set([
|
|
1940
|
+
...directCandidates,
|
|
1941
|
+
...directCandidates.map(directoryIndexSourceFile),
|
|
1942
|
+
]),
|
|
1943
|
+
];
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
function isSourceFileNamed(file: string, name: string): boolean {
|
|
1947
|
+
return (
|
|
1948
|
+
isAnalysisSourceFile(file) &&
|
|
1949
|
+
path.posix.basename(stripSourceFileExtension(file)) === name
|
|
1950
|
+
);
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
function directoryIndexSourceFile(file: string): string {
|
|
1954
|
+
const extension = path.posix.extname(file);
|
|
1955
|
+
return `${stripSourceFileExtension(file)}/index${extension}`;
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1760
1958
|
function contractFileFromImport(
|
|
1761
1959
|
sourcePath: string,
|
|
1762
1960
|
config: ResolvedBeignetConfig,
|
|
1763
1961
|
importerFile?: string,
|
|
1962
|
+
files?: string[],
|
|
1764
1963
|
): string | undefined {
|
|
1964
|
+
const resolveCandidate = (candidate: string) => {
|
|
1965
|
+
const candidates = sourceFileSpecifierCandidates(candidate, importerFile);
|
|
1966
|
+
if (!files) return candidates[0];
|
|
1967
|
+
return candidates.find((file) => files.includes(file)) ?? candidates[0];
|
|
1968
|
+
};
|
|
1765
1969
|
const contractsPath = directoryPath(config.paths.contracts);
|
|
1766
1970
|
const aliasPrefix = `@/${contractsPath}/`;
|
|
1767
1971
|
const aliasExact = `@/${contractsPath}`;
|
|
@@ -1769,21 +1973,21 @@ function contractFileFromImport(
|
|
|
1769
1973
|
const relativeExact = contractsPath;
|
|
1770
1974
|
|
|
1771
1975
|
if (sourcePath === aliasExact || sourcePath === relativeExact) {
|
|
1772
|
-
return `${contractsPath}/index
|
|
1976
|
+
return resolveCandidate(`${contractsPath}/index`);
|
|
1773
1977
|
}
|
|
1774
1978
|
if (sourcePath.startsWith(aliasPrefix)) {
|
|
1775
|
-
return
|
|
1979
|
+
return resolveCandidate(sourcePath.slice("@/".length));
|
|
1776
1980
|
}
|
|
1777
1981
|
|
|
1778
1982
|
if (sourcePath.startsWith(relativePrefix)) {
|
|
1779
|
-
return
|
|
1983
|
+
return resolveCandidate(sourcePath);
|
|
1780
1984
|
}
|
|
1781
1985
|
|
|
1782
1986
|
if (importerFile && sourcePath.startsWith(".")) {
|
|
1783
1987
|
const resolved = normalizePath(
|
|
1784
1988
|
path.join(path.dirname(importerFile), sourcePath),
|
|
1785
1989
|
);
|
|
1786
|
-
return
|
|
1990
|
+
return resolveCandidate(resolved);
|
|
1787
1991
|
}
|
|
1788
1992
|
|
|
1789
1993
|
return undefined;
|
|
@@ -1866,9 +2070,14 @@ async function inspectRouteSurface(
|
|
|
1866
2070
|
const routeFiles = files.filter(
|
|
1867
2071
|
(file) =>
|
|
1868
2072
|
file.startsWith(`${directoryPath(config.paths.routes)}/`) &&
|
|
1869
|
-
file
|
|
2073
|
+
isNextRouteFile(file, config),
|
|
2074
|
+
);
|
|
2075
|
+
const routeExports = await readRouteExports(
|
|
2076
|
+
targetDir,
|
|
2077
|
+
routeFiles,
|
|
2078
|
+
files,
|
|
2079
|
+
config,
|
|
1870
2080
|
);
|
|
1871
|
-
const routeExports = await readRouteExports(targetDir, routeFiles, config);
|
|
1872
2081
|
return matchRoutes(contracts, routeExports);
|
|
1873
2082
|
}
|
|
1874
2083
|
|
|
@@ -1940,8 +2149,7 @@ function findContracts(
|
|
|
1940
2149
|
contract.exportName === routeExport.contractRef &&
|
|
1941
2150
|
(!routeExport.contractFile ||
|
|
1942
2151
|
contract.file === routeExport.contractFile ||
|
|
1943
|
-
contract.file ===
|
|
1944
|
-
routeExport.contractFile.replace(/\.ts$/, "/index.ts")),
|
|
2152
|
+
contract.file === directoryIndexSourceFile(routeExport.contractFile)),
|
|
1945
2153
|
);
|
|
1946
2154
|
return contract ? [contract] : [];
|
|
1947
2155
|
}
|
|
@@ -2385,7 +2593,12 @@ async function inspectCanonicalConformance(
|
|
|
2385
2593
|
|
|
2386
2594
|
const featureRoutesPath = `${featuresPath}/`;
|
|
2387
2595
|
for (const file of files) {
|
|
2388
|
-
if (
|
|
2596
|
+
if (
|
|
2597
|
+
!file.startsWith(featureRoutesPath) ||
|
|
2598
|
+
!isAnalysisSourceFile(file) ||
|
|
2599
|
+
isAppTestFile(file) ||
|
|
2600
|
+
!stripSourceFileExtension(file).endsWith("/routes")
|
|
2601
|
+
) {
|
|
2389
2602
|
continue;
|
|
2390
2603
|
}
|
|
2391
2604
|
|
|
@@ -2774,7 +2987,7 @@ async function inspectProductionReadiness(
|
|
|
2774
2987
|
let hasSecurityHeadersHook = false;
|
|
2775
2988
|
|
|
2776
2989
|
for (const file of files) {
|
|
2777
|
-
if (!file
|
|
2990
|
+
if (!isAnalysisSourceFile(file) || isAppTestFile(file)) continue;
|
|
2778
2991
|
|
|
2779
2992
|
const source = await readCachedSource(targetDir, file, sourceCache);
|
|
2780
2993
|
const routeServerOrInfra = isRouteServerOrInfraFile(file, config);
|
|
@@ -2801,7 +3014,7 @@ async function inspectProductionReadiness(
|
|
|
2801
3014
|
|
|
2802
3015
|
if (
|
|
2803
3016
|
fileWithinDirectory(file, cronDirectory) &&
|
|
2804
|
-
file
|
|
3017
|
+
isNextRouteFile(file, config) &&
|
|
2805
3018
|
!/\bCRON_SECRET\b/.test(source) &&
|
|
2806
3019
|
!/\bcreateOutboxDrainRoute\s*\(/.test(source) &&
|
|
2807
3020
|
!/\bcreateScheduleRoute\s*\(/.test(source)
|
|
@@ -3103,6 +3316,25 @@ async function inspectProductionReadiness(
|
|
|
3103
3316
|
"@beignet/provider-auth-better-auth is installed, but BETTER_AUTH_TRUSTED_ORIGINS is not mentioned in app env/config files. Add it before deploying across multiple origins or document why the app is single-origin only.",
|
|
3104
3317
|
});
|
|
3105
3318
|
}
|
|
3319
|
+
|
|
3320
|
+
if (
|
|
3321
|
+
(await betterAuthRequiresIssuerSchema(targetDir, packageJson)) &&
|
|
3322
|
+
(await anyFileContains(
|
|
3323
|
+
targetDir,
|
|
3324
|
+
configFiles,
|
|
3325
|
+
"drizzleAdapter",
|
|
3326
|
+
sourceCache,
|
|
3327
|
+
)) &&
|
|
3328
|
+
!(await hasBetterAuthIssuerSchema(targetDir, files, sourceCache))
|
|
3329
|
+
) {
|
|
3330
|
+
diagnostics.push({
|
|
3331
|
+
severity: "warning",
|
|
3332
|
+
code: "BEIGNET_BETTER_AUTH_ISSUER_SCHEMA_MISSING",
|
|
3333
|
+
file: schemaDir,
|
|
3334
|
+
message:
|
|
3335
|
+
"This app uses Better Auth 1.7 or a version range that can install it with the Drizzle adapter, but its account schema does not include the required issuer field and unique (issuer, accountId) index. Complete the provider-specific account identity backfill, run bunx auth generate, and apply the reviewed migration before deploying Better Auth 1.7.",
|
|
3336
|
+
});
|
|
3337
|
+
}
|
|
3106
3338
|
}
|
|
3107
3339
|
|
|
3108
3340
|
const installedReadinessProviders = [...installedPackages].filter(
|
|
@@ -3148,7 +3380,7 @@ async function uploadHasExplicitSizeLimit(
|
|
|
3148
3380
|
);
|
|
3149
3381
|
if (!importedName) continue;
|
|
3150
3382
|
|
|
3151
|
-
const importedFile =
|
|
3383
|
+
const importedFile = sourceFileFromImport(moduleName, file, files);
|
|
3152
3384
|
if (!importedFile) continue;
|
|
3153
3385
|
|
|
3154
3386
|
const importedSource = await readCachedSource(
|
|
@@ -3203,20 +3435,6 @@ function exportedObjectHasProperty(
|
|
|
3203
3435
|
return new RegExp(`\\b${escapeRegExp(propertyName)}\\s*:`).test(initializer);
|
|
3204
3436
|
}
|
|
3205
3437
|
|
|
3206
|
-
function resolveLocalTypeScriptModule(
|
|
3207
|
-
importer: string,
|
|
3208
|
-
moduleName: string,
|
|
3209
|
-
files: string[],
|
|
3210
|
-
): string | undefined {
|
|
3211
|
-
const base = normalizePath(
|
|
3212
|
-
path.posix.normalize(
|
|
3213
|
-
path.posix.join(path.posix.dirname(importer), moduleName),
|
|
3214
|
-
),
|
|
3215
|
-
);
|
|
3216
|
-
const candidates = [base, `${base}.ts`, `${base}.tsx`, `${base}/index.ts`];
|
|
3217
|
-
return candidates.find((candidate) => files.includes(candidate));
|
|
3218
|
-
}
|
|
3219
|
-
|
|
3220
3438
|
async function hasReadinessCheck(
|
|
3221
3439
|
targetDir: string,
|
|
3222
3440
|
files: string[],
|
|
@@ -3224,10 +3442,18 @@ async function hasReadinessCheck(
|
|
|
3224
3442
|
sourceCache: Map<string, string>,
|
|
3225
3443
|
): Promise<boolean> {
|
|
3226
3444
|
const routesDir = directoryPath(config.paths.routes);
|
|
3227
|
-
if (
|
|
3445
|
+
if (
|
|
3446
|
+
files.some(
|
|
3447
|
+
(file) =>
|
|
3448
|
+
path.posix.dirname(file) === `${routesDir}/ready` &&
|
|
3449
|
+
isNextRouteFile(file, config),
|
|
3450
|
+
)
|
|
3451
|
+
) {
|
|
3452
|
+
return true;
|
|
3453
|
+
}
|
|
3228
3454
|
|
|
3229
3455
|
for (const file of files) {
|
|
3230
|
-
if (!file
|
|
3456
|
+
if (!isAnalysisSourceFile(file) || isAppTestFile(file)) continue;
|
|
3231
3457
|
if (!isRouteServerOrInfraFile(file, config)) continue;
|
|
3232
3458
|
|
|
3233
3459
|
const source = await readCachedSource(targetDir, file, sourceCache);
|
|
@@ -3769,10 +3995,11 @@ function isFeatureUploadDefinition(
|
|
|
3769
3995
|
file: string,
|
|
3770
3996
|
config: ResolvedBeignetConfig,
|
|
3771
3997
|
): boolean {
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3998
|
+
return isFeatureArtifactFile(
|
|
3999
|
+
file,
|
|
4000
|
+
directoryPath(config.paths.features),
|
|
4001
|
+
"uploads",
|
|
4002
|
+
);
|
|
3776
4003
|
}
|
|
3777
4004
|
|
|
3778
4005
|
function operationalConfigFiles(
|
|
@@ -3782,17 +4009,263 @@ function operationalConfigFiles(
|
|
|
3782
4009
|
return providerOperationalConfigFiles(files, config);
|
|
3783
4010
|
}
|
|
3784
4011
|
|
|
3785
|
-
|
|
4012
|
+
type InspectedPackageJson = {
|
|
4013
|
+
dependencies?: Record<string, string>;
|
|
4014
|
+
devDependencies?: Record<string, string>;
|
|
4015
|
+
peerDependencies?: Record<string, string>;
|
|
4016
|
+
};
|
|
4017
|
+
|
|
4018
|
+
function declaredBetterAuthMayRequireIssuerSchema(
|
|
4019
|
+
packageJson: InspectedPackageJson | undefined,
|
|
4020
|
+
): boolean {
|
|
4021
|
+
const version =
|
|
4022
|
+
packageJson?.dependencies?.["better-auth"] ??
|
|
4023
|
+
packageJson?.devDependencies?.["better-auth"] ??
|
|
4024
|
+
packageJson?.peerDependencies?.["better-auth"];
|
|
4025
|
+
if (!version) return false;
|
|
4026
|
+
|
|
4027
|
+
const normalized = version.trim();
|
|
4028
|
+
const workspaceRange = normalized.startsWith("workspace:")
|
|
4029
|
+
? normalized.slice("workspace:".length)
|
|
4030
|
+
: normalized;
|
|
4031
|
+
const npmAlias = /^npm:better-auth@(.+)$/.exec(workspaceRange);
|
|
4032
|
+
const candidate = npmAlias?.[1] ?? workspaceRange;
|
|
4033
|
+
const range = semver.validRange(candidate);
|
|
4034
|
+
|
|
4035
|
+
// Tags, catalogs, local paths, and other non-semver declarations can resolve
|
|
4036
|
+
// to 1.7. Prefer a migration warning until an installed version is readable.
|
|
4037
|
+
if (!range) return true;
|
|
4038
|
+
|
|
4039
|
+
return semver.intersects(range, ">=1.7.0");
|
|
4040
|
+
}
|
|
4041
|
+
|
|
4042
|
+
async function betterAuthRequiresIssuerSchema(
|
|
4043
|
+
targetDir: string,
|
|
4044
|
+
packageJson: InspectedPackageJson | undefined,
|
|
4045
|
+
): Promise<boolean> {
|
|
4046
|
+
const installed = await readInstalledBetterAuthPackageJson(targetDir);
|
|
4047
|
+
if (installed?.version) {
|
|
4048
|
+
const version = semver.valid(installed.version.trim());
|
|
4049
|
+
if (version) return semver.gte(version, "1.7.0");
|
|
4050
|
+
}
|
|
4051
|
+
|
|
4052
|
+
return declaredBetterAuthMayRequireIssuerSchema(packageJson);
|
|
4053
|
+
}
|
|
4054
|
+
|
|
4055
|
+
async function readInstalledBetterAuthPackageJson(
|
|
4056
|
+
targetDir: string,
|
|
4057
|
+
): Promise<{ name?: string; version?: string } | undefined> {
|
|
4058
|
+
const nodeModulesPackage =
|
|
4059
|
+
await readBetterAuthPackageFromNodeModules(targetDir);
|
|
4060
|
+
if (nodeModulesPackage) return nodeModulesPackage;
|
|
4061
|
+
|
|
4062
|
+
const requireFromTarget = createRequire(path.join(targetDir, "package.json"));
|
|
4063
|
+
const packageJsonPath = resolveModuleFromTarget(
|
|
4064
|
+
requireFromTarget,
|
|
4065
|
+
"better-auth/package.json",
|
|
4066
|
+
);
|
|
4067
|
+
if (packageJsonPath) {
|
|
4068
|
+
return readJsonIfExists(packageJsonPath);
|
|
4069
|
+
}
|
|
4070
|
+
|
|
4071
|
+
const entryPath = resolveModuleFromTarget(requireFromTarget, "better-auth");
|
|
4072
|
+
if (!entryPath) return undefined;
|
|
4073
|
+
|
|
4074
|
+
let directory = path.dirname(entryPath);
|
|
4075
|
+
while (true) {
|
|
4076
|
+
const candidate = await readJsonIfExists<{
|
|
4077
|
+
name?: string;
|
|
4078
|
+
version?: string;
|
|
4079
|
+
}>(path.join(directory, "package.json"));
|
|
4080
|
+
if (candidate?.name === "better-auth") return candidate;
|
|
4081
|
+
|
|
4082
|
+
const parent = path.dirname(directory);
|
|
4083
|
+
if (parent === directory) return undefined;
|
|
4084
|
+
directory = parent;
|
|
4085
|
+
}
|
|
4086
|
+
}
|
|
4087
|
+
|
|
4088
|
+
async function readBetterAuthPackageFromNodeModules(
|
|
4089
|
+
targetDir: string,
|
|
4090
|
+
): Promise<{ name?: string; version?: string } | undefined> {
|
|
4091
|
+
let directory = targetDir;
|
|
4092
|
+
while (true) {
|
|
4093
|
+
const candidate = await readJsonIfExists<{
|
|
4094
|
+
name?: string;
|
|
4095
|
+
version?: string;
|
|
4096
|
+
}>(path.join(directory, "node_modules", "better-auth", "package.json"));
|
|
4097
|
+
if (candidate?.name === "better-auth") return candidate;
|
|
4098
|
+
|
|
4099
|
+
const parent = path.dirname(directory);
|
|
4100
|
+
if (parent === directory) return undefined;
|
|
4101
|
+
directory = parent;
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
|
|
4105
|
+
function resolveModuleFromTarget(
|
|
4106
|
+
requireFromTarget: ReturnType<typeof createRequire>,
|
|
4107
|
+
request: string,
|
|
4108
|
+
): string | undefined {
|
|
4109
|
+
try {
|
|
4110
|
+
return requireFromTarget.resolve(request);
|
|
4111
|
+
} catch {
|
|
4112
|
+
return undefined;
|
|
4113
|
+
}
|
|
4114
|
+
}
|
|
4115
|
+
|
|
4116
|
+
async function hasBetterAuthIssuerSchema(
|
|
3786
4117
|
targetDir: string,
|
|
3787
4118
|
files: string[],
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
4119
|
+
sourceCache: Map<string, string>,
|
|
4120
|
+
): Promise<boolean> {
|
|
4121
|
+
const schemaFiles = files.filter(
|
|
4122
|
+
(file) =>
|
|
4123
|
+
!isTestSourceFile(file) &&
|
|
4124
|
+
/\.(?:ts|tsx|js|jsx|mts|mjs|cts|cjs)$/.test(file),
|
|
4125
|
+
);
|
|
4126
|
+
|
|
4127
|
+
for (const file of schemaFiles) {
|
|
4128
|
+
const source = await readCachedSource(targetDir, file, sourceCache);
|
|
4129
|
+
if (sourceHasBetterAuthIssuerSchema(file, source)) return true;
|
|
4130
|
+
}
|
|
4131
|
+
|
|
4132
|
+
return false;
|
|
4133
|
+
}
|
|
4134
|
+
|
|
4135
|
+
function sourceHasBetterAuthIssuerSchema(
|
|
4136
|
+
file: string,
|
|
4137
|
+
source: string,
|
|
4138
|
+
): boolean {
|
|
4139
|
+
const sourceFile = ts.createSourceFile(
|
|
4140
|
+
file,
|
|
4141
|
+
source,
|
|
4142
|
+
ts.ScriptTarget.Latest,
|
|
4143
|
+
true,
|
|
4144
|
+
);
|
|
4145
|
+
let found = false;
|
|
4146
|
+
const visit = (node: ts.Node): void => {
|
|
4147
|
+
if (found) return;
|
|
4148
|
+
if (
|
|
4149
|
+
ts.isCallExpression(node) &&
|
|
4150
|
+
isBetterAuthAccountTableCall(node) &&
|
|
4151
|
+
betterAuthAccountTableHasIssuerIdentity(node)
|
|
4152
|
+
) {
|
|
4153
|
+
found = true;
|
|
4154
|
+
return;
|
|
3793
4155
|
}
|
|
3794
|
-
|
|
3795
|
-
|
|
4156
|
+
ts.forEachChild(node, visit);
|
|
4157
|
+
};
|
|
4158
|
+
visit(sourceFile);
|
|
4159
|
+
return found;
|
|
4160
|
+
}
|
|
4161
|
+
|
|
4162
|
+
function isBetterAuthAccountTableCall(call: ts.CallExpression): boolean {
|
|
4163
|
+
const callee = unwrapContractExpression(call.expression);
|
|
4164
|
+
const factoryName = ts.isIdentifier(callee)
|
|
4165
|
+
? callee.text
|
|
4166
|
+
: ts.isPropertyAccessExpression(callee)
|
|
4167
|
+
? callee.name.text
|
|
4168
|
+
: undefined;
|
|
4169
|
+
return (
|
|
4170
|
+
factoryName !== undefined &&
|
|
4171
|
+
["mysqlTable", "pgTable", "sqliteTable"].includes(factoryName) &&
|
|
4172
|
+
staticString(call.arguments[0]) === "account"
|
|
4173
|
+
);
|
|
4174
|
+
}
|
|
4175
|
+
|
|
4176
|
+
function betterAuthAccountTableHasIssuerIdentity(
|
|
4177
|
+
call: ts.CallExpression,
|
|
4178
|
+
): boolean {
|
|
4179
|
+
const columns = call.arguments[1];
|
|
4180
|
+
if (!columns || !ts.isObjectLiteralExpression(columns)) return false;
|
|
4181
|
+
const issuer = columns.properties.find(
|
|
4182
|
+
(property) =>
|
|
4183
|
+
ts.isPropertyAssignment(property) &&
|
|
4184
|
+
staticPropertyName(property.name) === "issuer",
|
|
4185
|
+
);
|
|
4186
|
+
if (!issuer || !ts.isPropertyAssignment(issuer)) return false;
|
|
4187
|
+
if (!isBetterAuthIssuerColumn(issuer.initializer)) return false;
|
|
4188
|
+
|
|
4189
|
+
const constraints = call.arguments[2];
|
|
4190
|
+
if (
|
|
4191
|
+
!constraints ||
|
|
4192
|
+
(!ts.isArrowFunction(constraints) &&
|
|
4193
|
+
!ts.isFunctionExpression(constraints)) ||
|
|
4194
|
+
constraints.parameters.length === 0 ||
|
|
4195
|
+
!ts.isIdentifier(constraints.parameters[0]?.name)
|
|
4196
|
+
) {
|
|
4197
|
+
return false;
|
|
4198
|
+
}
|
|
4199
|
+
|
|
4200
|
+
const tableName = constraints.parameters[0].name.text;
|
|
4201
|
+
let found = false;
|
|
4202
|
+
const visit = (node: ts.Node): void => {
|
|
4203
|
+
if (found) return;
|
|
4204
|
+
if (
|
|
4205
|
+
ts.isCallExpression(node) &&
|
|
4206
|
+
isBetterAuthIssuerAccountUniqueConstraint(node, tableName)
|
|
4207
|
+
) {
|
|
4208
|
+
found = true;
|
|
4209
|
+
return;
|
|
4210
|
+
}
|
|
4211
|
+
ts.forEachChild(node, visit);
|
|
4212
|
+
};
|
|
4213
|
+
visit(constraints.body);
|
|
4214
|
+
return found;
|
|
4215
|
+
}
|
|
4216
|
+
|
|
4217
|
+
function isBetterAuthIssuerColumn(expression: ts.Expression): boolean {
|
|
4218
|
+
let current = unwrapContractExpression(expression);
|
|
4219
|
+
while (ts.isCallExpression(current)) {
|
|
4220
|
+
const callee = unwrapContractExpression(current.expression);
|
|
4221
|
+
if (
|
|
4222
|
+
ts.isIdentifier(callee) &&
|
|
4223
|
+
["text", "varchar"].includes(callee.text) &&
|
|
4224
|
+
staticString(current.arguments[0]) === "issuer"
|
|
4225
|
+
) {
|
|
4226
|
+
return true;
|
|
4227
|
+
}
|
|
4228
|
+
if (!ts.isPropertyAccessExpression(callee)) return false;
|
|
4229
|
+
current = unwrapContractExpression(callee.expression);
|
|
4230
|
+
}
|
|
4231
|
+
return false;
|
|
4232
|
+
}
|
|
4233
|
+
|
|
4234
|
+
function isBetterAuthIssuerAccountUniqueConstraint(
|
|
4235
|
+
call: ts.CallExpression,
|
|
4236
|
+
tableName: string,
|
|
4237
|
+
): boolean {
|
|
4238
|
+
const callee = unwrapContractExpression(call.expression);
|
|
4239
|
+
if (!ts.isPropertyAccessExpression(callee) || callee.name.text !== "on") {
|
|
4240
|
+
return false;
|
|
4241
|
+
}
|
|
4242
|
+
|
|
4243
|
+
const factoryCall = unwrapContractExpression(callee.expression);
|
|
4244
|
+
if (!ts.isCallExpression(factoryCall)) return false;
|
|
4245
|
+
const factory = unwrapContractExpression(factoryCall.expression);
|
|
4246
|
+
if (
|
|
4247
|
+
!ts.isIdentifier(factory) ||
|
|
4248
|
+
!["unique", "uniqueIndex"].includes(factory.text)
|
|
4249
|
+
) {
|
|
4250
|
+
return false;
|
|
4251
|
+
}
|
|
4252
|
+
|
|
4253
|
+
if (call.arguments.length !== 2) return false;
|
|
4254
|
+
const columnNames = call.arguments.map((argument) => {
|
|
4255
|
+
const column = unwrapContractExpression(argument);
|
|
4256
|
+
return ts.isPropertyAccessExpression(column) &&
|
|
4257
|
+
ts.isIdentifier(column.expression) &&
|
|
4258
|
+
column.expression.text === tableName
|
|
4259
|
+
? column.name.text
|
|
4260
|
+
: undefined;
|
|
4261
|
+
});
|
|
4262
|
+
return columnNames.includes("issuer") && columnNames.includes("accountId");
|
|
4263
|
+
}
|
|
4264
|
+
|
|
4265
|
+
async function readPackageJson(
|
|
4266
|
+
targetDir: string,
|
|
4267
|
+
files: string[],
|
|
4268
|
+
): Promise<InspectedPackageJson | undefined> {
|
|
3796
4269
|
if (!files.includes("package.json")) return undefined;
|
|
3797
4270
|
|
|
3798
4271
|
return JSON.parse(
|
|
@@ -3953,7 +4426,7 @@ async function hasBillingEntitlementsProviderWiring(
|
|
|
3953
4426
|
const serverPath = path.dirname(config.paths.server);
|
|
3954
4427
|
const candidateFiles = files.filter(
|
|
3955
4428
|
(file) =>
|
|
3956
|
-
|
|
4429
|
+
isAnalysisSourceFile(file) &&
|
|
3957
4430
|
!isTestSourceFile(file) &&
|
|
3958
4431
|
(file.startsWith(`${infraPath}/`) || file.startsWith(`${serverPath}/`)),
|
|
3959
4432
|
);
|
|
@@ -3969,10 +4442,7 @@ async function hasBillingEntitlementsProviderWiring(
|
|
|
3969
4442
|
}
|
|
3970
4443
|
|
|
3971
4444
|
function isTestSourceFile(file: string): boolean {
|
|
3972
|
-
return (
|
|
3973
|
-
/(?:^|\/)tests\//.test(file) ||
|
|
3974
|
-
/\.(?:test|spec)\.(?:ts|tsx|mts|cts)$/.test(file)
|
|
3975
|
-
);
|
|
4445
|
+
return /(?:^|\/)tests\//.test(file) || isAppTestFile(file);
|
|
3976
4446
|
}
|
|
3977
4447
|
|
|
3978
4448
|
function exportedConstInitializer(
|
|
@@ -3999,8 +4469,8 @@ function featureNameFromContractFile(
|
|
|
3999
4469
|
): string | undefined {
|
|
4000
4470
|
const featuresPath = directoryPath(config.paths.features);
|
|
4001
4471
|
const match = new RegExp(
|
|
4002
|
-
`^${escapeRegExp(featuresPath)}/([^/]+)/contracts
|
|
4003
|
-
).exec(file);
|
|
4472
|
+
`^${escapeRegExp(featuresPath)}/([^/]+)/contracts$`,
|
|
4473
|
+
).exec(stripSourceFileExtension(file));
|
|
4004
4474
|
return match?.[1];
|
|
4005
4475
|
}
|
|
4006
4476
|
|
|
@@ -4017,7 +4487,7 @@ function featureRuntimeFiles(
|
|
|
4017
4487
|
"use-cases",
|
|
4018
4488
|
];
|
|
4019
4489
|
return files.filter((file) => {
|
|
4020
|
-
if (!file
|
|
4490
|
+
if (!isAnalysisSourceFile(file) || isAppTestFile(file)) return false;
|
|
4021
4491
|
return runtimeFolders.some((folder) =>
|
|
4022
4492
|
file.startsWith(`${featuresPath}/${feature}/${folder}/`),
|
|
4023
4493
|
);
|
|
@@ -4033,11 +4503,16 @@ function featureTestFiles(
|
|
|
4033
4503
|
return files.filter((file) => {
|
|
4034
4504
|
if (
|
|
4035
4505
|
!file.startsWith(`${featuresPath}/${feature}/`) ||
|
|
4036
|
-
!file
|
|
4506
|
+
!isAppTestFile(file)
|
|
4037
4507
|
) {
|
|
4038
4508
|
return false;
|
|
4039
4509
|
}
|
|
4040
|
-
return
|
|
4510
|
+
return (
|
|
4511
|
+
!options.policyOnly ||
|
|
4512
|
+
["policy.test", "policy.spec"].includes(
|
|
4513
|
+
path.posix.basename(stripSourceFileExtension(file)),
|
|
4514
|
+
)
|
|
4515
|
+
);
|
|
4041
4516
|
});
|
|
4042
4517
|
}
|
|
4043
4518
|
|
|
@@ -4204,17 +4679,12 @@ async function inspectFeatureArtifactDrift(
|
|
|
4204
4679
|
severity: "warning",
|
|
4205
4680
|
code: "BEIGNET_FEATURE_TEST_PLACEMENT",
|
|
4206
4681
|
file,
|
|
4207
|
-
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
|
|
4682
|
+
message: `${file} places a test directly under ${featuresPath}/${match[1]}/. Move feature behavior tests into ${featuresPath}/${match[1]}/tests/ so feature suites stay in the canonical folder; adjacent test files are for infra and server modules.`,
|
|
4208
4683
|
});
|
|
4209
4684
|
}
|
|
4210
4685
|
|
|
4211
4686
|
for (const file of files) {
|
|
4212
|
-
if (
|
|
4213
|
-
!/\.(?:ts|tsx|mts|cts)$/.test(file) ||
|
|
4214
|
-
/\.d\.(?:ts|mts|cts)$/.test(file)
|
|
4215
|
-
) {
|
|
4216
|
-
continue;
|
|
4217
|
-
}
|
|
4687
|
+
if (!isAnalysisSourceFile(file)) continue;
|
|
4218
4688
|
|
|
4219
4689
|
const misplaced = directFeatureTestMatch(file, featuresPath)
|
|
4220
4690
|
? undefined
|
|
@@ -4229,7 +4699,6 @@ async function inspectFeatureArtifactDrift(
|
|
|
4229
4699
|
}
|
|
4230
4700
|
|
|
4231
4701
|
if (isTestSourceFile(file)) continue;
|
|
4232
|
-
if (!file.endsWith(".ts")) continue;
|
|
4233
4702
|
|
|
4234
4703
|
if (
|
|
4235
4704
|
isNotificationDefinitionFile(file, featuresPath) ||
|
|
@@ -4255,9 +4724,10 @@ function directFeatureTestMatch(
|
|
|
4255
4724
|
file: string,
|
|
4256
4725
|
featuresPath: string,
|
|
4257
4726
|
): RegExpExecArray | null {
|
|
4727
|
+
if (!isAppTestFile(file)) return null;
|
|
4258
4728
|
return new RegExp(
|
|
4259
|
-
`^${escapeRegExp(featuresPath)}/([^/]+)/[^/]+\\.(?:test|spec)
|
|
4260
|
-
).exec(file);
|
|
4729
|
+
`^${escapeRegExp(featuresPath)}/([^/]+)/[^/]+\\.(?:test|spec)$`,
|
|
4730
|
+
).exec(stripSourceFileExtension(file));
|
|
4261
4731
|
}
|
|
4262
4732
|
|
|
4263
4733
|
function isFeatureArtifactFile(
|
|
@@ -4265,9 +4735,10 @@ function isFeatureArtifactFile(
|
|
|
4265
4735
|
featuresPath: string,
|
|
4266
4736
|
folder: "uploads" | "notifications",
|
|
4267
4737
|
): boolean {
|
|
4738
|
+
if (!isAnalysisSourceFile(file) || isAppTestFile(file)) return false;
|
|
4268
4739
|
const match = new RegExp(
|
|
4269
|
-
`^${escapeRegExp(featuresPath)}/[^/]+/${folder}/([^/]+)
|
|
4270
|
-
).exec(file);
|
|
4740
|
+
`^${escapeRegExp(featuresPath)}/[^/]+/${folder}/([^/]+)$`,
|
|
4741
|
+
).exec(stripSourceFileExtension(file));
|
|
4271
4742
|
return Boolean(match && match[1] !== "index");
|
|
4272
4743
|
}
|
|
4273
4744
|
|
|
@@ -4277,7 +4748,7 @@ async function appHasUploadRoute(
|
|
|
4277
4748
|
config: ResolvedBeignetConfig,
|
|
4278
4749
|
): Promise<boolean> {
|
|
4279
4750
|
for (const file of files) {
|
|
4280
|
-
if (!file
|
|
4751
|
+
if (!isAnalysisSourceFile(file) || isAppTestFile(file)) continue;
|
|
4281
4752
|
if (!isRouteServerOrInfraFile(file, config)) continue;
|
|
4282
4753
|
|
|
4283
4754
|
const source = await readFile(path.join(targetDir, file), "utf8");
|
|
@@ -4831,11 +5302,17 @@ async function readFeatureWorkflowRegistries(
|
|
|
4831
5302
|
for (const kind of kinds) {
|
|
4832
5303
|
const folder = kind === "events" ? "domain/events" : kind;
|
|
4833
5304
|
const pattern = new RegExp(
|
|
4834
|
-
`^${escapeRegExp(featuresPath)}/[^/]+/${escapeRegExp(folder)}/index
|
|
5305
|
+
`^${escapeRegExp(featuresPath)}/[^/]+/${escapeRegExp(folder)}/index$`,
|
|
4835
5306
|
);
|
|
4836
5307
|
|
|
4837
5308
|
for (const file of files) {
|
|
4838
|
-
if (
|
|
5309
|
+
if (
|
|
5310
|
+
!isAnalysisSourceFile(file) ||
|
|
5311
|
+
isWorkflowTestFile(file) ||
|
|
5312
|
+
!pattern.test(stripSourceFileExtension(file))
|
|
5313
|
+
) {
|
|
5314
|
+
continue;
|
|
5315
|
+
}
|
|
4839
5316
|
|
|
4840
5317
|
const source = await readFile(path.join(targetDir, file), "utf8");
|
|
4841
5318
|
const memberFiles = exportedMemberFiles(source, file, files);
|
|
@@ -4895,8 +5372,7 @@ async function readRuntimeManifestIdentifiers(
|
|
|
4895
5372
|
const manifests: RuntimeManifestIdentifierSets[] = [];
|
|
4896
5373
|
|
|
4897
5374
|
for (const file of files) {
|
|
4898
|
-
if (!file
|
|
4899
|
-
if (file.endsWith(".d.ts") || isWorkflowTestFile(file)) continue;
|
|
5375
|
+
if (!isAnalysisSourceFile(file) || isWorkflowTestFile(file)) continue;
|
|
4900
5376
|
|
|
4901
5377
|
const source = await readFile(path.join(targetDir, file), "utf8");
|
|
4902
5378
|
if (!source.includes("defineRuntimeManifest")) continue;
|
|
@@ -5033,12 +5509,18 @@ async function listenedEventNames(
|
|
|
5033
5509
|
): Promise<Set<string>> {
|
|
5034
5510
|
const featuresPath = directoryPath(config.paths.features);
|
|
5035
5511
|
const listenerPattern = new RegExp(
|
|
5036
|
-
`^${escapeRegExp(featuresPath)}/[^/]+/listeners/[^/]
|
|
5512
|
+
`^${escapeRegExp(featuresPath)}/[^/]+/listeners/[^/]+$`,
|
|
5037
5513
|
);
|
|
5038
5514
|
const listened = new Set<string>();
|
|
5039
5515
|
|
|
5040
5516
|
for (const file of files) {
|
|
5041
|
-
if (
|
|
5517
|
+
if (
|
|
5518
|
+
!isAnalysisSourceFile(file) ||
|
|
5519
|
+
isWorkflowTestFile(file) ||
|
|
5520
|
+
!listenerPattern.test(stripSourceFileExtension(file))
|
|
5521
|
+
) {
|
|
5522
|
+
continue;
|
|
5523
|
+
}
|
|
5042
5524
|
|
|
5043
5525
|
const source = await readFile(path.join(targetDir, file), "utf8");
|
|
5044
5526
|
const callPattern = /\bdefineListener\s*\(/g;
|
|
@@ -5087,8 +5569,7 @@ async function listenerWiringReferences(
|
|
|
5087
5569
|
const listenerRegistryFile = listenersFilePath(config);
|
|
5088
5570
|
|
|
5089
5571
|
for (const file of files) {
|
|
5090
|
-
if (!file
|
|
5091
|
-
if (file.endsWith(".d.ts") || isWorkflowTestFile(file)) continue;
|
|
5572
|
+
if (!isAnalysisSourceFile(file) || isWorkflowTestFile(file)) continue;
|
|
5092
5573
|
|
|
5093
5574
|
const source = await readFile(path.join(targetDir, file), "utf8");
|
|
5094
5575
|
const namedImports = parseNamedImportSources(source);
|
|
@@ -5097,7 +5578,7 @@ async function listenerWiringReferences(
|
|
|
5097
5578
|
source,
|
|
5098
5579
|
ts.ScriptTarget.Latest,
|
|
5099
5580
|
true,
|
|
5100
|
-
file
|
|
5581
|
+
sourceFileScriptKind(file),
|
|
5101
5582
|
);
|
|
5102
5583
|
const registerListenersBindings = importedRegisterListenersBindings(
|
|
5103
5584
|
source,
|
|
@@ -5557,10 +6038,7 @@ function callReferencesCentralListenerRegistry(args: {
|
|
|
5557
6038
|
|
|
5558
6039
|
function isWorkflowTestFile(file: string): boolean {
|
|
5559
6040
|
return (
|
|
5560
|
-
file.
|
|
5561
|
-
file.endsWith(".test.tsx") ||
|
|
5562
|
-
file.startsWith("tests/") ||
|
|
5563
|
-
file.includes("/tests/")
|
|
6041
|
+
isAppTestFile(file) || file.startsWith("tests/") || file.includes("/tests/")
|
|
5564
6042
|
);
|
|
5565
6043
|
}
|
|
5566
6044
|
|
|
@@ -5575,13 +6053,7 @@ async function inspectServerlessFootguns(
|
|
|
5575
6053
|
const cronDirectory = path.posix.join(routeDirectory, "cron");
|
|
5576
6054
|
|
|
5577
6055
|
for (const file of files) {
|
|
5578
|
-
if (
|
|
5579
|
-
!file.endsWith(".ts") ||
|
|
5580
|
-
file.endsWith(".test.ts") ||
|
|
5581
|
-
file.endsWith(".d.ts")
|
|
5582
|
-
) {
|
|
5583
|
-
continue;
|
|
5584
|
-
}
|
|
6056
|
+
if (!isAnalysisSourceFile(file) || isAppTestFile(file)) continue;
|
|
5585
6057
|
|
|
5586
6058
|
const source = await readFile(path.join(targetDir, file), "utf8");
|
|
5587
6059
|
const isInfraOrServerFile = isRuntimeFile(file, config);
|
|
@@ -5727,9 +6199,10 @@ function fileWithinDirectory(file: string, directory: string): boolean {
|
|
|
5727
6199
|
}
|
|
5728
6200
|
|
|
5729
6201
|
function isProviderLifecycleFile(file: string, source?: string): boolean {
|
|
6202
|
+
const sourcePath = stripSourceFileExtension(file);
|
|
5730
6203
|
return (
|
|
5731
|
-
/(^|\/)provider(s)
|
|
5732
|
-
/(^|\/)[^/]*provider[^/]
|
|
6204
|
+
/(^|\/)provider(s)?$/.test(sourcePath) ||
|
|
6205
|
+
/(^|\/)[^/]*provider[^/]*$/.test(sourcePath) ||
|
|
5733
6206
|
/(^|\/)providers?\//.test(file) ||
|
|
5734
6207
|
(source !== undefined && containsCallExpression(source, "createProvider"))
|
|
5735
6208
|
);
|
|
@@ -5740,8 +6213,9 @@ function isNextRouteFile(file: string, config: ResolvedBeignetConfig): boolean {
|
|
|
5740
6213
|
|
|
5741
6214
|
return (
|
|
5742
6215
|
file.startsWith(`${routesPath}/`) &&
|
|
5743
|
-
file
|
|
5744
|
-
|
|
6216
|
+
isSourceFileNamed(file, "route") &&
|
|
6217
|
+
[".js", ".jsx", ".ts", ".tsx"].includes(path.posix.extname(file)) &&
|
|
6218
|
+
!isAppTestFile(file)
|
|
5745
6219
|
);
|
|
5746
6220
|
}
|
|
5747
6221
|
|
|
@@ -7676,7 +8150,13 @@ async function inspectFeatureRouteRegistration(
|
|
|
7676
8150
|
const feature = featureNameFromContractFile(contract.file, config);
|
|
7677
8151
|
if (!feature) continue;
|
|
7678
8152
|
|
|
7679
|
-
const
|
|
8153
|
+
const routeStem = `${featuresPath}/${feature}/routes`;
|
|
8154
|
+
const routeFile =
|
|
8155
|
+
files.find(
|
|
8156
|
+
(file) =>
|
|
8157
|
+
isAnalysisSourceFile(file) &&
|
|
8158
|
+
stripSourceFileExtension(file) === routeStem,
|
|
8159
|
+
) ?? `${routeStem}${path.posix.extname(contract.file) || ".ts"}`;
|
|
7680
8160
|
const featureRouteGroups = routeGroups.filter(
|
|
7681
8161
|
(routeGroup) => routeGroup.file === routeFile,
|
|
7682
8162
|
);
|
|
@@ -7766,13 +8246,14 @@ async function readFeatureRouteGroups(
|
|
|
7766
8246
|
): Promise<FeatureRouteGroup[]> {
|
|
7767
8247
|
const featuresPath = `${directoryPath(config.paths.features)}/`;
|
|
7768
8248
|
const routeFiles = files.filter(
|
|
7769
|
-
(file) =>
|
|
8249
|
+
(file) =>
|
|
8250
|
+
file.startsWith(featuresPath) && isSourceFileNamed(file, "routes"),
|
|
7770
8251
|
);
|
|
7771
8252
|
const routeGroups: FeatureRouteGroup[] = [];
|
|
7772
8253
|
|
|
7773
8254
|
for (const file of routeFiles) {
|
|
7774
8255
|
const source = await readFile(path.join(targetDir, file), "utf8");
|
|
7775
|
-
const imports = parseNamedImports(source, config, file);
|
|
8256
|
+
const imports = parseNamedImports(source, config, file, files);
|
|
7776
8257
|
routeGroups.push(...parseFeatureRouteGroups(source, file, imports));
|
|
7777
8258
|
}
|
|
7778
8259
|
|
|
@@ -7935,8 +8416,7 @@ function findRouteGroupContract(
|
|
|
7935
8416
|
contract.exportName === routeGroupContract.exportName &&
|
|
7936
8417
|
(!routeGroupContract.file ||
|
|
7937
8418
|
contract.file === routeGroupContract.file ||
|
|
7938
|
-
contract.file ===
|
|
7939
|
-
routeGroupContract.file.replace(/\.ts$/, "/index.ts")),
|
|
8419
|
+
contract.file === directoryIndexSourceFile(routeGroupContract.file)),
|
|
7940
8420
|
);
|
|
7941
8421
|
}
|
|
7942
8422
|
|
|
@@ -9313,7 +9793,7 @@ async function readContractLists(
|
|
|
9313
9793
|
|
|
9314
9794
|
for (const file of contractFiles) {
|
|
9315
9795
|
const source = await readFile(path.join(targetDir, file), "utf8");
|
|
9316
|
-
const imports = parseNamedImports(source, config, file);
|
|
9796
|
+
const imports = parseNamedImports(source, config, file, files);
|
|
9317
9797
|
for (const list of parseContractLists(source, file, imports)) {
|
|
9318
9798
|
lists.set(contractListKey(list.file, list.name), list);
|
|
9319
9799
|
}
|
|
@@ -9330,18 +9810,18 @@ function contractSourceFiles(
|
|
|
9330
9810
|
if (usesFeatureOwnedContracts(config)) {
|
|
9331
9811
|
return files.filter(
|
|
9332
9812
|
(file) =>
|
|
9333
|
-
|
|
9334
|
-
|
|
9335
|
-
).test(
|
|
9813
|
+
isAnalysisSourceFile(file) &&
|
|
9814
|
+
!isAppTestFile(file) &&
|
|
9815
|
+
new RegExp(`^${escapeRegExp(contractsPath)}/[^/]+/contracts$`).test(
|
|
9816
|
+
stripSourceFileExtension(file),
|
|
9817
|
+
),
|
|
9336
9818
|
);
|
|
9337
9819
|
}
|
|
9338
9820
|
|
|
9339
|
-
return files.filter(
|
|
9340
|
-
(file)
|
|
9341
|
-
|
|
9342
|
-
|
|
9343
|
-
!file.endsWith(".test.ts"),
|
|
9344
|
-
);
|
|
9821
|
+
return files.filter((file) => {
|
|
9822
|
+
if (!isAnalysisSourceFile(file) || isAppTestFile(file)) return false;
|
|
9823
|
+
return stripSourceFileExtension(file).startsWith(`${contractsPath}/`);
|
|
9824
|
+
});
|
|
9345
9825
|
}
|
|
9346
9826
|
|
|
9347
9827
|
function contractListSourceFiles(
|
|
@@ -9351,22 +9831,23 @@ function contractListSourceFiles(
|
|
|
9351
9831
|
const contractsPath = directoryPath(config.paths.contracts);
|
|
9352
9832
|
if (usesFeatureOwnedContracts(config)) {
|
|
9353
9833
|
return files.filter((file) => {
|
|
9354
|
-
if (!file
|
|
9355
|
-
|
|
9356
|
-
if (
|
|
9834
|
+
if (!isAnalysisSourceFile(file) || isAppTestFile(file)) return false;
|
|
9835
|
+
const sourcePath = stripSourceFileExtension(file);
|
|
9836
|
+
if (sourcePath === `${contractsPath}/index`) return true;
|
|
9837
|
+
if (sourcePath === `${contractsPath}/contracts`) return true;
|
|
9357
9838
|
return new RegExp(
|
|
9358
|
-
`^${escapeRegExp(contractsPath)}/[^/]+/contracts
|
|
9359
|
-
).test(
|
|
9839
|
+
`^${escapeRegExp(contractsPath)}/[^/]+/contracts$`,
|
|
9840
|
+
).test(sourcePath);
|
|
9360
9841
|
});
|
|
9361
9842
|
}
|
|
9362
9843
|
|
|
9363
|
-
return files.filter(
|
|
9364
|
-
(file)
|
|
9365
|
-
|
|
9366
|
-
|
|
9367
|
-
|
|
9368
|
-
|
|
9369
|
-
);
|
|
9844
|
+
return files.filter((file) => {
|
|
9845
|
+
if (!isAnalysisSourceFile(file) || isAppTestFile(file)) return false;
|
|
9846
|
+
const sourcePath = stripSourceFileExtension(file);
|
|
9847
|
+
return (
|
|
9848
|
+
sourcePath === contractsPath || sourcePath.startsWith(`${contractsPath}/`)
|
|
9849
|
+
);
|
|
9850
|
+
});
|
|
9370
9851
|
}
|
|
9371
9852
|
|
|
9372
9853
|
function parseContractLists(
|
|
@@ -9420,7 +9901,7 @@ function resolveContractList(
|
|
|
9420
9901
|
if (seen.has(key)) return new Set();
|
|
9421
9902
|
const list =
|
|
9422
9903
|
lists.get(key) ??
|
|
9423
|
-
lists.get(contractListKey(file
|
|
9904
|
+
lists.get(contractListKey(directoryIndexSourceFile(file), name));
|
|
9424
9905
|
if (!list) return undefined;
|
|
9425
9906
|
|
|
9426
9907
|
seen.add(key);
|
|
@@ -10010,7 +10491,7 @@ function nextRoutePath(file: string, routeRoot: string): string {
|
|
|
10010
10491
|
const normalizedRouteRoot = directoryPath(routeRoot);
|
|
10011
10492
|
const routeSegments = file
|
|
10012
10493
|
.slice(normalizedRouteRoot.length + 1)
|
|
10013
|
-
.replace(/\/route\.ts$/, "")
|
|
10494
|
+
.replace(/\/route\.(?:js|jsx|ts|tsx)$/, "")
|
|
10014
10495
|
.split("/")
|
|
10015
10496
|
.filter(Boolean);
|
|
10016
10497
|
const apiPath = routeSegments.map(nextSegmentToContractSegment).join("/");
|