@beignet/cli 0.0.48 → 0.0.50
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 +54 -0
- package/README.md +117 -22
- package/dist/analysis/contract-factories.d.ts +17 -0
- package/dist/analysis/contract-factories.d.ts.map +1 -0
- package/dist/analysis/contract-factories.js +176 -0
- package/dist/analysis/contract-factories.js.map +1 -0
- package/dist/analysis/layers.d.ts +5 -0
- package/dist/analysis/layers.d.ts.map +1 -0
- package/dist/analysis/layers.js +172 -0
- package/dist/analysis/layers.js.map +1 -0
- package/dist/analysis/port-wiring.d.ts +23 -0
- package/dist/analysis/port-wiring.d.ts.map +1 -0
- package/dist/analysis/port-wiring.js +379 -0
- package/dist/analysis/port-wiring.js.map +1 -0
- package/dist/analysis/source-index.d.ts +5 -0
- package/dist/analysis/source-index.d.ts.map +1 -1
- package/dist/analysis/source-index.js +171 -42
- package/dist/analysis/source-index.js.map +1 -1
- package/dist/analysis/workspace.d.ts +11 -4
- package/dist/analysis/workspace.d.ts.map +1 -1
- package/dist/analysis/workspace.js +78 -8
- package/dist/analysis/workspace.js.map +1 -1
- package/dist/app-map-changes.d.ts +103 -0
- package/dist/app-map-changes.d.ts.map +1 -0
- package/dist/app-map-changes.js +949 -0
- package/dist/app-map-changes.js.map +1 -0
- package/dist/app-map.d.ts.map +1 -1
- package/dist/app-map.js +311 -92
- package/dist/app-map.js.map +1 -1
- package/dist/check.js +37 -3
- package/dist/check.js.map +1 -1
- package/dist/explain.js +4 -4
- package/dist/explain.js.map +1 -1
- package/dist/git-changes.d.ts +30 -0
- package/dist/git-changes.d.ts.map +1 -0
- package/dist/git-changes.js +367 -0
- package/dist/git-changes.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -1
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +706 -186
- package/dist/inspect.js.map +1 -1
- package/dist/lib.d.ts +3 -0
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +1 -0
- package/dist/lib.js.map +1 -1
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +309 -487
- package/dist/lint.js.map +1 -1
- package/dist/make/inbox.d.ts.map +1 -1
- package/dist/make/inbox.js +11 -4
- package/dist/make/inbox.js.map +1 -1
- package/dist/make/payments.js +2 -2
- package/dist/make/shared.d.ts.map +1 -1
- package/dist/make/shared.js +31 -27
- package/dist/make/shared.js.map +1 -1
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +144 -8
- package/dist/make.js.map +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +43 -21
- package/dist/mcp.js.map +1 -1
- package/dist/provider-add.d.ts.map +1 -1
- package/dist/provider-add.js +142 -7
- package/dist/provider-add.js.map +1 -1
- package/dist/registry-edits.d.ts +7 -0
- package/dist/registry-edits.d.ts.map +1 -1
- package/dist/registry-edits.js +237 -53
- package/dist/registry-edits.js.map +1 -1
- package/dist/templates/agents.d.ts.map +1 -1
- package/dist/templates/agents.js +37 -1
- package/dist/templates/agents.js.map +1 -1
- package/dist/templates/base.d.ts.map +1 -1
- package/dist/templates/base.js +13 -2
- package/dist/templates/base.js.map +1 -1
- package/dist/templates/server.d.ts.map +1 -1
- package/dist/templates/server.js +2 -0
- package/dist/templates/server.js.map +1 -1
- package/dist/templates/shared.d.ts +1 -0
- package/dist/templates/shared.d.ts.map +1 -1
- package/dist/templates/shared.js +3 -2
- package/dist/templates/shared.js.map +1 -1
- package/package.json +7 -6
- package/skills/app-structure/SKILL.md +71 -7
- package/src/analysis/contract-factories.ts +256 -0
- package/src/analysis/layers.ts +266 -0
- package/src/analysis/port-wiring.ts +506 -0
- package/src/analysis/source-index.ts +225 -57
- package/src/analysis/workspace.ts +134 -9
- package/src/app-map-changes.ts +1462 -0
- package/src/app-map.ts +418 -109
- package/src/check.ts +51 -3
- package/src/explain.ts +4 -4
- package/src/git-changes.ts +511 -0
- package/src/index.ts +39 -1
- package/src/inspect.ts +1005 -238
- package/src/lib.ts +21 -0
- package/src/lint.ts +393 -643
- package/src/make/inbox.ts +11 -4
- package/src/make/payments.ts +2 -2
- package/src/make/shared.ts +31 -27
- package/src/make.ts +225 -8
- package/src/mcp.ts +51 -24
- package/src/provider-add.ts +186 -7
- package/src/registry-edits.ts +313 -63
- package/src/templates/agents.ts +37 -1
- package/src/templates/base.ts +13 -2
- package/src/templates/server.ts +2 -0
- package/src/templates/shared.ts +3 -2
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
|
|
3
|
+
const contractFactoryModule = "@beignet/core/contracts";
|
|
4
|
+
|
|
5
|
+
export type ContractFactoryName = "defineContract" | "defineContractGroup";
|
|
6
|
+
|
|
7
|
+
export type ContractFactoryBindings = {
|
|
8
|
+
localNames: Record<ContractFactoryName, Set<string>>;
|
|
9
|
+
namespaceNames: Set<string>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type ContractBindingNames = {
|
|
13
|
+
contracts: Set<string>;
|
|
14
|
+
groups: Set<string>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type ContractCall = {
|
|
18
|
+
name: string;
|
|
19
|
+
node: ts.CallExpression;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type ContractCallChain = {
|
|
23
|
+
baseIdentifier?: string;
|
|
24
|
+
rootCall?: ContractCall;
|
|
25
|
+
calls: ContractCall[];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const contractGroupMethods = new Set([
|
|
29
|
+
"defineContractGroup",
|
|
30
|
+
"deprecated",
|
|
31
|
+
"errors",
|
|
32
|
+
"headers",
|
|
33
|
+
"meta",
|
|
34
|
+
"namespace",
|
|
35
|
+
"prefix",
|
|
36
|
+
"responses",
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const httpContractMethods = new Set([
|
|
40
|
+
"delete",
|
|
41
|
+
"get",
|
|
42
|
+
"head",
|
|
43
|
+
"options",
|
|
44
|
+
"patch",
|
|
45
|
+
"post",
|
|
46
|
+
"put",
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
export function contractFactoryBindings(
|
|
50
|
+
sourceFile: ts.SourceFile,
|
|
51
|
+
): ContractFactoryBindings {
|
|
52
|
+
const bindings: ContractFactoryBindings = {
|
|
53
|
+
localNames: {
|
|
54
|
+
defineContract: new Set(),
|
|
55
|
+
defineContractGroup: new Set(),
|
|
56
|
+
},
|
|
57
|
+
namespaceNames: new Set(),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
for (const statement of sourceFile.statements) {
|
|
61
|
+
if (
|
|
62
|
+
!ts.isImportDeclaration(statement) ||
|
|
63
|
+
!ts.isStringLiteral(statement.moduleSpecifier) ||
|
|
64
|
+
statement.moduleSpecifier.text !== contractFactoryModule ||
|
|
65
|
+
!statement.importClause ||
|
|
66
|
+
statement.importClause.isTypeOnly
|
|
67
|
+
) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const namedBindings = statement.importClause.namedBindings;
|
|
72
|
+
if (namedBindings && ts.isNamespaceImport(namedBindings)) {
|
|
73
|
+
bindings.namespaceNames.add(namedBindings.name.text);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (!namedBindings || !ts.isNamedImports(namedBindings)) continue;
|
|
77
|
+
|
|
78
|
+
for (const element of namedBindings.elements) {
|
|
79
|
+
if (element.isTypeOnly) continue;
|
|
80
|
+
const importedName = (element.propertyName ?? element.name).text;
|
|
81
|
+
if (!isContractFactoryName(importedName)) continue;
|
|
82
|
+
bindings.localNames[importedName].add(element.name.text);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return bindings;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function isImportedContractFactoryCall(
|
|
90
|
+
call: ts.CallExpression,
|
|
91
|
+
name: ContractFactoryName,
|
|
92
|
+
bindings: ContractFactoryBindings,
|
|
93
|
+
): boolean {
|
|
94
|
+
const callee = unwrapExpression(call.expression);
|
|
95
|
+
if (ts.isIdentifier(callee)) {
|
|
96
|
+
return bindings.localNames[name].has(callee.text);
|
|
97
|
+
}
|
|
98
|
+
if (!ts.isPropertyAccessExpression(callee) || callee.name.text !== name) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const receiver = unwrapExpression(callee.expression);
|
|
103
|
+
return (
|
|
104
|
+
ts.isIdentifier(receiver) && bindings.namespaceNames.has(receiver.text)
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Follow imported factories through local group and contract builder aliases. */
|
|
109
|
+
export function contractBindingNames(
|
|
110
|
+
sourceFile: ts.SourceFile,
|
|
111
|
+
bindings: ContractFactoryBindings,
|
|
112
|
+
): ContractBindingNames {
|
|
113
|
+
const declarations = sourceFile.statements
|
|
114
|
+
.filter(ts.isVariableStatement)
|
|
115
|
+
.flatMap((statement) => [...statement.declarationList.declarations]);
|
|
116
|
+
const names: ContractBindingNames = {
|
|
117
|
+
contracts: new Set(),
|
|
118
|
+
groups: new Set(),
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
let changed = true;
|
|
122
|
+
while (changed) {
|
|
123
|
+
changed = false;
|
|
124
|
+
for (const declaration of declarations) {
|
|
125
|
+
if (
|
|
126
|
+
!ts.isIdentifier(declaration.name) ||
|
|
127
|
+
!declaration.initializer ||
|
|
128
|
+
names.contracts.has(declaration.name.text) ||
|
|
129
|
+
names.groups.has(declaration.name.text)
|
|
130
|
+
) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const chain = contractCallChain(declaration.initializer);
|
|
134
|
+
if (!chain) continue;
|
|
135
|
+
|
|
136
|
+
if (
|
|
137
|
+
contractFactoryCall(chain, "defineContract", bindings) ||
|
|
138
|
+
(chain.baseIdentifier && names.contracts.has(chain.baseIdentifier))
|
|
139
|
+
) {
|
|
140
|
+
names.contracts.add(declaration.name.text);
|
|
141
|
+
changed = true;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!isContractGroupDerived(chain, bindings, names.groups)) continue;
|
|
146
|
+
if (chain.calls.some((call) => httpContractMethods.has(call.name))) {
|
|
147
|
+
names.contracts.add(declaration.name.text);
|
|
148
|
+
changed = true;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (chain.calls.some((call) => !contractGroupMethods.has(call.name))) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
names.groups.add(declaration.name.text);
|
|
156
|
+
changed = true;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return names;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Determine whether an expression declares a direct or group-built contract. */
|
|
164
|
+
export function isImportedContractDeclaration(
|
|
165
|
+
expression: ts.Expression,
|
|
166
|
+
bindings: ContractFactoryBindings,
|
|
167
|
+
names: ContractBindingNames,
|
|
168
|
+
): boolean {
|
|
169
|
+
const chain = contractCallChain(expression);
|
|
170
|
+
if (!chain) return false;
|
|
171
|
+
if (
|
|
172
|
+
contractFactoryCall(chain, "defineContract", bindings) ||
|
|
173
|
+
(chain.baseIdentifier && names.contracts.has(chain.baseIdentifier))
|
|
174
|
+
) {
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
return (
|
|
178
|
+
isContractGroupDerived(chain, bindings, names.groups) &&
|
|
179
|
+
chain.calls.some((call) => httpContractMethods.has(call.name))
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function isContractGroupDerived(
|
|
184
|
+
chain: ContractCallChain,
|
|
185
|
+
bindings: ContractFactoryBindings,
|
|
186
|
+
groupNames: ReadonlySet<string>,
|
|
187
|
+
): boolean {
|
|
188
|
+
return Boolean(
|
|
189
|
+
contractFactoryCall(chain, "defineContractGroup", bindings) ||
|
|
190
|
+
(chain.baseIdentifier && groupNames.has(chain.baseIdentifier)),
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function contractFactoryCall(
|
|
195
|
+
chain: ContractCallChain,
|
|
196
|
+
name: ContractFactoryName,
|
|
197
|
+
bindings: ContractFactoryBindings,
|
|
198
|
+
): ContractCall | undefined {
|
|
199
|
+
const calls = chain.rootCall ? [chain.rootCall, ...chain.calls] : chain.calls;
|
|
200
|
+
return calls.find((call) =>
|
|
201
|
+
isImportedContractFactoryCall(call.node, name, bindings),
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function contractCallChain(
|
|
206
|
+
rawExpression: ts.Expression,
|
|
207
|
+
): ContractCallChain | undefined {
|
|
208
|
+
const calls: ContractCall[] = [];
|
|
209
|
+
let expression = rawExpression;
|
|
210
|
+
|
|
211
|
+
while (true) {
|
|
212
|
+
expression = unwrapExpression(expression);
|
|
213
|
+
if (ts.isPropertyAccessExpression(expression)) {
|
|
214
|
+
expression = expression.expression;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (ts.isCallExpression(expression)) {
|
|
218
|
+
const callee = unwrapExpression(expression.expression);
|
|
219
|
+
if (ts.isPropertyAccessExpression(callee)) {
|
|
220
|
+
calls.unshift({ name: callee.name.text, node: expression });
|
|
221
|
+
expression = callee.expression;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (ts.isIdentifier(callee)) {
|
|
225
|
+
return {
|
|
226
|
+
baseIdentifier: callee.text,
|
|
227
|
+
rootCall: { name: callee.text, node: expression },
|
|
228
|
+
calls,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
if (ts.isIdentifier(expression)) {
|
|
234
|
+
return { baseIdentifier: expression.text, calls };
|
|
235
|
+
}
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function isContractFactoryName(value: string): value is ContractFactoryName {
|
|
241
|
+
return value === "defineContract" || value === "defineContractGroup";
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function unwrapExpression(expression: ts.Expression): ts.Expression {
|
|
245
|
+
let current = expression;
|
|
246
|
+
while (
|
|
247
|
+
ts.isParenthesizedExpression(current) ||
|
|
248
|
+
ts.isAsExpression(current) ||
|
|
249
|
+
ts.isTypeAssertionExpression(current) ||
|
|
250
|
+
ts.isNonNullExpression(current) ||
|
|
251
|
+
ts.isSatisfiesExpression(current)
|
|
252
|
+
) {
|
|
253
|
+
current = current.expression;
|
|
254
|
+
}
|
|
255
|
+
return current;
|
|
256
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import {
|
|
3
|
+
clientDirPath,
|
|
4
|
+
directoryPath,
|
|
5
|
+
normalizePath,
|
|
6
|
+
type ResolvedBeignetConfig,
|
|
7
|
+
} from "../config.js";
|
|
8
|
+
|
|
9
|
+
export type SourceLayer =
|
|
10
|
+
| "app"
|
|
11
|
+
| "app-context"
|
|
12
|
+
| "agent-capability"
|
|
13
|
+
| "client"
|
|
14
|
+
| "component"
|
|
15
|
+
| "contract"
|
|
16
|
+
| "domain"
|
|
17
|
+
| "feature-helper"
|
|
18
|
+
| "feature-port"
|
|
19
|
+
| "infra"
|
|
20
|
+
| "lib"
|
|
21
|
+
| "policy"
|
|
22
|
+
| "port"
|
|
23
|
+
| "route"
|
|
24
|
+
| "schema"
|
|
25
|
+
| "seed"
|
|
26
|
+
| "server"
|
|
27
|
+
| "test"
|
|
28
|
+
| "use-case"
|
|
29
|
+
| "workflow"
|
|
30
|
+
| "unknown";
|
|
31
|
+
|
|
32
|
+
type FeatureLayerConvention = {
|
|
33
|
+
path: string;
|
|
34
|
+
layer: SourceLayer;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const canonicalFeatureDirectories = {
|
|
38
|
+
client: "client",
|
|
39
|
+
components: "component",
|
|
40
|
+
domain: "domain",
|
|
41
|
+
jobs: "workflow",
|
|
42
|
+
listeners: "workflow",
|
|
43
|
+
notifications: "workflow",
|
|
44
|
+
schedules: "workflow",
|
|
45
|
+
seeds: "seed",
|
|
46
|
+
tasks: "workflow",
|
|
47
|
+
tests: "test",
|
|
48
|
+
uploads: "workflow",
|
|
49
|
+
"use-cases": "use-case",
|
|
50
|
+
} as const satisfies Record<string, SourceLayer>;
|
|
51
|
+
|
|
52
|
+
const canonicalFeatureRootFiles = {
|
|
53
|
+
"agent-capabilities": "agent-capability",
|
|
54
|
+
contracts: "contract",
|
|
55
|
+
policy: "policy",
|
|
56
|
+
ports: "feature-port",
|
|
57
|
+
routes: "route",
|
|
58
|
+
schemas: "schema",
|
|
59
|
+
} as const satisfies Record<string, SourceLayer>;
|
|
60
|
+
|
|
61
|
+
const featureDirectoryAliases = {
|
|
62
|
+
command: "tasks",
|
|
63
|
+
commands: "tasks",
|
|
64
|
+
event: "domain/events",
|
|
65
|
+
events: "domain/events",
|
|
66
|
+
job: "jobs",
|
|
67
|
+
listener: "listeners",
|
|
68
|
+
notification: "notifications",
|
|
69
|
+
schedule: "schedules",
|
|
70
|
+
scheduled: "schedules",
|
|
71
|
+
task: "tasks",
|
|
72
|
+
upload: "uploads",
|
|
73
|
+
} as const;
|
|
74
|
+
|
|
75
|
+
export function classifySourcePath(
|
|
76
|
+
filePath: string,
|
|
77
|
+
config: ResolvedBeignetConfig,
|
|
78
|
+
): SourceLayer {
|
|
79
|
+
const normalizedPath = stripKnownExtension(normalizePath(filePath));
|
|
80
|
+
const featuresPath = directoryPath(config.paths.features);
|
|
81
|
+
const portsDir = directoryPath(path.dirname(config.paths.ports));
|
|
82
|
+
|
|
83
|
+
if (
|
|
84
|
+
isTestPath(normalizedPath) ||
|
|
85
|
+
isUnder(normalizedPath, directoryPath(config.paths.tests))
|
|
86
|
+
) {
|
|
87
|
+
return "test";
|
|
88
|
+
}
|
|
89
|
+
if (normalizedPath === stripKnownExtension(config.paths.appContext)) {
|
|
90
|
+
return "app-context";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const featurePath = featureRelativePath(normalizedPath, featuresPath);
|
|
94
|
+
if (featurePath) {
|
|
95
|
+
const layerSegment = featurePath.split("/")[1];
|
|
96
|
+
if (!layerSegment) return "feature-helper";
|
|
97
|
+
const convention = featureDirectoryConvention(layerSegment);
|
|
98
|
+
if (convention) return convention.layer;
|
|
99
|
+
const rootConvention = featureRootFileConvention(layerSegment);
|
|
100
|
+
if (rootConvention) return rootConvention.layer;
|
|
101
|
+
if (layerSegment.endsWith("-contracts")) return "contract";
|
|
102
|
+
return "feature-helper";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (
|
|
106
|
+
directoryPath(config.paths.contracts) !== featuresPath &&
|
|
107
|
+
isUnder(normalizedPath, directoryPath(config.paths.contracts))
|
|
108
|
+
) {
|
|
109
|
+
return "contract";
|
|
110
|
+
}
|
|
111
|
+
if (
|
|
112
|
+
directoryPath(config.paths.useCases) !== featuresPath &&
|
|
113
|
+
isUnder(normalizedPath, directoryPath(config.paths.useCases))
|
|
114
|
+
) {
|
|
115
|
+
return "use-case";
|
|
116
|
+
}
|
|
117
|
+
if (
|
|
118
|
+
directoryPath(config.paths.policies) !== featuresPath &&
|
|
119
|
+
isUnder(normalizedPath, directoryPath(config.paths.policies))
|
|
120
|
+
) {
|
|
121
|
+
return "policy";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (
|
|
125
|
+
isUnder(normalizedPath, directoryPath(path.dirname(config.paths.server)))
|
|
126
|
+
) {
|
|
127
|
+
return "server";
|
|
128
|
+
}
|
|
129
|
+
if (
|
|
130
|
+
isUnder(
|
|
131
|
+
normalizedPath,
|
|
132
|
+
directoryPath(path.dirname(config.paths.portWiring)),
|
|
133
|
+
)
|
|
134
|
+
) {
|
|
135
|
+
return "infra";
|
|
136
|
+
}
|
|
137
|
+
if (isUnder(normalizedPath, portsDir)) return "port";
|
|
138
|
+
if (isUnder(normalizedPath, clientDirPath(config))) return "client";
|
|
139
|
+
if (isUnder(normalizedPath, "components")) return "component";
|
|
140
|
+
if (isUnder(normalizedPath, legacySharedDomainPath(config))) return "domain";
|
|
141
|
+
if (isUnder(normalizedPath, "lib")) return "lib";
|
|
142
|
+
if (
|
|
143
|
+
isUnder(normalizedPath, directoryPath(path.dirname(config.paths.routes)))
|
|
144
|
+
) {
|
|
145
|
+
return "app";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return "unknown";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function canonicalFeatureLayerPath(
|
|
152
|
+
filePath: string,
|
|
153
|
+
config: ResolvedBeignetConfig,
|
|
154
|
+
): string | undefined {
|
|
155
|
+
const featuresPath = directoryPath(config.paths.features);
|
|
156
|
+
const relativePath = featureRelativePath(
|
|
157
|
+
normalizePath(filePath),
|
|
158
|
+
featuresPath,
|
|
159
|
+
);
|
|
160
|
+
if (!relativePath) return undefined;
|
|
161
|
+
|
|
162
|
+
const [feature, segment, ...rest] = relativePath.split("/");
|
|
163
|
+
if (!feature || !segment) return undefined;
|
|
164
|
+
|
|
165
|
+
if (isTestSourceFile(segment, rest) && segment !== "tests") {
|
|
166
|
+
return path.posix.join(
|
|
167
|
+
featuresPath,
|
|
168
|
+
feature,
|
|
169
|
+
"tests",
|
|
170
|
+
...(rest.length > 0 ? rest : [segment]),
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (rest.length > 0) {
|
|
175
|
+
const convention = featureDirectoryConvention(segment);
|
|
176
|
+
if (!convention || convention.path === segment) return undefined;
|
|
177
|
+
return path.posix.join(featuresPath, feature, convention.path, ...rest);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const extension = path.posix.extname(segment);
|
|
181
|
+
const stem = extension ? segment.slice(0, -extension.length) : segment;
|
|
182
|
+
const convention = featureRootFileConvention(stem);
|
|
183
|
+
if (!convention || convention.path === stem) return undefined;
|
|
184
|
+
return path.posix.join(
|
|
185
|
+
featuresPath,
|
|
186
|
+
feature,
|
|
187
|
+
`${convention.path}${extension}`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function isTestSourceFile(segment: string, rest: string[]): boolean {
|
|
192
|
+
const fileName = rest.at(-1) ?? segment;
|
|
193
|
+
return /\.(?:test|spec)\.(?:ts|tsx|mts|cts)$/.test(fileName);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function featureDirectoryConvention(
|
|
197
|
+
segment: string,
|
|
198
|
+
): FeatureLayerConvention | undefined {
|
|
199
|
+
const canonical = Object.entries(canonicalFeatureDirectories).find(
|
|
200
|
+
([candidate]) =>
|
|
201
|
+
normalizeLayerSegment(candidate) === normalizeLayerSegment(segment),
|
|
202
|
+
);
|
|
203
|
+
if (canonical) return { path: canonical[0], layer: canonical[1] };
|
|
204
|
+
|
|
205
|
+
const alias =
|
|
206
|
+
featureDirectoryAliases[
|
|
207
|
+
normalizeLayerSegment(segment) as keyof typeof featureDirectoryAliases
|
|
208
|
+
];
|
|
209
|
+
if (!alias) return undefined;
|
|
210
|
+
return {
|
|
211
|
+
path: alias,
|
|
212
|
+
layer:
|
|
213
|
+
alias === "domain/events"
|
|
214
|
+
? "domain"
|
|
215
|
+
: canonicalFeatureDirectories[
|
|
216
|
+
alias as keyof typeof canonicalFeatureDirectories
|
|
217
|
+
],
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function featureRootFileConvention(
|
|
222
|
+
segment: string,
|
|
223
|
+
): FeatureLayerConvention | undefined {
|
|
224
|
+
const canonical = Object.entries(canonicalFeatureRootFiles).find(
|
|
225
|
+
([candidate]) =>
|
|
226
|
+
normalizeLayerSegment(candidate) === normalizeLayerSegment(segment),
|
|
227
|
+
);
|
|
228
|
+
return canonical ? { path: canonical[0], layer: canonical[1] } : undefined;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function normalizeLayerSegment(segment: string): string {
|
|
232
|
+
return segment.replaceAll("-", "").replaceAll("_", "").toLowerCase();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function stripKnownExtension(filePath: string): string {
|
|
236
|
+
return filePath.replace(/\.(c|m)?tsx?$/, "");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function isTestPath(filePath: string): boolean {
|
|
240
|
+
return (
|
|
241
|
+
filePath.includes("/tests/") ||
|
|
242
|
+
filePath.endsWith(".test") ||
|
|
243
|
+
filePath.endsWith(".spec")
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function isUnder(filePath: string, directory: string): boolean {
|
|
248
|
+
const normalizedDir = directoryPath(directory);
|
|
249
|
+
if (!normalizedDir || normalizedDir === ".") return false;
|
|
250
|
+
return filePath === normalizedDir || filePath.startsWith(`${normalizedDir}/`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function featureRelativePath(
|
|
254
|
+
filePath: string,
|
|
255
|
+
featuresPath: string,
|
|
256
|
+
): string | undefined {
|
|
257
|
+
if (!isUnder(filePath, featuresPath)) return undefined;
|
|
258
|
+
return filePath.slice(`${directoryPath(featuresPath)}/`.length);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function legacySharedDomainPath(config: ResolvedBeignetConfig): string {
|
|
262
|
+
const featuresDir = directoryPath(config.paths.features);
|
|
263
|
+
const featuresParent = directoryPath(path.dirname(featuresDir));
|
|
264
|
+
if (!featuresParent || featuresParent === ".") return "domain";
|
|
265
|
+
return path.join(featuresParent, "domain");
|
|
266
|
+
}
|