@open-mercato/cli 0.6.7-develop.6756.1.5300c41145 → 0.6.7-develop.6766.1.40c9011995
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/.turbo/turbo-build.log +1 -1
- package/build.mjs +2 -1
- package/dist/agentic/guides/extensions.md +20 -3
- package/dist/agentic/guides/framework-extension-points.md +36 -0
- package/dist/agentic/guides/module-facts.json +39708 -230
- package/dist/agentic/guides/modules/ai_assistant.md +49 -9
- package/dist/agentic/guides/modules/api_docs.md +11 -1
- package/dist/agentic/guides/modules/api_keys.md +24 -1
- package/dist/agentic/guides/modules/attachments.md +22 -6
- package/dist/agentic/guides/modules/audit_logs.md +35 -1
- package/dist/agentic/guides/modules/auth.md +79 -15
- package/dist/agentic/guides/modules/business_rules.md +36 -3
- package/dist/agentic/guides/modules/catalog.md +160 -22
- package/dist/agentic/guides/modules/channel_gmail.md +14 -1
- package/dist/agentic/guides/modules/channel_imap.md +14 -1
- package/dist/agentic/guides/modules/checkout.md +134 -19
- package/dist/agentic/guides/modules/communication_channels.md +110 -20
- package/dist/agentic/guides/modules/configs.md +15 -1
- package/dist/agentic/guides/modules/content.md +11 -1
- package/dist/agentic/guides/modules/currencies.md +50 -9
- package/dist/agentic/guides/modules/customer_accounts.md +151 -28
- package/dist/agentic/guides/modules/customers.md +299 -52
- package/dist/agentic/guides/modules/dashboards.md +30 -1
- package/dist/agentic/guides/modules/data_sync.md +40 -7
- package/dist/agentic/guides/modules/dictionaries.md +24 -6
- package/dist/agentic/guides/modules/directory.md +52 -9
- package/dist/agentic/guides/modules/entities.md +41 -1
- package/dist/agentic/guides/modules/events.md +11 -1
- package/dist/agentic/guides/modules/feature_toggles.md +15 -1
- package/dist/agentic/guides/modules/gateway_stripe.md +14 -1
- package/dist/agentic/guides/modules/generators.md +11 -1
- package/dist/agentic/guides/modules/inbox_ops.md +58 -16
- package/dist/agentic/guides/modules/integrations.md +30 -7
- package/dist/agentic/guides/modules/messages.md +68 -13
- package/dist/agentic/guides/modules/notifications.md +13 -1
- package/dist/agentic/guides/modules/onboarding.md +13 -1
- package/dist/agentic/guides/modules/payment_gateways.md +51 -12
- package/dist/agentic/guides/modules/perspectives.md +16 -1
- package/dist/agentic/guides/modules/planner.md +33 -9
- package/dist/agentic/guides/modules/portal.md +24 -1
- package/dist/agentic/guides/modules/progress.md +34 -9
- package/dist/agentic/guides/modules/query_index.md +56 -3
- package/dist/agentic/guides/modules/record_locks.md +60 -13
- package/dist/agentic/guides/modules/resources.md +79 -18
- package/dist/agentic/guides/modules/sales.md +238 -48
- package/dist/agentic/guides/modules/scheduler.md +23 -7
- package/dist/agentic/guides/modules/search.md +28 -3
- package/dist/agentic/guides/modules/security.md +106 -23
- package/dist/agentic/guides/modules/shipping_carriers.md +38 -10
- package/dist/agentic/guides/modules/sso.md +47 -15
- package/dist/agentic/guides/modules/staff.md +117 -38
- package/dist/agentic/guides/modules/storage_s3.md +13 -1
- package/dist/agentic/guides/modules/sync_akeneo.md +13 -1
- package/dist/agentic/guides/modules/sync_excel.md +15 -1
- package/dist/agentic/guides/modules/system_status_overlays.md +14 -1
- package/dist/agentic/guides/modules/translations.md +25 -5
- package/dist/agentic/guides/modules/webhooks.md +85 -15
- package/dist/agentic/guides/modules/wms.md +234 -31
- package/dist/agentic/guides/modules/workflows.md +114 -30
- package/dist/agentic/guides/upstream/BACKWARD_COMPATIBILITY.md +7 -0
- package/dist/agentic/guides/upstream/manifest.json +2 -2
- package/dist/agentic/shared/AGENTS.md.template +2 -2
- package/dist/agentic/shared/ai/harness/README.md +2 -0
- package/dist/agentic/shared/ai/harness/cases.json +80 -39
- package/dist/agentic/shared/ai/skills/om-system-extension/references/mechanism-selector.md +3 -1
- package/dist/agentic/shared/ai/skills/om-system-extension/references/unified-overrides.md +2 -1
- package/dist/agentic/shared/scripts/framework-context.mjs +3 -9
- package/dist/lib/generators/module-extension-facts.js +1185 -0
- package/dist/lib/generators/module-extension-facts.js.map +7 -0
- package/dist/lib/generators/module-facts.js +134 -7
- package/dist/lib/generators/module-facts.js.map +2 -2
- package/dist/lib/generators/module-registry.js +1 -0
- package/dist/lib/generators/module-registry.js.map +2 -2
- package/package.json +5 -5
- package/src/lib/generators/__tests__/module-extension-facts.test.ts +402 -0
- package/src/lib/generators/__tests__/module-facts.bc-guard.test.ts +39 -2
- package/src/lib/generators/module-extension-facts.ts +1428 -0
- package/src/lib/generators/module-facts.ts +143 -6
- package/src/lib/generators/module-registry.ts +1 -1
|
@@ -0,0 +1,1185 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import ts from "typescript-js";
|
|
4
|
+
import {
|
|
5
|
+
CRUD_FORM_EXTENSION_SURFACES,
|
|
6
|
+
CRUD_FORM_LIFECYCLE_PHASES,
|
|
7
|
+
DATA_TABLE_EXTENSION_SURFACES
|
|
8
|
+
} from "@open-mercato/shared/modules/widgets/extension-points";
|
|
9
|
+
import { extractCommandIdsFromSource } from "./module-registry.js";
|
|
10
|
+
import { scanModuleDir, SCAN_CONFIGS } from "./scanner.js";
|
|
11
|
+
const FRAMEWORK_PREFIXES = [
|
|
12
|
+
"dashboard:",
|
|
13
|
+
"menu:",
|
|
14
|
+
"notification:",
|
|
15
|
+
"integration:",
|
|
16
|
+
"integrations.detail:",
|
|
17
|
+
"system-status:",
|
|
18
|
+
"topbar:"
|
|
19
|
+
];
|
|
20
|
+
const FRAMEWORK_HOSTS = [
|
|
21
|
+
frameworkHost("framework.backend.sidebar", "menu:backend.sidebar", "menu", ["menu-item"]),
|
|
22
|
+
frameworkHost("framework.settings.sidebar", "menu:settings.sidebar", "menu", ["menu-item"]),
|
|
23
|
+
frameworkHost("framework.profile.sidebar", "menu:profile.sidebar", "menu", ["menu-item"]),
|
|
24
|
+
frameworkHost("framework.portal.sidebar", "menu:portal.sidebar", "menu", ["menu-item"]),
|
|
25
|
+
frameworkHost("framework.dashboard", "dashboard:*", "dashboard", ["registry-contribution"]),
|
|
26
|
+
frameworkHost("framework.notifications", "notification:*", "notification", ["registry-contribution"]),
|
|
27
|
+
frameworkHost("framework.integration-detail", "integrations.detail:{integrationId}", "integration", ["render-widget"]),
|
|
28
|
+
frameworkHost("framework.integration-detail-fallback", "integrations.detail:tabs", "integration", ["render-widget"]),
|
|
29
|
+
frameworkHost("framework.backend-mutation", "backend-mutation:global", "generic", ["headless-widget"]),
|
|
30
|
+
frameworkHost("framework.crud-form-wildcard", "crud-form:*", "crud-form", ["field-widget", "lifecycle-handler"])
|
|
31
|
+
];
|
|
32
|
+
function frameworkHost(key, id, family, capabilities) {
|
|
33
|
+
return {
|
|
34
|
+
key,
|
|
35
|
+
id,
|
|
36
|
+
resolution: id.includes("*") || id.includes("{") ? "pattern" : "framework",
|
|
37
|
+
family,
|
|
38
|
+
ownerModule: "framework",
|
|
39
|
+
capabilities,
|
|
40
|
+
bound: true,
|
|
41
|
+
stability: "frozen",
|
|
42
|
+
source: { kind: "framework", path: "packages/ui/src", symbol: key }
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function frameworkOverrideHost(key, mode) {
|
|
46
|
+
return {
|
|
47
|
+
key: `framework.module-override.${key}`,
|
|
48
|
+
id: `module-override:${key}`,
|
|
49
|
+
resolution: "framework",
|
|
50
|
+
family: "module-override",
|
|
51
|
+
ownerModule: "framework",
|
|
52
|
+
capabilities: ["module-override"],
|
|
53
|
+
operations: [mode],
|
|
54
|
+
bound: true,
|
|
55
|
+
stability: "stable",
|
|
56
|
+
source: {
|
|
57
|
+
kind: "framework",
|
|
58
|
+
path: "packages/shared/src/modules/overrides.ts",
|
|
59
|
+
symbol: `ModuleOverrides.${key}`
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const FRAMEWORK_OVERRIDE_HOSTS = [
|
|
64
|
+
frameworkOverrideHost("ai.agents", "disable-replace"),
|
|
65
|
+
frameworkOverrideHost("ai.tools", "disable-replace"),
|
|
66
|
+
frameworkOverrideHost("ai.extensions", "additive"),
|
|
67
|
+
frameworkOverrideHost("routes.api", "disable-replace"),
|
|
68
|
+
frameworkOverrideHost("routes.pages", "disable-replace"),
|
|
69
|
+
frameworkOverrideHost("events.subscribers", "disable-replace"),
|
|
70
|
+
frameworkOverrideHost("workers", "disable-replace"),
|
|
71
|
+
frameworkOverrideHost("widgets.injection", "disable-replace"),
|
|
72
|
+
frameworkOverrideHost("widgets.components", "disable-replace"),
|
|
73
|
+
frameworkOverrideHost("widgets.dashboard", "disable-replace"),
|
|
74
|
+
frameworkOverrideHost("notifications.types", "disable-replace"),
|
|
75
|
+
frameworkOverrideHost("notifications.handlers", "disable-replace"),
|
|
76
|
+
frameworkOverrideHost("interceptors", "disable-replace"),
|
|
77
|
+
frameworkOverrideHost("commandInterceptors", "disable-replace"),
|
|
78
|
+
frameworkOverrideHost("enrichers", "disable-replace"),
|
|
79
|
+
frameworkOverrideHost("guards", "disable-replace"),
|
|
80
|
+
frameworkOverrideHost("cli", "disable-replace"),
|
|
81
|
+
frameworkOverrideHost("setup", "replace"),
|
|
82
|
+
frameworkOverrideHost("acl.features", "disable-replace"),
|
|
83
|
+
frameworkOverrideHost("di", "disable-replace"),
|
|
84
|
+
frameworkOverrideHost("encryption.maps", "disable-replace"),
|
|
85
|
+
frameworkOverrideHost("nav.groupOrder", "additive")
|
|
86
|
+
];
|
|
87
|
+
function allFrameworkHosts() {
|
|
88
|
+
return [...FRAMEWORK_HOSTS, ...FRAMEWORK_OVERRIDE_HOSTS];
|
|
89
|
+
}
|
|
90
|
+
function sourceFile(filePath) {
|
|
91
|
+
if (!fs.existsSync(filePath)) return null;
|
|
92
|
+
const scriptKind = filePath.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
|
|
93
|
+
return ts.createSourceFile(filePath, fs.readFileSync(filePath, "utf8"), ts.ScriptTarget.ES2020, true, scriptKind);
|
|
94
|
+
}
|
|
95
|
+
function unwrap(expression) {
|
|
96
|
+
let current = expression;
|
|
97
|
+
while (ts.isAsExpression(current) || ts.isTypeAssertionExpression(current) || ts.isParenthesizedExpression(current) || ts.isSatisfiesExpression(current)) {
|
|
98
|
+
current = current.expression;
|
|
99
|
+
}
|
|
100
|
+
return current;
|
|
101
|
+
}
|
|
102
|
+
function propertyName(node) {
|
|
103
|
+
if (ts.isIdentifier(node) || ts.isStringLiteralLike(node) || ts.isNumericLiteral(node)) return node.text;
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
function buildStaticContext(file) {
|
|
107
|
+
const initializers = /* @__PURE__ */ new Map();
|
|
108
|
+
const visit = (node) => {
|
|
109
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
|
|
110
|
+
initializers.set(node.name.text, node.initializer);
|
|
111
|
+
}
|
|
112
|
+
if (ts.isFunctionDeclaration(node) && node.name && node.body) {
|
|
113
|
+
initializers.set(node.name.text, node);
|
|
114
|
+
}
|
|
115
|
+
node.forEachChild(visit);
|
|
116
|
+
};
|
|
117
|
+
file.forEachChild(visit);
|
|
118
|
+
return { initializers, sourceFile: file, resolving: /* @__PURE__ */ new Set() };
|
|
119
|
+
}
|
|
120
|
+
function entityRegistryValue(expression) {
|
|
121
|
+
const parts = [];
|
|
122
|
+
let current = expression;
|
|
123
|
+
while (ts.isPropertyAccessExpression(current)) {
|
|
124
|
+
parts.unshift(current.name.text);
|
|
125
|
+
current = current.expression;
|
|
126
|
+
}
|
|
127
|
+
return ts.isIdentifier(current) && current.text === "E" && parts.length === 2 ? `${parts[0]}:${parts[1]}` : null;
|
|
128
|
+
}
|
|
129
|
+
function staticTemplate(expression, context) {
|
|
130
|
+
let value = expression.head.text;
|
|
131
|
+
for (const span of expression.templateSpans) {
|
|
132
|
+
const substitution = staticValue(span.expression, context);
|
|
133
|
+
if (typeof substitution !== "string" && typeof substitution !== "number") return null;
|
|
134
|
+
value += `${substitution}${span.literal.text}`;
|
|
135
|
+
}
|
|
136
|
+
return value;
|
|
137
|
+
}
|
|
138
|
+
function staticObject(expression, context) {
|
|
139
|
+
const result = {};
|
|
140
|
+
for (const property of expression.properties) {
|
|
141
|
+
if (ts.isPropertyAssignment(property)) {
|
|
142
|
+
const name = propertyName(property.name);
|
|
143
|
+
if (!name) continue;
|
|
144
|
+
const value = staticValue(property.initializer, context);
|
|
145
|
+
if (value !== void 0) result[name] = value;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (ts.isShorthandPropertyAssignment(property)) {
|
|
149
|
+
const value = staticValue(property.name, context);
|
|
150
|
+
if (value !== void 0) result[property.name.text] = value;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (ts.isMethodDeclaration(property)) {
|
|
154
|
+
const name = propertyName(property.name);
|
|
155
|
+
if (name) result[name] = true;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (ts.isSpreadAssignment(property)) {
|
|
159
|
+
const value = staticValue(property.expression, context);
|
|
160
|
+
if (isStaticObject(value)) Object.assign(result, value);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
function staticValue(expression, context) {
|
|
166
|
+
const current = unwrap(expression);
|
|
167
|
+
if (ts.isStringLiteralLike(current) || ts.isNumericLiteral(current)) return current.text;
|
|
168
|
+
if (current.kind === ts.SyntaxKind.TrueKeyword) return true;
|
|
169
|
+
if (current.kind === ts.SyntaxKind.FalseKeyword) return false;
|
|
170
|
+
if (current.kind === ts.SyntaxKind.NullKeyword) return null;
|
|
171
|
+
if (ts.isNoSubstitutionTemplateLiteral(current)) return current.text;
|
|
172
|
+
if (ts.isTemplateExpression(current)) return staticTemplate(current, context) ?? void 0;
|
|
173
|
+
if (ts.isArrayLiteralExpression(current)) {
|
|
174
|
+
return current.elements.flatMap((element) => {
|
|
175
|
+
if (ts.isSpreadElement(element)) {
|
|
176
|
+
const spread = staticValue(element.expression, context);
|
|
177
|
+
return Array.isArray(spread) ? spread : [];
|
|
178
|
+
}
|
|
179
|
+
const value = staticValue(element, context);
|
|
180
|
+
return value === void 0 ? [] : [value];
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
if (ts.isObjectLiteralExpression(current)) return staticObject(current, context);
|
|
184
|
+
if (ts.isPropertyAccessExpression(current)) {
|
|
185
|
+
return entityRegistryValue(current) ?? void 0;
|
|
186
|
+
}
|
|
187
|
+
if (ts.isIdentifier(current)) {
|
|
188
|
+
if (context.resolving.has(current.text)) return void 0;
|
|
189
|
+
const initializer = context.initializers.get(current.text);
|
|
190
|
+
if (!initializer) return void 0;
|
|
191
|
+
context.resolving.add(current.text);
|
|
192
|
+
const value = ts.isFunctionDeclaration(initializer) ? void 0 : staticValue(initializer, context);
|
|
193
|
+
context.resolving.delete(current.text);
|
|
194
|
+
return value;
|
|
195
|
+
}
|
|
196
|
+
if (ts.isCallExpression(current)) {
|
|
197
|
+
if (ts.isIdentifier(current.expression)) {
|
|
198
|
+
const callable = context.initializers.get(current.expression.text);
|
|
199
|
+
if (callable && (ts.isArrowFunction(callable) || ts.isFunctionExpression(callable) || ts.isFunctionDeclaration(callable))) {
|
|
200
|
+
const initializers = new Map(context.initializers);
|
|
201
|
+
callable.parameters.forEach((parameter, index) => {
|
|
202
|
+
if (ts.isIdentifier(parameter.name) && current.arguments[index]) {
|
|
203
|
+
initializers.set(parameter.name.text, current.arguments[index]);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
const childContext = {
|
|
207
|
+
initializers,
|
|
208
|
+
sourceFile: context.sourceFile,
|
|
209
|
+
resolving: new Set(context.resolving)
|
|
210
|
+
};
|
|
211
|
+
if (callable.body && ts.isExpression(callable.body)) return staticValue(callable.body, childContext);
|
|
212
|
+
const returnStatement = callable.body?.statements.find(ts.isReturnStatement);
|
|
213
|
+
if (returnStatement?.expression) return staticValue(returnStatement.expression, childContext);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const firstArgument = current.arguments[0];
|
|
217
|
+
if (firstArgument) {
|
|
218
|
+
const value = staticValue(firstArgument, context);
|
|
219
|
+
if (!isStaticObject(value) || !ts.isIdentifier(current.expression)) return value;
|
|
220
|
+
if (current.expression.text === "dataTableExtensionHost") return { family: "data-table", ...value };
|
|
221
|
+
if (current.expression.text === "crudFormExtensionHost") return { family: "crud-form", ...value };
|
|
222
|
+
if (current.expression.text === "componentExtensionHost") return { family: "component-handle", ...value };
|
|
223
|
+
return value;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (ts.isConditionalExpression(current)) {
|
|
227
|
+
const left = staticValue(current.whenTrue, context);
|
|
228
|
+
const right = staticValue(current.whenFalse, context);
|
|
229
|
+
return left !== void 0 && JSON.stringify(left) === JSON.stringify(right) ? left : void 0;
|
|
230
|
+
}
|
|
231
|
+
return void 0;
|
|
232
|
+
}
|
|
233
|
+
function isStaticObject(value) {
|
|
234
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
235
|
+
}
|
|
236
|
+
function strings(value) {
|
|
237
|
+
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
238
|
+
}
|
|
239
|
+
function stringValue(value) {
|
|
240
|
+
return typeof value === "string" ? value : void 0;
|
|
241
|
+
}
|
|
242
|
+
function numberValue(value) {
|
|
243
|
+
if (typeof value === "number") return value;
|
|
244
|
+
if (typeof value === "string" && /^\d+$/.test(value)) return Number(value);
|
|
245
|
+
return void 0;
|
|
246
|
+
}
|
|
247
|
+
function booleanValue(value) {
|
|
248
|
+
return typeof value === "boolean" ? value : void 0;
|
|
249
|
+
}
|
|
250
|
+
function portablePath(moduleRoot, sourceRoot, filePath) {
|
|
251
|
+
return path.posix.join(sourceRoot, path.relative(moduleRoot, filePath).split(path.sep).join("/"));
|
|
252
|
+
}
|
|
253
|
+
function conventionPath(moduleRoot, relativePath) {
|
|
254
|
+
const direct = path.join(moduleRoot, relativePath);
|
|
255
|
+
if (fs.existsSync(direct)) return direct;
|
|
256
|
+
if (relativePath.endsWith(".ts")) {
|
|
257
|
+
const tsx = `${direct.slice(0, -3)}.tsx`;
|
|
258
|
+
if (fs.existsSync(tsx)) return tsx;
|
|
259
|
+
}
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
function readConventionObjectArray(filePath, exportName) {
|
|
263
|
+
const file = sourceFile(filePath);
|
|
264
|
+
if (!file) return [];
|
|
265
|
+
const context = buildStaticContext(file);
|
|
266
|
+
const initializer = context.initializers.get(exportName);
|
|
267
|
+
if (!initializer || ts.isFunctionDeclaration(initializer)) return [];
|
|
268
|
+
const value = staticValue(initializer, context);
|
|
269
|
+
return Array.isArray(value) ? value.filter(isStaticObject) : [];
|
|
270
|
+
}
|
|
271
|
+
function readRootObject(filePath, variableName) {
|
|
272
|
+
const file = sourceFile(filePath);
|
|
273
|
+
if (!file) return null;
|
|
274
|
+
const context = buildStaticContext(file);
|
|
275
|
+
const initializer = context.initializers.get(variableName);
|
|
276
|
+
if (!initializer || ts.isFunctionDeclaration(initializer)) return null;
|
|
277
|
+
const value = staticValue(initializer, context);
|
|
278
|
+
return isStaticObject(value) ? value : null;
|
|
279
|
+
}
|
|
280
|
+
function readCallObjectArguments(filePath, calleeNames) {
|
|
281
|
+
const file = sourceFile(filePath);
|
|
282
|
+
if (!file) return [];
|
|
283
|
+
const context = buildStaticContext(file);
|
|
284
|
+
const result = [];
|
|
285
|
+
const visit = (node) => {
|
|
286
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && calleeNames.has(node.expression.text)) {
|
|
287
|
+
const firstArgument = node.arguments[0];
|
|
288
|
+
const value = firstArgument ? staticValue(firstArgument, context) : void 0;
|
|
289
|
+
if (isStaticObject(value)) result.push({ callee: node.expression.text, value });
|
|
290
|
+
}
|
|
291
|
+
node.forEachChild(visit);
|
|
292
|
+
};
|
|
293
|
+
file.forEachChild(visit);
|
|
294
|
+
return result;
|
|
295
|
+
}
|
|
296
|
+
function sortHosts(hosts) {
|
|
297
|
+
const sourceKey = (host) => host.source.kind === "fact-ref" ? `${host.source.factSection}:${host.source.factKey}` : `${host.source.path}:${host.source.symbol}`;
|
|
298
|
+
return hosts.sort((left, right) => left.family.localeCompare(right.family) || left.id.localeCompare(right.id) || sourceKey(left).localeCompare(sourceKey(right)) || left.key.localeCompare(right.key));
|
|
299
|
+
}
|
|
300
|
+
function sortContributions(contributions) {
|
|
301
|
+
const targetKey = (contribution) => contribution.targets.map((entry) => entry.id).join("\0");
|
|
302
|
+
const sourceKey = (contribution) => `${contribution.source.path}:${contribution.source.symbol ?? ""}`;
|
|
303
|
+
return contributions.sort((left, right) => left.kind.localeCompare(right.kind) || left.id.localeCompare(right.id) || targetKey(left).localeCompare(targetKey(right)) || sourceKey(left).localeCompare(sourceKey(right)));
|
|
304
|
+
}
|
|
305
|
+
function target(id, resolution = "unresolved") {
|
|
306
|
+
return { id, resolution };
|
|
307
|
+
}
|
|
308
|
+
function factTarget(id, factSection, factKey = id) {
|
|
309
|
+
return { id, resolution: "fact-ref", factRef: { factSection, factKey } };
|
|
310
|
+
}
|
|
311
|
+
function openTarget(id) {
|
|
312
|
+
return target(id);
|
|
313
|
+
}
|
|
314
|
+
function declarationSource(sourceRoot, symbol) {
|
|
315
|
+
return { kind: "declaration", path: path.posix.join(sourceRoot, "extension-points.ts"), symbol };
|
|
316
|
+
}
|
|
317
|
+
function hasDeclarationBinding(moduleRoot, hostKey, declaration) {
|
|
318
|
+
const relativeSource = stringValue(declaration.source);
|
|
319
|
+
if (!relativeSource) return false;
|
|
320
|
+
const bindingFile = sourceFile(path.join(moduleRoot, relativeSource));
|
|
321
|
+
if (!bindingFile) return false;
|
|
322
|
+
const escapedHostKey = hostKey.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
323
|
+
const bindingPattern = new RegExp(
|
|
324
|
+
`extensionPoints\\s*\\.\\s*hosts(?:\\s*\\.\\s*${escapedHostKey}|\\s*\\[\\s*['"]${escapedHostKey}['"]\\s*\\])`
|
|
325
|
+
);
|
|
326
|
+
if (bindingPattern.test(bindingFile.text)) return true;
|
|
327
|
+
return declaration.family === "integration" && typeof declaration.pattern === "string" && /resolveIntegrationDetailWidgetSpotId\s*\(/.test(bindingFile.text);
|
|
328
|
+
}
|
|
329
|
+
function baseHost(options) {
|
|
330
|
+
const { declaration } = options;
|
|
331
|
+
const aliases = strings(declaration.aliases);
|
|
332
|
+
const fallbacks = strings(declaration.fallbacks);
|
|
333
|
+
const fact = {
|
|
334
|
+
key: options.key,
|
|
335
|
+
id: options.id,
|
|
336
|
+
resolution: options.resolution ?? (options.id.includes("{") || options.id.includes("*") ? "pattern" : "exact"),
|
|
337
|
+
family: options.family,
|
|
338
|
+
ownerModule: options.moduleId,
|
|
339
|
+
capabilities: options.capabilities,
|
|
340
|
+
bound: options.bound ?? true,
|
|
341
|
+
stability: "frozen",
|
|
342
|
+
source: declarationSource(options.sourceRoot, `extensionPoints.hosts.${options.key.split(".")[0]}`)
|
|
343
|
+
};
|
|
344
|
+
const contextContract = stringValue(declaration.contextContract);
|
|
345
|
+
const dataContract = stringValue(declaration.dataContract);
|
|
346
|
+
const scopeContract = stringValue(declaration.scopeContract);
|
|
347
|
+
const runtimeContract = stringValue(declaration.runtimeContract);
|
|
348
|
+
const activation = stringValue(declaration.activation);
|
|
349
|
+
if (contextContract) fact.contextContract = contextContract;
|
|
350
|
+
if (dataContract) fact.dataContract = dataContract;
|
|
351
|
+
if (scopeContract) fact.scopeContract = scopeContract;
|
|
352
|
+
if (runtimeContract) fact.runtimeContract = runtimeContract;
|
|
353
|
+
if (activation) fact.activation = activation;
|
|
354
|
+
if (aliases.length > 0) fact.aliases = aliases;
|
|
355
|
+
if (fallbacks.length > 0) fact.fallbacks = fallbacks;
|
|
356
|
+
return fact;
|
|
357
|
+
}
|
|
358
|
+
function extractDeclaredHosts(options) {
|
|
359
|
+
const filePath = conventionPath(options.moduleRoot, "extension-points.ts");
|
|
360
|
+
if (!filePath) return { hosts: [], unresolved: [] };
|
|
361
|
+
const declaration = readRootObject(filePath, "extensionPoints");
|
|
362
|
+
const hostsObject = declaration && isStaticObject(declaration.hosts) ? declaration.hosts : null;
|
|
363
|
+
if (!hostsObject) {
|
|
364
|
+
return {
|
|
365
|
+
hosts: [],
|
|
366
|
+
unresolved: [{
|
|
367
|
+
key: `${options.moduleId}.extension-points`,
|
|
368
|
+
source: { path: portablePath(options.moduleRoot, options.sourceRoot, filePath), symbol: "extensionPoints" },
|
|
369
|
+
reason: "unclassified-binding"
|
|
370
|
+
}]
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
const hosts = [];
|
|
374
|
+
const unresolved = [];
|
|
375
|
+
for (const hostKey of Object.keys(hostsObject).sort((left, right) => left.localeCompare(right))) {
|
|
376
|
+
const host = hostsObject[hostKey];
|
|
377
|
+
if (!isStaticObject(host)) continue;
|
|
378
|
+
const family = stringValue(host.family);
|
|
379
|
+
if (!family) continue;
|
|
380
|
+
const declarationBound = hasDeclarationBinding(options.moduleRoot, hostKey, host);
|
|
381
|
+
if (!declarationBound) {
|
|
382
|
+
unresolved.push({
|
|
383
|
+
key: `${options.moduleId}.${hostKey}`,
|
|
384
|
+
source: {
|
|
385
|
+
path: stringValue(host.source) ?? portablePath(options.moduleRoot, options.sourceRoot, filePath),
|
|
386
|
+
symbol: `extensionPoints.hosts.${hostKey}`
|
|
387
|
+
},
|
|
388
|
+
reason: "unbound-declaration"
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
if (family === "data-table") {
|
|
392
|
+
const tableId = stringValue(host.tableId);
|
|
393
|
+
if (!tableId) continue;
|
|
394
|
+
const baseSpotId = stringValue(host.baseSpotId) ?? `data-table:${tableId}`;
|
|
395
|
+
for (const surface of DATA_TABLE_EXTENSION_SURFACES) {
|
|
396
|
+
const usesBase = ["header", "footer", "toolbar", "searchTrailing", "emptyState"].includes(surface.key);
|
|
397
|
+
const id2 = surface.key === "replacement" ? `data-table:${tableId}` : `${usesBase ? baseSpotId : `data-table:${tableId}`}:${surface.suffix}`;
|
|
398
|
+
hosts.push(baseHost({
|
|
399
|
+
key: `${hostKey}.${surface.key}`,
|
|
400
|
+
id: id2,
|
|
401
|
+
moduleId: options.moduleId,
|
|
402
|
+
family,
|
|
403
|
+
capabilities: [...surface.capabilities],
|
|
404
|
+
sourceRoot: options.sourceRoot,
|
|
405
|
+
declaration: host,
|
|
406
|
+
bound: surface.bound && declarationBound
|
|
407
|
+
}));
|
|
408
|
+
}
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
if (family === "crud-form") {
|
|
412
|
+
const entityId = stringValue(host.entityId);
|
|
413
|
+
if (!entityId) continue;
|
|
414
|
+
const spotId = stringValue(host.spotId) ?? `crud-form:${entityId}`;
|
|
415
|
+
for (const surface of CRUD_FORM_EXTENSION_SURFACES) {
|
|
416
|
+
const id2 = surface.suffix ? `${spotId}:${surface.suffix}` : spotId;
|
|
417
|
+
const fact2 = baseHost({
|
|
418
|
+
key: `${hostKey}.${surface.key}`,
|
|
419
|
+
id: id2,
|
|
420
|
+
moduleId: options.moduleId,
|
|
421
|
+
family,
|
|
422
|
+
capabilities: [...surface.capabilities],
|
|
423
|
+
sourceRoot: options.sourceRoot,
|
|
424
|
+
declaration: host,
|
|
425
|
+
bound: surface.bound && declarationBound
|
|
426
|
+
});
|
|
427
|
+
if (surface.key === "base") fact2.phases = [...CRUD_FORM_LIFECYCLE_PHASES];
|
|
428
|
+
hosts.push(fact2);
|
|
429
|
+
}
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
const id = stringValue(host.spotId) ?? stringValue(host.pattern) ?? stringValue(host.componentId);
|
|
433
|
+
if (!id) {
|
|
434
|
+
unresolved.push({
|
|
435
|
+
key: `${options.moduleId}.${hostKey}`,
|
|
436
|
+
source: { path: portablePath(options.moduleRoot, options.sourceRoot, filePath), symbol: hostKey },
|
|
437
|
+
reason: "dynamic-without-pattern"
|
|
438
|
+
});
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
const capabilities = family === "component-handle" ? ["component-replacement"] : strings(host.supported);
|
|
442
|
+
const fact = baseHost({
|
|
443
|
+
key: hostKey,
|
|
444
|
+
id,
|
|
445
|
+
moduleId: options.moduleId,
|
|
446
|
+
family,
|
|
447
|
+
capabilities,
|
|
448
|
+
sourceRoot: options.sourceRoot,
|
|
449
|
+
declaration: host,
|
|
450
|
+
bound: declarationBound
|
|
451
|
+
});
|
|
452
|
+
if (isStaticObject(host.parameters)) {
|
|
453
|
+
fact.patternParameters = Object.fromEntries(Object.entries(host.parameters).flatMap(([key, value]) => {
|
|
454
|
+
if (!isStaticObject(value) || typeof value.source !== "string") return [];
|
|
455
|
+
return [[key, {
|
|
456
|
+
source: value.source,
|
|
457
|
+
...typeof value.pattern === "string" ? { pattern: value.pattern } : {}
|
|
458
|
+
}]];
|
|
459
|
+
}));
|
|
460
|
+
}
|
|
461
|
+
hosts.push(fact);
|
|
462
|
+
}
|
|
463
|
+
return { hosts: sortHosts(hosts), unresolved };
|
|
464
|
+
}
|
|
465
|
+
function factRefHost(options) {
|
|
466
|
+
return {
|
|
467
|
+
key: options.key,
|
|
468
|
+
id: options.id,
|
|
469
|
+
resolution: "fact-ref",
|
|
470
|
+
family: options.family,
|
|
471
|
+
ownerModule: options.moduleId,
|
|
472
|
+
capabilities: options.capabilities,
|
|
473
|
+
...options.phases ? { phases: options.phases } : {},
|
|
474
|
+
...options.activation ? { activation: options.activation } : {},
|
|
475
|
+
...options.scopeContract ? { scopeContract: options.scopeContract } : {},
|
|
476
|
+
bound: true,
|
|
477
|
+
stability: "stable",
|
|
478
|
+
source: { kind: "fact-ref", factSection: options.factSection, factKey: options.id }
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
function sourceFilesBelow(directory) {
|
|
482
|
+
if (!fs.existsSync(directory)) return [];
|
|
483
|
+
const files = [];
|
|
484
|
+
const visit = (current) => {
|
|
485
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
486
|
+
const entryPath = path.join(current, entry.name);
|
|
487
|
+
if (entry.isDirectory()) {
|
|
488
|
+
if (entry.name !== "__tests__" && entry.name !== "__integration__") visit(entryPath);
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
if (entry.isFile() && /\.(?:ts|tsx)$/.test(entry.name) && !/\.(?:test|spec)\./.test(entry.name)) files.push(entryPath);
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
visit(directory);
|
|
495
|
+
return files;
|
|
496
|
+
}
|
|
497
|
+
function apiRouteId(moduleId, moduleRoot, filePath) {
|
|
498
|
+
const relative = path.relative(path.join(moduleRoot, "api"), filePath).split(path.sep).join("/");
|
|
499
|
+
if (relative === "interceptors.ts" || relative === "openapi.ts") return null;
|
|
500
|
+
const withoutExtension = relative.replace(/\.tsx?$/, "");
|
|
501
|
+
const suffix = withoutExtension.endsWith("/route") ? withoutExtension.slice(0, -6) : withoutExtension;
|
|
502
|
+
return suffix && !suffix.includes("__tests__") ? `${moduleId.replace(/_/g, "-")}/${suffix}` : null;
|
|
503
|
+
}
|
|
504
|
+
function apiExtensionHostIds(moduleId, moduleRoot) {
|
|
505
|
+
const entityIds = /* @__PURE__ */ new Set();
|
|
506
|
+
const commandIds = /* @__PURE__ */ new Set();
|
|
507
|
+
const routeIds = /* @__PURE__ */ new Set();
|
|
508
|
+
for (const filePath of sourceFilesBelow(path.join(moduleRoot, "api"))) {
|
|
509
|
+
const routeId = apiRouteId(moduleId, moduleRoot, filePath);
|
|
510
|
+
if (routeId) routeIds.add(routeId);
|
|
511
|
+
const file = sourceFile(filePath);
|
|
512
|
+
if (!file) continue;
|
|
513
|
+
const context = buildStaticContext(file);
|
|
514
|
+
const visit = (node) => {
|
|
515
|
+
if (ts.isPropertyAssignment(node)) {
|
|
516
|
+
const name = propertyName(node.name);
|
|
517
|
+
if (name === "commandId") {
|
|
518
|
+
const id = stringValue(staticValue(node.initializer, context));
|
|
519
|
+
if (id) commandIds.add(id);
|
|
520
|
+
}
|
|
521
|
+
if (name === "enrichers") {
|
|
522
|
+
const config = staticValue(node.initializer, context);
|
|
523
|
+
const entityId = isStaticObject(config) ? stringValue(config.entityId) : void 0;
|
|
524
|
+
if (entityId) entityIds.add(entityId);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "validateCrudMutationGuard") {
|
|
528
|
+
const input = node.arguments[1] ? staticValue(node.arguments[1], context) : void 0;
|
|
529
|
+
const entityId = isStaticObject(input) ? stringValue(input.resourceKind) : void 0;
|
|
530
|
+
if (entityId) entityIds.add(entityId);
|
|
531
|
+
}
|
|
532
|
+
node.forEachChild(visit);
|
|
533
|
+
};
|
|
534
|
+
file.forEachChild(visit);
|
|
535
|
+
}
|
|
536
|
+
return {
|
|
537
|
+
entityIds: [...entityIds].sort((left, right) => left.localeCompare(right)),
|
|
538
|
+
commandIds: [...commandIds].sort((left, right) => left.localeCompare(right)),
|
|
539
|
+
routeIds: [...routeIds].sort((left, right) => left.localeCompare(right))
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
function moduleCommandIds(moduleRoot, apiCommandIds) {
|
|
543
|
+
const commandIds = new Set(apiCommandIds);
|
|
544
|
+
for (const filePath of sourceFilesBelow(path.join(moduleRoot, "commands"))) {
|
|
545
|
+
if (path.basename(filePath).replace(/\.(?:ts|tsx)$/, "") === "interceptors") continue;
|
|
546
|
+
for (const id of extractCommandIdsFromSource(filePath)) commandIds.add(id);
|
|
547
|
+
}
|
|
548
|
+
return [...commandIds].sort((left, right) => left.localeCompare(right));
|
|
549
|
+
}
|
|
550
|
+
function extractKnownApiRouteIds(moduleId, moduleRoot) {
|
|
551
|
+
return apiExtensionHostIds(moduleId, moduleRoot).routeIds;
|
|
552
|
+
}
|
|
553
|
+
function extractKnownCommandIds(moduleId, moduleRoot) {
|
|
554
|
+
const apiHosts = apiExtensionHostIds(moduleId, moduleRoot);
|
|
555
|
+
return moduleCommandIds(moduleRoot, apiHosts.commandIds);
|
|
556
|
+
}
|
|
557
|
+
function extractFactRefHosts(options) {
|
|
558
|
+
const hosts = [];
|
|
559
|
+
for (const entity of options.entities) {
|
|
560
|
+
hosts.push(factRefHost({
|
|
561
|
+
key: `entity.${entity.id}`,
|
|
562
|
+
id: entity.id,
|
|
563
|
+
moduleId: options.moduleId,
|
|
564
|
+
family: "entity",
|
|
565
|
+
capabilities: ["response-enricher", "query-enricher", "mutation-guard", "entity-extension"],
|
|
566
|
+
factSection: "entities",
|
|
567
|
+
activation: "host-opt-in",
|
|
568
|
+
scopeContract: "tenant-and-organization"
|
|
569
|
+
}));
|
|
570
|
+
}
|
|
571
|
+
for (const route of options.apiRoutes) {
|
|
572
|
+
hosts.push(factRefHost({
|
|
573
|
+
key: `api-route.${route.path}`,
|
|
574
|
+
id: route.path,
|
|
575
|
+
moduleId: options.moduleId,
|
|
576
|
+
family: "api-route",
|
|
577
|
+
capabilities: ["api-interceptor", "mutation-guard"],
|
|
578
|
+
factSection: "apiRoutes",
|
|
579
|
+
phases: ["before", "after"],
|
|
580
|
+
activation: "host-opt-in",
|
|
581
|
+
scopeContract: "request-auth-scope"
|
|
582
|
+
}));
|
|
583
|
+
}
|
|
584
|
+
for (const event of options.events) {
|
|
585
|
+
const capabilities = ["async-subscriber", "sync-subscriber"];
|
|
586
|
+
if (event.clientBroadcast) capabilities.push("browser-client");
|
|
587
|
+
if (event.portalBroadcast) capabilities.push("browser-portal");
|
|
588
|
+
hosts.push(factRefHost({
|
|
589
|
+
key: `event.${event.id}`,
|
|
590
|
+
id: event.id,
|
|
591
|
+
moduleId: options.moduleId,
|
|
592
|
+
family: "event",
|
|
593
|
+
capabilities,
|
|
594
|
+
factSection: "events",
|
|
595
|
+
scopeContract: "tenant-organization-and-audience"
|
|
596
|
+
}));
|
|
597
|
+
}
|
|
598
|
+
for (const entityId of options.searchEntities) {
|
|
599
|
+
const eventStem = entityId.replace(":", ".");
|
|
600
|
+
for (const phase of ["querying", "queried"]) {
|
|
601
|
+
hosts.push(factRefHost({
|
|
602
|
+
key: `query-lifecycle.${eventStem}.${phase}`,
|
|
603
|
+
id: `${eventStem}.${phase}`,
|
|
604
|
+
moduleId: options.moduleId,
|
|
605
|
+
family: "query-lifecycle",
|
|
606
|
+
capabilities: ["sync-subscriber"],
|
|
607
|
+
factSection: "searchEntities",
|
|
608
|
+
phases: phase === "querying" ? ["block", "query-transform"] : ["result-transform", "scope-reapply"],
|
|
609
|
+
activation: "caller-opt-in",
|
|
610
|
+
scopeContract: "tenant-and-organization-reapplied-after-result"
|
|
611
|
+
}));
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
const apiHosts = apiExtensionHostIds(options.moduleId, options.moduleRoot);
|
|
615
|
+
for (const entityId of apiHosts.entityIds) {
|
|
616
|
+
hosts.push(factRefHost({
|
|
617
|
+
key: `api-entity.${entityId}`,
|
|
618
|
+
id: entityId,
|
|
619
|
+
moduleId: options.moduleId,
|
|
620
|
+
family: "entity",
|
|
621
|
+
capabilities: ["response-enricher", "query-enricher", "mutation-guard"],
|
|
622
|
+
factSection: "apiRoutes",
|
|
623
|
+
activation: "host-opt-in",
|
|
624
|
+
scopeContract: "tenant-and-organization"
|
|
625
|
+
}));
|
|
626
|
+
}
|
|
627
|
+
return sortHosts([...new Map(hosts.map((host) => [`${host.family}:${host.id}`, host])).values()]);
|
|
628
|
+
}
|
|
629
|
+
function contributionBase(id, sourcePath, sourceSymbol) {
|
|
630
|
+
return {
|
|
631
|
+
id,
|
|
632
|
+
targets: [],
|
|
633
|
+
scopeContract: "tenant-and-organization",
|
|
634
|
+
source: { path: sourcePath, ...sourceSymbol ? { symbol: sourceSymbol } : {} }
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
function extractInjectionTable(options) {
|
|
638
|
+
const filePath = conventionPath(options.moduleRoot, "widgets/injection-table.ts");
|
|
639
|
+
if (!filePath) return [];
|
|
640
|
+
const table = readRootObject(filePath, "injectionTable");
|
|
641
|
+
if (!table) return [];
|
|
642
|
+
const sourcePath = portablePath(options.moduleRoot, options.sourceRoot, filePath);
|
|
643
|
+
const facts = [];
|
|
644
|
+
for (const targetId of Object.keys(table).sort((left, right) => left.localeCompare(right))) {
|
|
645
|
+
const entries = table[targetId];
|
|
646
|
+
if (!Array.isArray(entries)) continue;
|
|
647
|
+
for (const entry of entries) {
|
|
648
|
+
if (!isStaticObject(entry)) continue;
|
|
649
|
+
const widgetId = stringValue(entry.widgetId);
|
|
650
|
+
if (!widgetId) continue;
|
|
651
|
+
const payload = targetId.endsWith(":columns") ? "column" : targetId.endsWith(":row-actions") ? "row-action" : targetId.endsWith(":bulk-actions") ? "bulk-action" : targetId.endsWith(":filters") ? "filter" : targetId.endsWith(":fields") ? "field" : "render";
|
|
652
|
+
const base = contributionBase(`${widgetId}@${targetId}`, sourcePath, "injectionTable");
|
|
653
|
+
const shared = {
|
|
654
|
+
...base,
|
|
655
|
+
targets: [target(targetId)],
|
|
656
|
+
features: strings(entry.features),
|
|
657
|
+
placement: numberValue(entry.priority) !== void 0 ? { priority: numberValue(entry.priority) } : void 0
|
|
658
|
+
};
|
|
659
|
+
if (targetId.startsWith("data-table:")) {
|
|
660
|
+
facts.push({
|
|
661
|
+
...shared,
|
|
662
|
+
kind: "data-table",
|
|
663
|
+
details: {
|
|
664
|
+
payload: payload === "field" ? "render" : payload,
|
|
665
|
+
tableId: targetId.replace(/^data-table:/, "").replace(/:(?:columns|row-actions|bulk-actions|filters|toolbar|header|footer|search-trailing)$/, ""),
|
|
666
|
+
executionGuard: strings(entry.features).length > 0 ? "both" : "host"
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
} else if (targetId.startsWith("crud-form:")) {
|
|
670
|
+
facts.push({
|
|
671
|
+
...shared,
|
|
672
|
+
kind: "crud-form",
|
|
673
|
+
details: {
|
|
674
|
+
payload: payload === "field" ? "field" : "render",
|
|
675
|
+
entityId: targetId.replace(/^crud-form:/, "").replace(/:(?:fields|header)$/, ""),
|
|
676
|
+
requestHeaderCapability: payload === "field"
|
|
677
|
+
}
|
|
678
|
+
});
|
|
679
|
+
} else {
|
|
680
|
+
facts.push({
|
|
681
|
+
...shared,
|
|
682
|
+
kind: "widget",
|
|
683
|
+
details: {
|
|
684
|
+
payload: "render",
|
|
685
|
+
registryKey: widgetId,
|
|
686
|
+
executionGuard: strings(entry.features).length > 0 ? "both" : "host"
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
return facts;
|
|
693
|
+
}
|
|
694
|
+
function extractObjectConvention(options) {
|
|
695
|
+
const filePath = conventionPath(options.module.moduleRoot, options.relativePath);
|
|
696
|
+
if (!filePath) return [];
|
|
697
|
+
const exportNames = [options.exportName, ...options.exportAliases ?? []];
|
|
698
|
+
const entries = exportNames.reduce((found, exportName) => found.length > 0 ? found : readConventionObjectArray(filePath, exportName), []);
|
|
699
|
+
const sourcePath = portablePath(options.module.moduleRoot, options.module.sourceRoot, filePath);
|
|
700
|
+
return entries.flatMap((entry, index) => options.build(entry, sourcePath, index) ?? []);
|
|
701
|
+
}
|
|
702
|
+
function methodPhases(entry, map) {
|
|
703
|
+
return map.flatMap(([property, phase]) => entry[property] === true ? [phase] : []);
|
|
704
|
+
}
|
|
705
|
+
function extractEnrichers(options) {
|
|
706
|
+
return extractObjectConvention({
|
|
707
|
+
module: options,
|
|
708
|
+
relativePath: "data/enrichers.ts",
|
|
709
|
+
exportName: "enrichers",
|
|
710
|
+
build(entry, sourcePath, index) {
|
|
711
|
+
const id = stringValue(entry.id) ?? `${options.moduleId}.enricher.${index}`;
|
|
712
|
+
const targetEntity = stringValue(entry.targetEntity);
|
|
713
|
+
if (!targetEntity) return null;
|
|
714
|
+
const timeoutMs = numberValue(entry.timeout) ?? 2e3;
|
|
715
|
+
const queryEngine = isStaticObject(entry.queryEngine) ? entry.queryEngine : null;
|
|
716
|
+
const surfaces = [entry.enrichMany === true ? "list" : null, entry.enrichOne === true ? "detail" : null].filter((value) => value !== null);
|
|
717
|
+
const base = contributionBase(id, sourcePath, id);
|
|
718
|
+
return {
|
|
719
|
+
...base,
|
|
720
|
+
kind: "response-enricher",
|
|
721
|
+
targets: [openTarget(targetEntity)],
|
|
722
|
+
features: strings(entry.features),
|
|
723
|
+
phases: surfaces,
|
|
724
|
+
activation: queryEngine ? "caller-opt-in" : "host-opt-in",
|
|
725
|
+
roundTripId: `${targetEntity}:read`,
|
|
726
|
+
details: {
|
|
727
|
+
targetEntity,
|
|
728
|
+
surfaces,
|
|
729
|
+
timeoutMs,
|
|
730
|
+
fallback: entry.fallback === void 0 ? "none" : "configured",
|
|
731
|
+
critical: booleanValue(entry.critical) ?? false,
|
|
732
|
+
cachePosture: "rerun-on-list-cache-hit",
|
|
733
|
+
...queryEngine ? { queryEngine: {
|
|
734
|
+
engines: strings(queryEngine.engines),
|
|
735
|
+
applyOn: (strings(queryEngine.applyOn).length > 0 ? strings(queryEngine.applyOn) : ["list", "detail"]).filter((value) => value === "list" || value === "detail"),
|
|
736
|
+
activation: "caller-opt-in"
|
|
737
|
+
} } : {}
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
function extractApiInterceptors(options) {
|
|
744
|
+
return extractObjectConvention({
|
|
745
|
+
module: options,
|
|
746
|
+
relativePath: "api/interceptors.ts",
|
|
747
|
+
exportName: "interceptors",
|
|
748
|
+
build(entry, sourcePath, index) {
|
|
749
|
+
const id = stringValue(entry.id) ?? `${options.moduleId}.api-interceptor.${index}`;
|
|
750
|
+
const targetRoute = stringValue(entry.targetRoute);
|
|
751
|
+
if (!targetRoute) return null;
|
|
752
|
+
const phases = methodPhases(entry, [["before", "before"], ["after", "after"]]).filter((phase) => phase === "before" || phase === "after");
|
|
753
|
+
const methods = strings(entry.methods);
|
|
754
|
+
const base = contributionBase(id, sourcePath, id);
|
|
755
|
+
return {
|
|
756
|
+
...base,
|
|
757
|
+
kind: "api-interceptor",
|
|
758
|
+
targets: [openTarget(targetRoute)],
|
|
759
|
+
phases,
|
|
760
|
+
features: strings(entry.features),
|
|
761
|
+
details: {
|
|
762
|
+
route: targetRoute,
|
|
763
|
+
methods,
|
|
764
|
+
phases,
|
|
765
|
+
activation: targetRoute.startsWith("/") ? "custom-route-bridge" : "crud-pipeline",
|
|
766
|
+
timeoutMs: numberValue(entry.timeout) ?? 2e3,
|
|
767
|
+
failurePosture: booleanValue(entry.critical) ? "fail-closed" : "fallback"
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
function extractCommandInterceptors(options) {
|
|
774
|
+
return extractObjectConvention({
|
|
775
|
+
module: options,
|
|
776
|
+
relativePath: "commands/interceptors.ts",
|
|
777
|
+
exportName: "interceptors",
|
|
778
|
+
build(entry, sourcePath, index) {
|
|
779
|
+
const id = stringValue(entry.id) ?? `${options.moduleId}.command-interceptor.${index}`;
|
|
780
|
+
const targetCommand = stringValue(entry.targetCommand);
|
|
781
|
+
if (!targetCommand) return null;
|
|
782
|
+
const phases = methodPhases(entry, [
|
|
783
|
+
["beforeExecute", "before-execute"],
|
|
784
|
+
["afterExecute", "after-execute"],
|
|
785
|
+
["beforeUndo", "before-undo"],
|
|
786
|
+
["afterUndo", "after-undo"]
|
|
787
|
+
]).filter((phase) => phase === "before-execute" || phase === "after-execute" || phase === "before-undo" || phase === "after-undo");
|
|
788
|
+
const base = contributionBase(id, sourcePath, id);
|
|
789
|
+
return {
|
|
790
|
+
...base,
|
|
791
|
+
kind: "command-interceptor",
|
|
792
|
+
targets: [openTarget(targetCommand)],
|
|
793
|
+
phases,
|
|
794
|
+
features: strings(entry.features),
|
|
795
|
+
details: { targetCommand, phases }
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
function extractGuards(options) {
|
|
801
|
+
return extractObjectConvention({
|
|
802
|
+
module: options,
|
|
803
|
+
relativePath: "data/guards.ts",
|
|
804
|
+
exportName: "guards",
|
|
805
|
+
build(entry, sourcePath, index) {
|
|
806
|
+
const id = stringValue(entry.id) ?? `${options.moduleId}.guard.${index}`;
|
|
807
|
+
const targetEntity = stringValue(entry.targetEntity);
|
|
808
|
+
if (!targetEntity) return null;
|
|
809
|
+
const operations = strings(entry.operations).filter((operation) => operation === "create" || operation === "update" || operation === "delete");
|
|
810
|
+
const capabilities = [entry.validate === true ? "block" : null, entry.validate === true ? "rewrite" : null, entry.afterSuccess === true ? "after-success" : null].filter((value) => value !== null);
|
|
811
|
+
const base = contributionBase(id, sourcePath, id);
|
|
812
|
+
return {
|
|
813
|
+
...base,
|
|
814
|
+
kind: "mutation-guard",
|
|
815
|
+
targets: [openTarget(targetEntity)],
|
|
816
|
+
operations,
|
|
817
|
+
features: strings(entry.features),
|
|
818
|
+
roundTripId: `${targetEntity}:write`,
|
|
819
|
+
details: { entityId: targetEntity, operations, capabilities, optimisticLock: "preserved" }
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
function extractEntityExtensions(options) {
|
|
825
|
+
return extractObjectConvention({
|
|
826
|
+
module: options,
|
|
827
|
+
relativePath: "data/extensions.ts",
|
|
828
|
+
exportName: "entityExtensions",
|
|
829
|
+
exportAliases: ["extensions"],
|
|
830
|
+
build(entry, sourcePath, index) {
|
|
831
|
+
const baseEntity = stringValue(entry.base);
|
|
832
|
+
const extensionEntity = stringValue(entry.extension);
|
|
833
|
+
const join = isStaticObject(entry.join) ? entry.join : null;
|
|
834
|
+
if (!baseEntity || !extensionEntity || !join) return null;
|
|
835
|
+
const id = `${options.moduleId}.entity-extension.${index}:${baseEntity}->${extensionEntity}`;
|
|
836
|
+
const contribution = contributionBase(id, sourcePath, "extensions");
|
|
837
|
+
return {
|
|
838
|
+
...contribution,
|
|
839
|
+
kind: "entity-extension",
|
|
840
|
+
targets: [openTarget(baseEntity)],
|
|
841
|
+
details: {
|
|
842
|
+
hostEntityId: baseEntity,
|
|
843
|
+
extensionEntityId: extensionEntity,
|
|
844
|
+
linkId: `${stringValue(join.baseKey) ?? "id"}:${stringValue(join.extensionKey) ?? "id"}`,
|
|
845
|
+
scopeContract: "tenant-and-organization",
|
|
846
|
+
orphanContract: booleanValue(entry.required) ? "required" : "optional"
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
function extractSubscribers(options) {
|
|
853
|
+
const directory = path.join(options.moduleRoot, "subscribers");
|
|
854
|
+
if (!fs.existsSync(directory)) return [];
|
|
855
|
+
const facts = [];
|
|
856
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
857
|
+
if (!entry.isFile() || !/\.(?:ts|tsx)$/.test(entry.name) || /\.(?:test|spec)\./.test(entry.name)) continue;
|
|
858
|
+
const filePath = path.join(directory, entry.name);
|
|
859
|
+
const metadata = readRootObject(filePath, "metadata");
|
|
860
|
+
if (!metadata) continue;
|
|
861
|
+
const event = stringValue(metadata.event);
|
|
862
|
+
if (!event) continue;
|
|
863
|
+
const subscriberId = stringValue(metadata.id) ?? `${options.moduleId}:${entry.name.replace(/\.tsx?$/, "")}`;
|
|
864
|
+
const persistent = booleanValue(metadata.persistent) ?? false;
|
|
865
|
+
const sync = booleanValue(metadata.sync) ?? !persistent;
|
|
866
|
+
const contribution = contributionBase(subscriberId, portablePath(options.moduleRoot, options.sourceRoot, filePath), "metadata");
|
|
867
|
+
facts.push({
|
|
868
|
+
...contribution,
|
|
869
|
+
kind: "subscriber",
|
|
870
|
+
targets: [openTarget(event)],
|
|
871
|
+
phases: sync ? ["before-or-after"] : ["async-delivery"],
|
|
872
|
+
details: {
|
|
873
|
+
event,
|
|
874
|
+
subscriberId,
|
|
875
|
+
persistent,
|
|
876
|
+
sync,
|
|
877
|
+
...numberValue(metadata.priority) !== void 0 ? { priority: numberValue(metadata.priority) } : {}
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
return facts;
|
|
882
|
+
}
|
|
883
|
+
function extractBrowserReactions(options) {
|
|
884
|
+
return options.events.flatMap((event) => {
|
|
885
|
+
const transports = [event.clientBroadcast ? "client" : null, event.portalBroadcast ? "portal" : null].filter((value) => value !== null);
|
|
886
|
+
if (transports.length === 0) return [];
|
|
887
|
+
const contribution = contributionBase(`${event.id}.browser`, path.posix.join(options.sourceRoot, "events.ts"), "events");
|
|
888
|
+
return [{
|
|
889
|
+
...contribution,
|
|
890
|
+
kind: "browser-reaction",
|
|
891
|
+
targets: [factTarget(event.id, "events")],
|
|
892
|
+
details: {
|
|
893
|
+
transports,
|
|
894
|
+
hooks: transports.flatMap((transport) => transport === "client" ? ["useAppEvent", "useOperationProgress"] : ["usePortalAppEvent"]),
|
|
895
|
+
audienceScopeContract: "tenant-organization-user-role-and-customer"
|
|
896
|
+
}
|
|
897
|
+
}];
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
function extractNotificationReactions(options) {
|
|
901
|
+
return extractObjectConvention({
|
|
902
|
+
module: options,
|
|
903
|
+
relativePath: "notifications.handlers.ts",
|
|
904
|
+
exportName: "notificationHandlers",
|
|
905
|
+
build(entry, sourcePath, index) {
|
|
906
|
+
const notificationType = stringValue(entry.notificationType);
|
|
907
|
+
if (!notificationType) return null;
|
|
908
|
+
const id = stringValue(entry.id) ?? `${options.moduleId}.notification-handler.${index}`;
|
|
909
|
+
const contribution = contributionBase(id, sourcePath, "notificationHandlers");
|
|
910
|
+
return {
|
|
911
|
+
...contribution,
|
|
912
|
+
kind: "browser-reaction",
|
|
913
|
+
targets: [factTarget(notificationType, "notifications")],
|
|
914
|
+
features: strings(entry.features),
|
|
915
|
+
placement: numberValue(entry.priority) !== void 0 ? { priority: numberValue(entry.priority) } : void 0,
|
|
916
|
+
details: {
|
|
917
|
+
transports: ["notification-effect"],
|
|
918
|
+
hooks: ["useNotificationEffect"],
|
|
919
|
+
audienceScopeContract: "tenant-organization-user-role-and-customer"
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
function extractComponentOverrides(options) {
|
|
926
|
+
return extractObjectConvention({
|
|
927
|
+
module: options,
|
|
928
|
+
relativePath: "widgets/components.ts",
|
|
929
|
+
exportName: "componentOverrides",
|
|
930
|
+
build(entry, sourcePath, index) {
|
|
931
|
+
const targetDefinition = isStaticObject(entry.target) ? entry.target : null;
|
|
932
|
+
const handle = targetDefinition ? stringValue(targetDefinition.componentId) : void 0;
|
|
933
|
+
if (!handle) return null;
|
|
934
|
+
const mode = entry.wrapper !== void 0 ? "wrapper" : entry.props !== void 0 ? "props" : "replace";
|
|
935
|
+
const id = `${options.moduleId}.component-override.${index}:${handle}`;
|
|
936
|
+
const contribution = contributionBase(id, sourcePath, "componentOverrides");
|
|
937
|
+
return {
|
|
938
|
+
...contribution,
|
|
939
|
+
kind: "component-override",
|
|
940
|
+
targets: [target(handle)],
|
|
941
|
+
details: {
|
|
942
|
+
handle,
|
|
943
|
+
mode,
|
|
944
|
+
propsContract: stringValue(entry.propsContract) ?? "component-props-schema"
|
|
945
|
+
}
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
function extractSpecializedRegistries(options) {
|
|
951
|
+
const facts = [];
|
|
952
|
+
const add = (id, registry, sourcePath, specialistRoute, symbol = id) => {
|
|
953
|
+
const contributionId = `${registry}:${id}`;
|
|
954
|
+
const contribution = contributionBase(contributionId, sourcePath, id);
|
|
955
|
+
const fact = {
|
|
956
|
+
...contribution,
|
|
957
|
+
kind: "specialized-registry",
|
|
958
|
+
targets: [factTarget(id, specialistRoute)],
|
|
959
|
+
source: { path: sourcePath, symbol },
|
|
960
|
+
details: { registry, registryId: id, specialistRoute }
|
|
961
|
+
};
|
|
962
|
+
const existingIndex = facts.findIndex((entry) => entry.id === contributionId);
|
|
963
|
+
if (existingIndex >= 0) facts[existingIndex] = fact;
|
|
964
|
+
else facts.push(fact);
|
|
965
|
+
};
|
|
966
|
+
for (const id of options.notifications ?? []) add(id, "notification", path.posix.join(options.sourceRoot, "notifications.ts"), "notifications");
|
|
967
|
+
for (const id of options.searchEntities) add(id, "search", path.posix.join(options.sourceRoot, "search.ts"), "search");
|
|
968
|
+
for (const tool of options.aiTools ?? []) add(tool.name, "ai", tool.sourcePath, "aiTools");
|
|
969
|
+
for (const agent of options.aiAgents ?? []) add(agent.id, "ai", agent.sourcePath, "aiAgents");
|
|
970
|
+
const vectorPath = conventionPath(options.moduleRoot, "vector.ts");
|
|
971
|
+
if (vectorPath) {
|
|
972
|
+
const vectorConfig = readRootObject(vectorPath, "vectorConfig") ?? readRootObject(vectorPath, "config");
|
|
973
|
+
const sourcePath = portablePath(options.moduleRoot, options.sourceRoot, vectorPath);
|
|
974
|
+
if (vectorConfig && Array.isArray(vectorConfig.entities)) {
|
|
975
|
+
for (const entity of vectorConfig.entities) {
|
|
976
|
+
if (!isStaticObject(entity)) continue;
|
|
977
|
+
const entityId = stringValue(entity.entityId);
|
|
978
|
+
if (entityId) add(entityId, "vector", sourcePath, "vector", "vectorConfig");
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
const integrationPath = conventionPath(options.moduleRoot, "integration.ts");
|
|
983
|
+
if (integrationPath) {
|
|
984
|
+
const integration = readRootObject(integrationPath, "integration");
|
|
985
|
+
const sourcePath = portablePath(options.moduleRoot, options.sourceRoot, integrationPath);
|
|
986
|
+
const integrationId = integration ? stringValue(integration.id) : void 0;
|
|
987
|
+
if (integrationId) add(integrationId, "integration", sourcePath, "integrations", "integration");
|
|
988
|
+
const providerKey = integration ? stringValue(integration.providerKey) : void 0;
|
|
989
|
+
const category = integration ? stringValue(integration.category) : void 0;
|
|
990
|
+
const categoryRegistry = category === "payment" ? "payment" : category === "shipping" ? "shipping" : category === "currency" ? "currency" : null;
|
|
991
|
+
if (providerKey && categoryRegistry) {
|
|
992
|
+
const specialistRoute = categoryRegistry === "payment" ? "paymentGateways" : categoryRegistry === "shipping" ? "shippingCarriers" : "currencies";
|
|
993
|
+
add(providerKey, categoryRegistry, sourcePath, specialistRoute, "integration.providerKey");
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
const workflowsPath = conventionPath(options.moduleRoot, "workflows.ts");
|
|
997
|
+
if (workflowsPath) {
|
|
998
|
+
const config = readRootObject(workflowsPath, "workflowsConfig");
|
|
999
|
+
const sourcePath = portablePath(options.moduleRoot, options.sourceRoot, workflowsPath);
|
|
1000
|
+
if (config && Array.isArray(config.workflows)) {
|
|
1001
|
+
for (const workflow of config.workflows) {
|
|
1002
|
+
if (!isStaticObject(workflow)) continue;
|
|
1003
|
+
const workflowId = stringValue(workflow.workflowId);
|
|
1004
|
+
if (workflowId) add(workflowId, "workflow", sourcePath, "workflows", "workflowsConfig");
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
const diPath = conventionPath(options.moduleRoot, "di.ts");
|
|
1009
|
+
if (diPath) {
|
|
1010
|
+
const sourcePath = portablePath(options.moduleRoot, options.sourceRoot, diPath);
|
|
1011
|
+
const registrations = readCallObjectArguments(diPath, /* @__PURE__ */ new Set([
|
|
1012
|
+
"registerPaymentGatewayDescriptor",
|
|
1013
|
+
"registerShippingAdapter",
|
|
1014
|
+
"registerCurrencyProvider"
|
|
1015
|
+
]));
|
|
1016
|
+
for (const registration of registrations) {
|
|
1017
|
+
const registry = registration.callee === "registerPaymentGatewayDescriptor" ? "payment" : registration.callee === "registerShippingAdapter" ? "shipping" : "currency";
|
|
1018
|
+
const registryId = stringValue(registration.value.providerKey) ?? stringValue(registration.value.key) ?? stringValue(registration.value.id);
|
|
1019
|
+
if (!registryId) continue;
|
|
1020
|
+
const specialistRoute = registry === "payment" ? "paymentGateways" : registry === "shipping" ? "shippingCarriers" : "currencies";
|
|
1021
|
+
add(registryId, registry, sourcePath, specialistRoute, registration.callee);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
const dashboardRoots = { appBase: path.join(options.moduleRoot, "__app__"), pkgBase: options.moduleRoot };
|
|
1025
|
+
for (const widgetFile of scanModuleDir(dashboardRoots, SCAN_CONFIGS.dashboardWidgets)) {
|
|
1026
|
+
const filePath = path.join(options.moduleRoot, "widgets", "dashboard", widgetFile.relPath);
|
|
1027
|
+
const widget = readRootObject(filePath, "widget");
|
|
1028
|
+
const metadata = widget && isStaticObject(widget.metadata) ? widget.metadata : null;
|
|
1029
|
+
if (!metadata) continue;
|
|
1030
|
+
const widgetId = stringValue(metadata.id);
|
|
1031
|
+
if (!widgetId) continue;
|
|
1032
|
+
const sourcePath = portablePath(options.moduleRoot, options.sourceRoot, filePath);
|
|
1033
|
+
const contribution = contributionBase(`dashboard:${widgetId}`, sourcePath, "widget.metadata");
|
|
1034
|
+
facts.push({
|
|
1035
|
+
...contribution,
|
|
1036
|
+
kind: "widget",
|
|
1037
|
+
targets: [target(`dashboard:${widgetId}`)],
|
|
1038
|
+
features: strings(metadata.features),
|
|
1039
|
+
details: {
|
|
1040
|
+
payload: "dashboard",
|
|
1041
|
+
registryKey: widgetId,
|
|
1042
|
+
executionGuard: strings(metadata.features).length > 0 ? "both" : "host"
|
|
1043
|
+
}
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
return facts;
|
|
1047
|
+
}
|
|
1048
|
+
function extractModuleExtensionFacts(options) {
|
|
1049
|
+
const declared = extractDeclaredHosts(options);
|
|
1050
|
+
const hosts = sortHosts([...declared.hosts, ...extractFactRefHosts(options)]);
|
|
1051
|
+
const contributions = sortContributions([
|
|
1052
|
+
...extractInjectionTable(options),
|
|
1053
|
+
...extractEnrichers(options),
|
|
1054
|
+
...extractApiInterceptors(options),
|
|
1055
|
+
...extractCommandInterceptors(options),
|
|
1056
|
+
...extractGuards(options),
|
|
1057
|
+
...extractEntityExtensions(options),
|
|
1058
|
+
...extractSubscribers(options),
|
|
1059
|
+
...extractBrowserReactions(options),
|
|
1060
|
+
...extractNotificationReactions(options),
|
|
1061
|
+
...extractComponentOverrides(options),
|
|
1062
|
+
...extractSpecializedRegistries(options)
|
|
1063
|
+
]);
|
|
1064
|
+
return { hosts, contributions, unresolved: declared.unresolved };
|
|
1065
|
+
}
|
|
1066
|
+
function patternMatches(pattern, targetId) {
|
|
1067
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\\\{[^}]+\\\}/g, "[^:]+").replace(/\*/g, ".*");
|
|
1068
|
+
return new RegExp(`^${escaped}$`).test(targetId);
|
|
1069
|
+
}
|
|
1070
|
+
function targetModuleId(targetId) {
|
|
1071
|
+
const withoutPrefix = targetId.replace(/^(?:data-table|crud-form|detail|portal-page):/, "");
|
|
1072
|
+
const match = /^([a-z][a-z0-9_]*)[.:]/.exec(withoutPrefix);
|
|
1073
|
+
return match?.[1] ?? null;
|
|
1074
|
+
}
|
|
1075
|
+
function correlateExtensionTarget(targetFact, options) {
|
|
1076
|
+
if (targetFact.resolution !== "unresolved") return targetFact;
|
|
1077
|
+
const allHosts = Object.values(options.surfacesByModule).flatMap((surface) => surface.hosts);
|
|
1078
|
+
if (targetFact.id.includes("*") || targetFact.id.includes("{")) {
|
|
1079
|
+
const knownIds = [
|
|
1080
|
+
...allHosts.filter((host) => host.bound).map((host) => host.id),
|
|
1081
|
+
...options.entityIds,
|
|
1082
|
+
...options.eventIds,
|
|
1083
|
+
...options.apiRoutes,
|
|
1084
|
+
...options.commandIds ?? []
|
|
1085
|
+
];
|
|
1086
|
+
if (targetFact.id === "*" || knownIds.some((knownId) => patternMatches(targetFact.id, knownId))) {
|
|
1087
|
+
return { id: targetFact.id, resolution: "pattern" };
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
const exact = allHosts.find((host) => host.bound && (host.id === targetFact.id || host.aliases?.includes(targetFact.id) || host.family === "api-route" && host.id.endsWith(`/${targetFact.id}`)));
|
|
1091
|
+
if (exact) {
|
|
1092
|
+
return exact.source.kind === "fact-ref" ? factTarget(targetFact.id, exact.source.factSection, exact.source.factKey) : { id: targetFact.id, resolution: "exact" };
|
|
1093
|
+
}
|
|
1094
|
+
const patterned = allHosts.find((host) => host.bound && host.resolution === "pattern" && patternMatches(host.id, targetFact.id));
|
|
1095
|
+
if (patterned) return { id: targetFact.id, resolution: "pattern" };
|
|
1096
|
+
const framework = allFrameworkHosts().find((host) => patternMatches(host.id, targetFact.id)) ?? (FRAMEWORK_PREFIXES.some((prefix) => targetFact.id.startsWith(prefix)) ? FRAMEWORK_HOSTS[0] : void 0);
|
|
1097
|
+
if (framework) return { id: targetFact.id, resolution: "framework" };
|
|
1098
|
+
if (options.entityIds.has(targetFact.id)) {
|
|
1099
|
+
return { id: targetFact.id, resolution: "fact-ref", factRef: { factSection: "entities", factKey: targetFact.id } };
|
|
1100
|
+
}
|
|
1101
|
+
if (options.eventIds.has(targetFact.id)) {
|
|
1102
|
+
return { id: targetFact.id, resolution: "fact-ref", factRef: { factSection: "events", factKey: targetFact.id } };
|
|
1103
|
+
}
|
|
1104
|
+
if (options.commandIds?.has(targetFact.id)) {
|
|
1105
|
+
return factTarget(targetFact.id, "commands");
|
|
1106
|
+
}
|
|
1107
|
+
const route = [...options.apiRoutes].find((apiRoute) => apiRoute === targetFact.id || apiRoute.endsWith(`/${targetFact.id}`));
|
|
1108
|
+
if (route) return { id: targetFact.id, resolution: "fact-ref", factRef: { factSection: "apiRoutes", factKey: route } };
|
|
1109
|
+
const ownerModule = targetModuleId(targetFact.id);
|
|
1110
|
+
if (ownerModule && !options.surfacesByModule[ownerModule]) {
|
|
1111
|
+
return { id: targetFact.id, resolution: "optional-external", optionalOwnerPackage: `module:${ownerModule}` };
|
|
1112
|
+
}
|
|
1113
|
+
if (ownerModule && options.contributingModuleId && ownerModule !== options.contributingModuleId) {
|
|
1114
|
+
return { id: targetFact.id, resolution: "optional-external", optionalOwnerPackage: `module:${ownerModule}` };
|
|
1115
|
+
}
|
|
1116
|
+
return targetFact;
|
|
1117
|
+
}
|
|
1118
|
+
function correlateModuleExtensionFacts(options) {
|
|
1119
|
+
const result = {};
|
|
1120
|
+
for (const moduleId of Object.keys(options.surfacesByModule).sort((left, right) => left.localeCompare(right))) {
|
|
1121
|
+
const surface = options.surfacesByModule[moduleId];
|
|
1122
|
+
const unresolved = [...surface.unresolved];
|
|
1123
|
+
const contributions = surface.contributions.map((contribution) => {
|
|
1124
|
+
const targets = contribution.targets.map((entry) => correlateExtensionTarget(entry, {
|
|
1125
|
+
...options,
|
|
1126
|
+
contributingModuleId: moduleId
|
|
1127
|
+
}));
|
|
1128
|
+
for (const entry of targets) {
|
|
1129
|
+
if (entry.resolution !== "unresolved") continue;
|
|
1130
|
+
unresolved.push({
|
|
1131
|
+
key: `${contribution.id}:${entry.id}`,
|
|
1132
|
+
source: contribution.source,
|
|
1133
|
+
reason: "unresolved-first-party-target"
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
return { ...contribution, targets };
|
|
1137
|
+
});
|
|
1138
|
+
result[moduleId] = {
|
|
1139
|
+
hosts: sortHosts([...surface.hosts]),
|
|
1140
|
+
contributions: sortContributions(contributions),
|
|
1141
|
+
unresolved: unresolved.sort((left, right) => left.key.localeCompare(right.key))
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
return result;
|
|
1145
|
+
}
|
|
1146
|
+
function assertNoUnresolvedExtensionTargets(surfacesByModule) {
|
|
1147
|
+
const unresolved = Object.entries(surfacesByModule).flatMap(
|
|
1148
|
+
([moduleId, surface]) => surface.unresolved.filter((entry) => entry.reason === "unresolved-first-party-target").map((entry) => `${moduleId}:${entry.key}`)
|
|
1149
|
+
).sort((left, right) => left.localeCompare(right));
|
|
1150
|
+
if (unresolved.length > 0) {
|
|
1151
|
+
throw new Error(`[module-facts] unresolved first-party extension targets: ${unresolved.join(", ")}`);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
function getFrameworkExtensionHosts() {
|
|
1155
|
+
return sortHosts(allFrameworkHosts().map((host) => ({
|
|
1156
|
+
...host,
|
|
1157
|
+
capabilities: [...host.capabilities],
|
|
1158
|
+
...host.operations ? { operations: [...host.operations] } : {}
|
|
1159
|
+
})));
|
|
1160
|
+
}
|
|
1161
|
+
function renderFrameworkExtensionPointsMarkdown() {
|
|
1162
|
+
const rows = getFrameworkExtensionHosts().map(
|
|
1163
|
+
(host) => `| ${host.id} | ${host.family} | ${[...host.capabilities, ...host.operations ?? []].join(", ")} | ${host.stability.toUpperCase()} |`
|
|
1164
|
+
);
|
|
1165
|
+
return [
|
|
1166
|
+
"# Framework extension points (generated, do not edit)",
|
|
1167
|
+
"",
|
|
1168
|
+
"| ID / pattern | Family | Supports | Stability |",
|
|
1169
|
+
"|---|---|---|---|",
|
|
1170
|
+
...rows,
|
|
1171
|
+
""
|
|
1172
|
+
].join("\n");
|
|
1173
|
+
}
|
|
1174
|
+
export {
|
|
1175
|
+
assertNoUnresolvedExtensionTargets,
|
|
1176
|
+
correlateExtensionTarget,
|
|
1177
|
+
correlateModuleExtensionFacts,
|
|
1178
|
+
extractKnownApiRouteIds,
|
|
1179
|
+
extractKnownCommandIds,
|
|
1180
|
+
extractModuleExtensionFacts,
|
|
1181
|
+
getFrameworkExtensionHosts,
|
|
1182
|
+
readConventionObjectArray,
|
|
1183
|
+
renderFrameworkExtensionPointsMarkdown
|
|
1184
|
+
};
|
|
1185
|
+
//# sourceMappingURL=module-extension-facts.js.map
|