@beignet/cli 0.0.1 → 0.0.3
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 +26 -0
- package/README.md +144 -55
- package/dist/config.d.ts +31 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +18 -0
- package/dist/config.js.map +1 -1
- package/dist/create.d.ts +9 -0
- package/dist/create.d.ts.map +1 -1
- package/dist/create.js +5 -0
- package/dist/create.js.map +1 -1
- package/dist/db.d.ts +36 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +111 -0
- package/dist/db.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +243 -2
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts +24 -0
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +288 -4
- package/dist/inspect.js.map +1 -1
- package/dist/lint.d.ts +12 -0
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +30 -5
- package/dist/lint.js.map +1 -1
- package/dist/make.d.ts +91 -0
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +602 -72
- package/dist/make.js.map +1 -1
- package/dist/templates.d.ts +33 -0
- package/dist/templates.d.ts.map +1 -1
- package/dist/templates.js +1396 -112
- package/dist/templates.js.map +1 -1
- package/package.json +1 -1
- package/src/config.ts +31 -0
- package/src/create.ts +11 -0
- package/src/db.ts +166 -0
- package/src/index.ts +320 -0
- package/src/inspect.ts +497 -3
- package/src/lint.ts +43 -9
- package/src/make.ts +917 -80
- package/src/templates.ts +1425 -111
package/src/inspect.ts
CHANGED
|
@@ -24,6 +24,9 @@ type HttpMethod =
|
|
|
24
24
|
| "HEAD"
|
|
25
25
|
| "OPTIONS";
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Contract discovered by app inspection.
|
|
29
|
+
*/
|
|
27
30
|
export type InspectedContract = {
|
|
28
31
|
exportName: string;
|
|
29
32
|
file: string;
|
|
@@ -31,12 +34,18 @@ export type InspectedContract = {
|
|
|
31
34
|
path: string;
|
|
32
35
|
};
|
|
33
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Route handler mapping discovered by app inspection.
|
|
39
|
+
*/
|
|
34
40
|
export type InspectedRoute = InspectedContract & {
|
|
35
41
|
handlerFile?: string;
|
|
36
42
|
handlerExport?: HttpMethod;
|
|
37
43
|
handlerSource: "next-route" | "route-local" | "missing";
|
|
38
44
|
};
|
|
39
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Doctor diagnostic produced by app inspection.
|
|
48
|
+
*/
|
|
40
49
|
export type InspectDiagnostic = {
|
|
41
50
|
severity: "error" | "warning";
|
|
42
51
|
code: string;
|
|
@@ -45,12 +54,18 @@ export type InspectDiagnostic = {
|
|
|
45
54
|
contract?: string;
|
|
46
55
|
};
|
|
47
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Automatic doctor fix that was applied.
|
|
59
|
+
*/
|
|
48
60
|
export type InspectFix = {
|
|
49
61
|
code: string;
|
|
50
62
|
message: string;
|
|
51
63
|
file: string;
|
|
52
64
|
};
|
|
53
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Detected app convention and missing convention files.
|
|
68
|
+
*/
|
|
54
69
|
export type InspectConvention = {
|
|
55
70
|
kind: "standard" | "next" | "custom";
|
|
56
71
|
nextLayout: boolean;
|
|
@@ -60,6 +75,9 @@ export type InspectConvention = {
|
|
|
60
75
|
missingResourceGenerator: string[];
|
|
61
76
|
};
|
|
62
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Full app inspection result.
|
|
80
|
+
*/
|
|
63
81
|
export type InspectAppResult = {
|
|
64
82
|
targetDir: string;
|
|
65
83
|
config: ResolvedBeignetConfig;
|
|
@@ -176,6 +194,9 @@ export async function applyDoctorFixes(
|
|
|
176
194
|
return fixes;
|
|
177
195
|
}
|
|
178
196
|
|
|
197
|
+
/**
|
|
198
|
+
* Format inspected routes as a CLI table.
|
|
199
|
+
*/
|
|
179
200
|
export function formatRoutes(result: InspectAppResult): string {
|
|
180
201
|
if (result.routes.length === 0) {
|
|
181
202
|
return `No Beignet routes found in ${result.targetDir}`;
|
|
@@ -197,6 +218,9 @@ export function formatRoutes(result: InspectAppResult): string {
|
|
|
197
218
|
]);
|
|
198
219
|
}
|
|
199
220
|
|
|
221
|
+
/**
|
|
222
|
+
* Format doctor diagnostics and applied fixes for CLI output.
|
|
223
|
+
*/
|
|
200
224
|
export function formatDoctor(result: InspectAppResult): string {
|
|
201
225
|
const fixLines =
|
|
202
226
|
result.fixes.length === 0
|
|
@@ -797,6 +821,20 @@ async function inspectDiagnostics(
|
|
|
797
821
|
contracts,
|
|
798
822
|
)),
|
|
799
823
|
...(await inspectPortProviderDrift(targetDir, files, config, convention)),
|
|
824
|
+
...(await inspectDatabaseLifecycleDrift(
|
|
825
|
+
targetDir,
|
|
826
|
+
files,
|
|
827
|
+
config,
|
|
828
|
+
convention,
|
|
829
|
+
strict,
|
|
830
|
+
)),
|
|
831
|
+
...(await inspectServerlessFootguns(targetDir, files)),
|
|
832
|
+
...(await inspectFeatureArtifactDrift(
|
|
833
|
+
targetDir,
|
|
834
|
+
files,
|
|
835
|
+
config,
|
|
836
|
+
convention,
|
|
837
|
+
)),
|
|
800
838
|
...(await inspectResourceSlices(
|
|
801
839
|
targetDir,
|
|
802
840
|
files,
|
|
@@ -809,6 +847,284 @@ async function inspectDiagnostics(
|
|
|
809
847
|
return dedupeDiagnostics(diagnostics);
|
|
810
848
|
}
|
|
811
849
|
|
|
850
|
+
async function inspectFeatureArtifactDrift(
|
|
851
|
+
targetDir: string,
|
|
852
|
+
files: string[],
|
|
853
|
+
config: ResolvedBeignetConfig,
|
|
854
|
+
convention: InspectConvention,
|
|
855
|
+
): Promise<InspectDiagnostic[]> {
|
|
856
|
+
if (!convention.resourceGenerator) return [];
|
|
857
|
+
|
|
858
|
+
const diagnostics: InspectDiagnostic[] = [];
|
|
859
|
+
const featuresPath = directoryPath(config.paths.features);
|
|
860
|
+
const uploadDefinitions = files.filter((file) =>
|
|
861
|
+
isFeatureArtifactFile(file, featuresPath, "uploads"),
|
|
862
|
+
);
|
|
863
|
+
|
|
864
|
+
if (
|
|
865
|
+
uploadDefinitions.length > 0 &&
|
|
866
|
+
!(await appHasUploadRoute(targetDir, files, config))
|
|
867
|
+
) {
|
|
868
|
+
diagnostics.push({
|
|
869
|
+
severity: "warning",
|
|
870
|
+
code: "CK_UPLOAD_ROUTE_MISSING",
|
|
871
|
+
file: uploadDefinitions[0],
|
|
872
|
+
message:
|
|
873
|
+
"This app defines feature uploads but no known upload route. Register the upload registry with createUploadRouter(...) and expose it through createUploadRoute(...) so browser uploads can reach the workflow.",
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
for (const file of files) {
|
|
878
|
+
if (
|
|
879
|
+
!file.endsWith(".ts") ||
|
|
880
|
+
file.endsWith(".test.ts") ||
|
|
881
|
+
file.endsWith(".d.ts")
|
|
882
|
+
) {
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
const misplaced = misplacedFeatureArtifactFolder(file, featuresPath);
|
|
887
|
+
if (misplaced) {
|
|
888
|
+
diagnostics.push({
|
|
889
|
+
severity: "warning",
|
|
890
|
+
code: "CK_FEATURE_ARTIFACT_FOLDER",
|
|
891
|
+
file,
|
|
892
|
+
message: `${file} uses ${misplaced.actual}, but Beignet expects ${misplaced.expected}. Keep feature-owned workflow artifacts in the canonical feature folders so CLI generators, docs, and architecture lint stay aligned.`,
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
if (
|
|
897
|
+
isNotificationDefinitionFile(file, featuresPath) ||
|
|
898
|
+
isInfraOrServerFile(file, config)
|
|
899
|
+
) {
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
const source = await readFile(path.join(targetDir, file), "utf8");
|
|
903
|
+
if (containsCallExpression(source, "createInlineNotificationDispatcher")) {
|
|
904
|
+
diagnostics.push({
|
|
905
|
+
severity: "warning",
|
|
906
|
+
code: "CK_NOTIFICATION_PORT_BYPASS",
|
|
907
|
+
file,
|
|
908
|
+
message: `${file} creates a notification dispatcher directly. Application workflows should send notifications through ctx.ports.notifications so delivery can be swapped, tested, instrumented, and run through jobs/outbox consistently.`,
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
return diagnostics;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function isFeatureArtifactFile(
|
|
917
|
+
file: string,
|
|
918
|
+
featuresPath: string,
|
|
919
|
+
folder: "uploads" | "notifications",
|
|
920
|
+
): boolean {
|
|
921
|
+
const match = new RegExp(
|
|
922
|
+
`^${escapeRegExp(featuresPath)}/[^/]+/${folder}/([^/]+)\\.ts$`,
|
|
923
|
+
).exec(file);
|
|
924
|
+
return Boolean(match && match[1] !== "index");
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
async function appHasUploadRoute(
|
|
928
|
+
targetDir: string,
|
|
929
|
+
files: string[],
|
|
930
|
+
config: ResolvedBeignetConfig,
|
|
931
|
+
): Promise<boolean> {
|
|
932
|
+
for (const file of files) {
|
|
933
|
+
if (!file.endsWith(".ts") || file.endsWith(".test.ts")) continue;
|
|
934
|
+
if (!isRouteServerOrInfraFile(file, config)) continue;
|
|
935
|
+
|
|
936
|
+
const source = await readFile(path.join(targetDir, file), "utf8");
|
|
937
|
+
if (
|
|
938
|
+
containsCallExpression(source, "createUploadRoute") ||
|
|
939
|
+
containsCallExpression(source, "createUploadRouter")
|
|
940
|
+
) {
|
|
941
|
+
return true;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
return false;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function misplacedFeatureArtifactFolder(
|
|
949
|
+
file: string,
|
|
950
|
+
featuresPath: string,
|
|
951
|
+
): { actual: string; expected: string } | undefined {
|
|
952
|
+
const match = new RegExp(
|
|
953
|
+
`^${escapeRegExp(featuresPath)}/([^/]+)/([^/]+)/`,
|
|
954
|
+
).exec(file);
|
|
955
|
+
if (!match) return undefined;
|
|
956
|
+
|
|
957
|
+
const folder = match[2];
|
|
958
|
+
const mappings: Record<string, string> = {
|
|
959
|
+
event: "domain/events/",
|
|
960
|
+
events: "domain/events/",
|
|
961
|
+
job: "jobs/",
|
|
962
|
+
listener: "listeners/",
|
|
963
|
+
notification: "notifications/",
|
|
964
|
+
schedule: "schedules/",
|
|
965
|
+
scheduled: "schedules/",
|
|
966
|
+
upload: "uploads/",
|
|
967
|
+
};
|
|
968
|
+
|
|
969
|
+
const expected = mappings[folder];
|
|
970
|
+
if (!expected) return undefined;
|
|
971
|
+
|
|
972
|
+
return { actual: `${folder}/`, expected };
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
function isNotificationDefinitionFile(
|
|
976
|
+
file: string,
|
|
977
|
+
featuresPath: string,
|
|
978
|
+
): boolean {
|
|
979
|
+
return isFeatureArtifactFile(file, featuresPath, "notifications");
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function isRouteServerOrInfraFile(
|
|
983
|
+
file: string,
|
|
984
|
+
config: ResolvedBeignetConfig,
|
|
985
|
+
): boolean {
|
|
986
|
+
return (
|
|
987
|
+
file.startsWith(`${directoryPath(config.paths.routes)}/`) ||
|
|
988
|
+
isInfraOrServerFile(file, config)
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
function isInfraOrServerFile(
|
|
993
|
+
file: string,
|
|
994
|
+
config: ResolvedBeignetConfig,
|
|
995
|
+
): boolean {
|
|
996
|
+
const serverDir = directoryPath(path.dirname(config.paths.server));
|
|
997
|
+
const infrastructureDir = directoryPath(
|
|
998
|
+
path.dirname(config.paths.infrastructurePorts),
|
|
999
|
+
);
|
|
1000
|
+
|
|
1001
|
+
return (
|
|
1002
|
+
file === config.paths.server ||
|
|
1003
|
+
file.startsWith(`${serverDir}/`) ||
|
|
1004
|
+
file === config.paths.infrastructurePorts ||
|
|
1005
|
+
file.startsWith(`${infrastructureDir}/`)
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function containsCallExpression(source: string, name: string): boolean {
|
|
1010
|
+
return new RegExp(
|
|
1011
|
+
`\\b${escapeRegExp(name)}\\s*(?:<[^;=(){}]*>\\s*)?\\(`,
|
|
1012
|
+
).test(source);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
async function inspectServerlessFootguns(
|
|
1016
|
+
targetDir: string,
|
|
1017
|
+
files: string[],
|
|
1018
|
+
): Promise<InspectDiagnostic[]> {
|
|
1019
|
+
const diagnostics: InspectDiagnostic[] = [];
|
|
1020
|
+
|
|
1021
|
+
for (const file of files) {
|
|
1022
|
+
if (
|
|
1023
|
+
!file.endsWith(".ts") ||
|
|
1024
|
+
file.endsWith(".test.ts") ||
|
|
1025
|
+
file.endsWith(".d.ts")
|
|
1026
|
+
) {
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
const isInfraOrServerFile =
|
|
1031
|
+
file.startsWith("infra/") || file.startsWith("server/");
|
|
1032
|
+
const isProviderFile =
|
|
1033
|
+
/(^|\/)provider(s)?\.ts$/.test(file) ||
|
|
1034
|
+
/(^|\/)[^/]*provider[^/]*\.ts$/.test(file);
|
|
1035
|
+
const source = await readFile(path.join(targetDir, file), "utf8");
|
|
1036
|
+
|
|
1037
|
+
if (
|
|
1038
|
+
isInfraOrServerFile &&
|
|
1039
|
+
isProviderFile &&
|
|
1040
|
+
/\bset(?:Interval|Timeout)\s*\(/.test(source)
|
|
1041
|
+
) {
|
|
1042
|
+
diagnostics.push({
|
|
1043
|
+
severity: "warning",
|
|
1044
|
+
code: "CK_PROVIDER_BACKGROUND_WORK",
|
|
1045
|
+
file,
|
|
1046
|
+
message: `${file} starts timer-based work from provider/server setup. Serverless apps should expose bounded runtime entrypoints such as cron routes, scheduled handlers, job functions, or worker processes instead of provider polling loops.`,
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
if (
|
|
1051
|
+
isInfraOrServerFile &&
|
|
1052
|
+
isProviderFile &&
|
|
1053
|
+
/\bcreateProvider\s*\(/.test(source) &&
|
|
1054
|
+
/\bdrainOutbox\s*\(/.test(source)
|
|
1055
|
+
) {
|
|
1056
|
+
diagnostics.push({
|
|
1057
|
+
severity: "warning",
|
|
1058
|
+
code: "CK_PROVIDER_OUTBOX_DRAIN",
|
|
1059
|
+
file,
|
|
1060
|
+
message: `${file} drains the outbox from provider lifecycle code. Move outbox delivery to createOutboxDrainRoute(...) or another bounded runtime entrypoint so imports and serverless cold starts do not run background work.`,
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
if (
|
|
1065
|
+
file.startsWith("app/api/cron/") &&
|
|
1066
|
+
file.endsWith("/route.ts") &&
|
|
1067
|
+
!/\bauthorization\b/i.test(source) &&
|
|
1068
|
+
!/\bcreateOutboxDrainRoute\s*\(/.test(source)
|
|
1069
|
+
) {
|
|
1070
|
+
diagnostics.push({
|
|
1071
|
+
severity: "warning",
|
|
1072
|
+
code: "CK_CRON_ROUTE_AUTH_MISSING",
|
|
1073
|
+
file,
|
|
1074
|
+
message: `${file} looks like a cron route without an authorization check. Require a shared secret or use a framework route helper such as createOutboxDrainRoute(...).`,
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
if (
|
|
1080
|
+
hasOutboxRegistry(files) &&
|
|
1081
|
+
!(await appHasOutboxDrainEntrypoint(targetDir, files))
|
|
1082
|
+
) {
|
|
1083
|
+
diagnostics.push({
|
|
1084
|
+
severity: "warning",
|
|
1085
|
+
code: "CK_OUTBOX_DRAIN_MISSING",
|
|
1086
|
+
message:
|
|
1087
|
+
"This app defines an outbox registry but no known drain entrypoint. Add a cron/schedule/job route with createOutboxDrainRoute(...) so durable events and jobs are delivered in serverless deployments.",
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
return diagnostics;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
function hasOutboxRegistry(files: string[]): boolean {
|
|
1095
|
+
return files.some(isOutboxRegistryFile);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function isOutboxRegistryFile(file: string): boolean {
|
|
1099
|
+
return file === "server/outbox.ts";
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
async function appHasOutboxDrainEntrypoint(
|
|
1103
|
+
targetDir: string,
|
|
1104
|
+
files: string[],
|
|
1105
|
+
): Promise<boolean> {
|
|
1106
|
+
for (const file of files) {
|
|
1107
|
+
if (!file.endsWith(".ts") || file.endsWith(".test.ts")) continue;
|
|
1108
|
+
if (
|
|
1109
|
+
!file.startsWith("app/api/") &&
|
|
1110
|
+
!file.startsWith("server/") &&
|
|
1111
|
+
!file.startsWith("infra/")
|
|
1112
|
+
) {
|
|
1113
|
+
continue;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
const source = await readFile(path.join(targetDir, file), "utf8");
|
|
1117
|
+
if (
|
|
1118
|
+
/\bcreateOutboxDrainRoute\s*\(/.test(source) ||
|
|
1119
|
+
/\bdrainOutbox\s*\(/.test(source)
|
|
1120
|
+
) {
|
|
1121
|
+
return true;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
return false;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
812
1128
|
async function inspectPortProviderDrift(
|
|
813
1129
|
targetDir: string,
|
|
814
1130
|
files: string[],
|
|
@@ -875,6 +1191,178 @@ async function inspectPortProviderDrift(
|
|
|
875
1191
|
return diagnostics;
|
|
876
1192
|
}
|
|
877
1193
|
|
|
1194
|
+
async function inspectDatabaseLifecycleDrift(
|
|
1195
|
+
targetDir: string,
|
|
1196
|
+
files: string[],
|
|
1197
|
+
config: ResolvedBeignetConfig,
|
|
1198
|
+
convention: InspectConvention,
|
|
1199
|
+
strict: boolean,
|
|
1200
|
+
): Promise<InspectDiagnostic[]> {
|
|
1201
|
+
if (!convention.resourceGenerator) return [];
|
|
1202
|
+
|
|
1203
|
+
const diagnostics: InspectDiagnostic[] = [];
|
|
1204
|
+
const resources = await collectResourceNames(targetDir, files, config);
|
|
1205
|
+
const infrastructurePath = directoryPath(
|
|
1206
|
+
path.dirname(config.paths.infrastructurePorts),
|
|
1207
|
+
);
|
|
1208
|
+
const featuresPath = directoryPath(config.paths.features);
|
|
1209
|
+
const repositoriesPath = path.join(
|
|
1210
|
+
infrastructurePath,
|
|
1211
|
+
"db",
|
|
1212
|
+
"repositories.ts",
|
|
1213
|
+
);
|
|
1214
|
+
const repositoriesSource = files.includes(repositoriesPath)
|
|
1215
|
+
? await readFile(path.join(targetDir, repositoriesPath), "utf8")
|
|
1216
|
+
: "";
|
|
1217
|
+
|
|
1218
|
+
for (const resource of resources) {
|
|
1219
|
+
const singular = singularize(resource);
|
|
1220
|
+
const pascal = pascalCase(singular);
|
|
1221
|
+
const pluralCamel = camelCase(resource);
|
|
1222
|
+
const infrastructureDir = `${infrastructurePath}/${resource}/`;
|
|
1223
|
+
const hasRepositoryAdapter = hasRepositoryAdapterFile(
|
|
1224
|
+
files,
|
|
1225
|
+
infrastructureDir,
|
|
1226
|
+
);
|
|
1227
|
+
const drizzleRepositoryAdapter = drizzleRepositoryAdapterFile(
|
|
1228
|
+
files,
|
|
1229
|
+
infrastructureDir,
|
|
1230
|
+
singular,
|
|
1231
|
+
);
|
|
1232
|
+
|
|
1233
|
+
const portFile = resourcePortFile(resource, singular, config);
|
|
1234
|
+
|
|
1235
|
+
if (files.includes(portFile) && !hasRepositoryAdapter) {
|
|
1236
|
+
diagnostics.push({
|
|
1237
|
+
severity: "warning",
|
|
1238
|
+
code: "CK_REPOSITORY_ADAPTER_MISSING",
|
|
1239
|
+
file: portFile,
|
|
1240
|
+
message: `${portFile} declares a repository port, but ${infrastructureDir} does not contain a repository adapter. Add an infra adapter such as drizzle-${singular}-repository.ts and wire it through app ports.`,
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
if (drizzleRepositoryAdapter) {
|
|
1245
|
+
const factoryName = `createDrizzle${pascal}Repository`;
|
|
1246
|
+
if (
|
|
1247
|
+
repositoriesSource &&
|
|
1248
|
+
(!repositoriesSource.includes(factoryName) ||
|
|
1249
|
+
!new RegExp(`\\b${pluralCamel}\\s*:`).test(repositoriesSource))
|
|
1250
|
+
) {
|
|
1251
|
+
diagnostics.push({
|
|
1252
|
+
severity: "warning",
|
|
1253
|
+
code: "CK_DRIZZLE_REPOSITORY_UNWIRED",
|
|
1254
|
+
file: repositoriesPath,
|
|
1255
|
+
message: `${drizzleRepositoryAdapter} exists, but ${repositoriesPath} does not appear to import and return ${factoryName}(db). Wire the repository through createRepositories(...) so normal and transaction-scoped ports stay aligned.`,
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
if (
|
|
1261
|
+
strict &&
|
|
1262
|
+
hasFeatureSeeds(files, featuresPath, resource) &&
|
|
1263
|
+
!hasFeatureFactories(files, featuresPath, resource)
|
|
1264
|
+
) {
|
|
1265
|
+
diagnostics.push({
|
|
1266
|
+
severity: "warning",
|
|
1267
|
+
code: "CK_FACTORY_MISSING",
|
|
1268
|
+
file: `${featuresPath}/${resource}/seeds/`,
|
|
1269
|
+
message: `${resource} defines feature seeds but no feature factory. Add a factory under ${featuresPath}/${resource}/tests/factories/ so seeds and tests share the same app-owned data setup path.`,
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
if (
|
|
1275
|
+
hasAnyFeatureSeeds(files, featuresPath) &&
|
|
1276
|
+
!(await packageScriptExists(targetDir, files, "db:seed"))
|
|
1277
|
+
) {
|
|
1278
|
+
diagnostics.push({
|
|
1279
|
+
severity: "warning",
|
|
1280
|
+
code: "CK_DB_SEED_SCRIPT_MISSING",
|
|
1281
|
+
file: "package.json",
|
|
1282
|
+
message:
|
|
1283
|
+
'This app defines feature seeds, but package.json does not define "db:seed". Add an app-owned db:seed script so beignet db seed can run the seed entrypoint.',
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
return diagnostics;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
function hasRepositoryAdapterFile(
|
|
1291
|
+
files: string[],
|
|
1292
|
+
infrastructureDir: string,
|
|
1293
|
+
): boolean {
|
|
1294
|
+
return files.some(
|
|
1295
|
+
(file) =>
|
|
1296
|
+
file.startsWith(infrastructureDir) &&
|
|
1297
|
+
file.endsWith("-repository.ts") &&
|
|
1298
|
+
!file.endsWith(".test.ts"),
|
|
1299
|
+
);
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
function drizzleRepositoryAdapterFile(
|
|
1303
|
+
files: string[],
|
|
1304
|
+
infrastructureDir: string,
|
|
1305
|
+
singular: string,
|
|
1306
|
+
): string | undefined {
|
|
1307
|
+
return files.find(
|
|
1308
|
+
(file) =>
|
|
1309
|
+
file.startsWith(infrastructureDir) &&
|
|
1310
|
+
file.endsWith(`/drizzle-${singular}-repository.ts`),
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
function hasFeatureSeeds(
|
|
1315
|
+
files: string[],
|
|
1316
|
+
featuresPath: string,
|
|
1317
|
+
resource: string,
|
|
1318
|
+
): boolean {
|
|
1319
|
+
return files.some(
|
|
1320
|
+
(file) =>
|
|
1321
|
+
file.startsWith(`${featuresPath}/${resource}/seeds/`) &&
|
|
1322
|
+
file.endsWith(".ts") &&
|
|
1323
|
+
!file.endsWith("/index.ts"),
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
function hasAnyFeatureSeeds(files: string[], featuresPath: string): boolean {
|
|
1328
|
+
return files.some(
|
|
1329
|
+
(file) =>
|
|
1330
|
+
new RegExp(`^${escapeRegExp(featuresPath)}/[^/]+/seeds/`).test(file) &&
|
|
1331
|
+
file.endsWith(".ts") &&
|
|
1332
|
+
!file.endsWith("/index.ts"),
|
|
1333
|
+
);
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
function hasFeatureFactories(
|
|
1337
|
+
files: string[],
|
|
1338
|
+
featuresPath: string,
|
|
1339
|
+
resource: string,
|
|
1340
|
+
): boolean {
|
|
1341
|
+
return files.some(
|
|
1342
|
+
(file) =>
|
|
1343
|
+
file === `${featuresPath}/${resource}/tests/factories.ts` ||
|
|
1344
|
+
(file.startsWith(`${featuresPath}/${resource}/tests/factories/`) &&
|
|
1345
|
+
file.endsWith(".ts") &&
|
|
1346
|
+
!file.endsWith("/index.ts")),
|
|
1347
|
+
);
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
async function packageScriptExists(
|
|
1351
|
+
targetDir: string,
|
|
1352
|
+
files: string[],
|
|
1353
|
+
script: string,
|
|
1354
|
+
): Promise<boolean> {
|
|
1355
|
+
if (!files.includes("package.json")) return false;
|
|
1356
|
+
|
|
1357
|
+
const packageJson = JSON.parse(
|
|
1358
|
+
await readFile(path.join(targetDir, "package.json"), "utf8"),
|
|
1359
|
+
) as {
|
|
1360
|
+
scripts?: Record<string, string>;
|
|
1361
|
+
};
|
|
1362
|
+
|
|
1363
|
+
return Boolean(packageJson.scripts?.[script]);
|
|
1364
|
+
}
|
|
1365
|
+
|
|
878
1366
|
const canonicalProviderPorts = [
|
|
879
1367
|
{
|
|
880
1368
|
packageName: "@beignet/provider-auth-better-auth",
|
|
@@ -1128,7 +1616,7 @@ function parseFeatureRouteGroups(
|
|
|
1128
1616
|
): FeatureRouteGroup[] {
|
|
1129
1617
|
const routeGroups: FeatureRouteGroup[] = [];
|
|
1130
1618
|
const exportRegex =
|
|
1131
|
-
/(?:^|\n)export const\s+([A-Za-z_$][\w$]*)\s*=\s*defineRouteGroup(?:<[^>]+>)?\(\s*(?:\{[\s\S]*?routes:\s*)?\[([\s\S]*?)\]\s*(?:,?\s*\})?\s*\)\s*;?/g;
|
|
1619
|
+
/(?:^|\n)export const\s+([A-Za-z_$][\w$]*)\s*=\s*defineRouteGroup(?:<[^>]+>)?\(\s*(?:\)\s*\(\s*)?(?:\{[\s\S]*?routes:\s*)?\[([\s\S]*?)\]\s*(?:,?\s*\})?\s*\)\s*;?/g;
|
|
1132
1620
|
|
|
1133
1621
|
for (const match of source.matchAll(exportRegex)) {
|
|
1134
1622
|
const contracts = [...match[2].matchAll(/contract:\s*([^,\n}]+)/g)]
|
|
@@ -1745,9 +2233,10 @@ async function isGeneratedResourcePort(
|
|
|
1745
2233
|
|
|
1746
2234
|
return (
|
|
1747
2235
|
source.includes(`Create${singularPascal}Input`) &&
|
|
1748
|
-
source.includes(`List${pluralPascal}Input`) &&
|
|
1749
2236
|
source.includes(`List${pluralPascal}Result`) &&
|
|
2237
|
+
source.includes("OffsetPage") &&
|
|
1750
2238
|
source.includes(`interface ${singularPascal}Repository`) &&
|
|
2239
|
+
source.includes("list(page: OffsetPage)") &&
|
|
1751
2240
|
source.includes(`create(input: Create${singularPascal}Input)`)
|
|
1752
2241
|
);
|
|
1753
2242
|
}
|
|
@@ -1853,7 +2342,7 @@ function resourceDiagnostic(
|
|
|
1853
2342
|
severity: "error",
|
|
1854
2343
|
code,
|
|
1855
2344
|
file,
|
|
1856
|
-
message: `${resource} appears to be a generated
|
|
2345
|
+
message: `${resource} appears to be a generated feature slice, but ${file} is missing. Re-run make feature ${resource}, restore the missing file, or remove the partial feature wiring.`,
|
|
1857
2346
|
};
|
|
1858
2347
|
}
|
|
1859
2348
|
|
|
@@ -1893,6 +2382,11 @@ function pascalCase(value: string): string {
|
|
|
1893
2382
|
.join("");
|
|
1894
2383
|
}
|
|
1895
2384
|
|
|
2385
|
+
function camelCase(value: string): string {
|
|
2386
|
+
const pascal = pascalCase(value);
|
|
2387
|
+
return `${pascal.charAt(0).toLowerCase()}${pascal.slice(1)}`;
|
|
2388
|
+
}
|
|
2389
|
+
|
|
1896
2390
|
function singularize(value: string): string {
|
|
1897
2391
|
if (value.endsWith("ies") && value.length > 3) {
|
|
1898
2392
|
return `${value.slice(0, -3)}y`;
|