@happyvertical/smrt-scanner 0.55.1 → 0.55.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/AGENTS.md +31 -0
- package/dist/chunks/{scanner-ItT4WTxN.js → scanner-CeTWAIar.js} +1013 -18
- package/dist/chunks/scanner-CeTWAIar.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/discovery.d.ts +1 -1
- package/dist/enum-values.d.ts +23 -0
- package/dist/enum-values.d.ts.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/manifest-adapter.d.ts +31 -1
- package/dist/manifest-adapter.d.ts.map +1 -1
- package/dist/oxc-parser.d.ts +14 -1
- package/dist/oxc-parser.d.ts.map +1 -1
- package/dist/recipes.d.ts +105 -0
- package/dist/recipes.d.ts.map +1 -0
- package/dist/scanner.d.ts +15 -0
- package/dist/scanner.d.ts.map +1 -1
- package/dist/types.d.ts +33 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui-selectors.d.ts +30 -0
- package/dist/ui-selectors.d.ts.map +1 -0
- package/package.json +2 -2
- package/dist/chunks/scanner-ItT4WTxN.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import { isAbsolute, relative, resolve, sep, win32 } from "node:path";
|
|
2
|
+
import { dirname, isAbsolute, relative, resolve, sep, win32 } from "node:path";
|
|
3
3
|
import fg from "fast-glob";
|
|
4
4
|
import { parseSync } from "oxc-parser";
|
|
5
5
|
//#region src/source-location.ts
|
|
@@ -759,6 +759,87 @@ function normalizeGlobSeparators(pattern, pathSeparator = sep) {
|
|
|
759
759
|
return pathSeparator === "/" ? pattern : pattern.replaceAll(pathSeparator, "/");
|
|
760
760
|
}
|
|
761
761
|
//#endregion
|
|
762
|
+
//#region src/enum-values.ts
|
|
763
|
+
var MAX_ALIAS_DEPTH = 8;
|
|
764
|
+
function splitUnion(type) {
|
|
765
|
+
const parts = [];
|
|
766
|
+
let current = "";
|
|
767
|
+
let quote = null;
|
|
768
|
+
let escaped = false;
|
|
769
|
+
for (const ch of type) {
|
|
770
|
+
if (quote) {
|
|
771
|
+
current += ch;
|
|
772
|
+
if (escaped) escaped = false;
|
|
773
|
+
else if (ch === "\\") escaped = true;
|
|
774
|
+
else if (ch === quote) quote = null;
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
if (ch === "'" || ch === "\"") {
|
|
778
|
+
quote = ch;
|
|
779
|
+
current += ch;
|
|
780
|
+
} else if (ch === "|") {
|
|
781
|
+
parts.push(current.trim());
|
|
782
|
+
current = "";
|
|
783
|
+
} else current += ch;
|
|
784
|
+
}
|
|
785
|
+
parts.push(current.trim());
|
|
786
|
+
return parts.filter((part) => part !== "");
|
|
787
|
+
}
|
|
788
|
+
function resolveMember(member, aliases, depth, out) {
|
|
789
|
+
const quoted = member.match(/^(['"])(.*)\1$/s);
|
|
790
|
+
if (quoted) try {
|
|
791
|
+
let encoded = member;
|
|
792
|
+
if (quoted[1] === "'") {
|
|
793
|
+
encoded = "\"";
|
|
794
|
+
for (let index = 0; index < quoted[2].length; index += 1) {
|
|
795
|
+
const char = quoted[2][index];
|
|
796
|
+
if (char === "\\" && quoted[2][index + 1]) {
|
|
797
|
+
const escapedChar = quoted[2][index + 1];
|
|
798
|
+
encoded += escapedChar === "'" ? "'" : `\\${escapedChar}`;
|
|
799
|
+
index += 1;
|
|
800
|
+
} else encoded += char === "\"" ? "\\\"" : char;
|
|
801
|
+
}
|
|
802
|
+
encoded += "\"";
|
|
803
|
+
}
|
|
804
|
+
const value = JSON.parse(encoded);
|
|
805
|
+
if (typeof value !== "string") return false;
|
|
806
|
+
out.push(value);
|
|
807
|
+
return true;
|
|
808
|
+
} catch {
|
|
809
|
+
return false;
|
|
810
|
+
}
|
|
811
|
+
if (/^-?\d+(\.\d+)?$/.test(member)) {
|
|
812
|
+
out.push(Number(member));
|
|
813
|
+
return true;
|
|
814
|
+
}
|
|
815
|
+
if (member === "null" || member === "undefined") return true;
|
|
816
|
+
if (depth < MAX_ALIAS_DEPTH && /^[A-Za-z_$][\w$]*$/.test(member) && Object.hasOwn(aliases, member)) {
|
|
817
|
+
const nested = collect$1(aliases[member], aliases, depth + 1);
|
|
818
|
+
if (!nested) return false;
|
|
819
|
+
out.push(...nested);
|
|
820
|
+
return true;
|
|
821
|
+
}
|
|
822
|
+
return false;
|
|
823
|
+
}
|
|
824
|
+
function collect$1(type, aliases, depth) {
|
|
825
|
+
const out = [];
|
|
826
|
+
for (const member of splitUnion(type)) if (!resolveMember(member, aliases, depth, out)) return void 0;
|
|
827
|
+
return out;
|
|
828
|
+
}
|
|
829
|
+
function resolveEnumValues(type, aliases = {}) {
|
|
830
|
+
if (!type) return void 0;
|
|
831
|
+
const values = collect$1(type.trim(), aliases, 0);
|
|
832
|
+
if (!values || values.length === 0) return void 0;
|
|
833
|
+
const seen = /* @__PURE__ */ new Set();
|
|
834
|
+
const unique = [];
|
|
835
|
+
for (const value of values) {
|
|
836
|
+
if (seen.has(value)) continue;
|
|
837
|
+
seen.add(value);
|
|
838
|
+
unique.push(value);
|
|
839
|
+
}
|
|
840
|
+
return unique;
|
|
841
|
+
}
|
|
842
|
+
//#endregion
|
|
762
843
|
//#region src/inheritance-resolver.ts
|
|
763
844
|
var FRAMEWORK_BASE_CLASSES = /* @__PURE__ */ new Set([
|
|
764
845
|
"SmrtObject",
|
|
@@ -1076,6 +1157,606 @@ var InheritanceResolver = class {
|
|
|
1076
1157
|
}
|
|
1077
1158
|
};
|
|
1078
1159
|
//#endregion
|
|
1160
|
+
//#region src/recipes.ts
|
|
1161
|
+
var RECIPE_BASE = "SmrtRecipe";
|
|
1162
|
+
var CORE_SPECIFIER = "@happyvertical/smrt-core";
|
|
1163
|
+
function isCoreSpecifier(source) {
|
|
1164
|
+
return source === CORE_SPECIFIER || source.startsWith(`${CORE_SPECIFIER}/`);
|
|
1165
|
+
}
|
|
1166
|
+
var RECIPE_ID_PATTERN = /^[a-z][a-z0-9]*(?:\.[a-z0-9][a-z0-9_-]*)+$/;
|
|
1167
|
+
var RECIPE_ID_MAX_LENGTH = 128;
|
|
1168
|
+
var FIELD_OPTION_KEYS = /* @__PURE__ */ new Set([
|
|
1169
|
+
"default",
|
|
1170
|
+
"label",
|
|
1171
|
+
"help",
|
|
1172
|
+
"order",
|
|
1173
|
+
"visibility",
|
|
1174
|
+
"locked"
|
|
1175
|
+
]);
|
|
1176
|
+
var VISIBILITIES = /* @__PURE__ */ new Set([
|
|
1177
|
+
"basic",
|
|
1178
|
+
"advanced",
|
|
1179
|
+
"hidden"
|
|
1180
|
+
]);
|
|
1181
|
+
var EXPOSURE_TRANSPORTS = [
|
|
1182
|
+
"api",
|
|
1183
|
+
"mcp",
|
|
1184
|
+
"cli"
|
|
1185
|
+
];
|
|
1186
|
+
function sourceMayDeclareRecipe(sourceText) {
|
|
1187
|
+
return sourceText.includes(RECIPE_BASE);
|
|
1188
|
+
}
|
|
1189
|
+
var RecipeReadError = class extends Error {
|
|
1190
|
+
constructor(message, node) {
|
|
1191
|
+
super(message);
|
|
1192
|
+
this.node = node;
|
|
1193
|
+
}
|
|
1194
|
+
node;
|
|
1195
|
+
};
|
|
1196
|
+
function collectImports(body) {
|
|
1197
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
1198
|
+
for (const node of body) {
|
|
1199
|
+
if (node.type !== "ImportDeclaration") continue;
|
|
1200
|
+
const source = node.source?.value;
|
|
1201
|
+
if (typeof source !== "string") continue;
|
|
1202
|
+
for (const spec of node.specifiers ?? []) {
|
|
1203
|
+
const local = spec.local?.name;
|
|
1204
|
+
if (typeof local !== "string") continue;
|
|
1205
|
+
if (spec.type === "ImportSpecifier") {
|
|
1206
|
+
const imported = spec.imported;
|
|
1207
|
+
const name = imported?.name ?? imported?.value;
|
|
1208
|
+
if (typeof name === "string") bindings.set(local, {
|
|
1209
|
+
imported: name,
|
|
1210
|
+
source,
|
|
1211
|
+
namespace: false
|
|
1212
|
+
});
|
|
1213
|
+
} else if (spec.type === "ImportDefaultSpecifier") bindings.set(local, {
|
|
1214
|
+
imported: local,
|
|
1215
|
+
source,
|
|
1216
|
+
namespace: false
|
|
1217
|
+
});
|
|
1218
|
+
else if (spec.type === "ImportNamespaceSpecifier") bindings.set(local, {
|
|
1219
|
+
imported: "*",
|
|
1220
|
+
source,
|
|
1221
|
+
namespace: true
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
return bindings;
|
|
1226
|
+
}
|
|
1227
|
+
function unwrap(node) {
|
|
1228
|
+
let current = node;
|
|
1229
|
+
for (let depth = 0; depth < 8; depth++) {
|
|
1230
|
+
if (current.type === "TSAsExpression" || current.type === "TSSatisfiesExpression" || current.type === "TSNonNullExpression" || current.type === "TSTypeAssertion" || current.type === "ParenthesizedExpression") {
|
|
1231
|
+
current = current.expression;
|
|
1232
|
+
continue;
|
|
1233
|
+
}
|
|
1234
|
+
break;
|
|
1235
|
+
}
|
|
1236
|
+
return current;
|
|
1237
|
+
}
|
|
1238
|
+
function isRecipeBase(superClass, imports) {
|
|
1239
|
+
if (!superClass) return false;
|
|
1240
|
+
const node = unwrap(superClass);
|
|
1241
|
+
if (node.type === "Identifier") {
|
|
1242
|
+
const binding = imports.get(node.name);
|
|
1243
|
+
return !!binding && binding.imported === RECIPE_BASE && isCoreSpecifier(binding.source);
|
|
1244
|
+
}
|
|
1245
|
+
if (node.type === "MemberExpression" && node.computed !== true) {
|
|
1246
|
+
const object = node.object;
|
|
1247
|
+
const property = node.property;
|
|
1248
|
+
const binding = object.type === "Identifier" ? imports.get(object.name) : void 0;
|
|
1249
|
+
return !!binding && binding.namespace && isCoreSpecifier(binding.source) && property.name === RECIPE_BASE;
|
|
1250
|
+
}
|
|
1251
|
+
return false;
|
|
1252
|
+
}
|
|
1253
|
+
function keyName(node) {
|
|
1254
|
+
if (node.computed === true) return null;
|
|
1255
|
+
const key = node.key;
|
|
1256
|
+
if (!key) return null;
|
|
1257
|
+
if (key.type === "Identifier") return key.name;
|
|
1258
|
+
if (key.type === "Literal" && typeof key.value === "string") return key.value;
|
|
1259
|
+
return null;
|
|
1260
|
+
}
|
|
1261
|
+
function stringValue(node, what) {
|
|
1262
|
+
const value = unwrap(node);
|
|
1263
|
+
if (value.type === "Literal" && typeof value.value === "string") return value.value;
|
|
1264
|
+
if (value.type === "TemplateLiteral" && value.expressions.length === 0) {
|
|
1265
|
+
const cooked = value.quasis[0]?.value?.cooked;
|
|
1266
|
+
if (typeof cooked === "string") return cooked;
|
|
1267
|
+
}
|
|
1268
|
+
throw new RecipeReadError(`${what} must be a string literal`, node);
|
|
1269
|
+
}
|
|
1270
|
+
function arrayElements(node, what) {
|
|
1271
|
+
const value = unwrap(node);
|
|
1272
|
+
if (value.type !== "ArrayExpression") throw new RecipeReadError(`${what} must be an array literal`, node);
|
|
1273
|
+
return value.elements.map((element) => {
|
|
1274
|
+
if (!element || element.type === "SpreadElement") throw new RecipeReadError(`${what} must list its entries literally (no spread or holes)`, element ?? node);
|
|
1275
|
+
return element;
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
function objectEntries(node, what) {
|
|
1279
|
+
const value = unwrap(node);
|
|
1280
|
+
if (value.type !== "ObjectExpression") throw new RecipeReadError(`${what} must be an object literal`, node);
|
|
1281
|
+
return value.properties.map((property) => {
|
|
1282
|
+
if (property.type !== "Property" || property.kind !== "init") throw new RecipeReadError(`${what} must use plain \`key: value\` properties (no spread, getters, or methods)`, property);
|
|
1283
|
+
if (property.method === true) throw new RecipeReadError(`${what} cannot contain methods`, property);
|
|
1284
|
+
const key = keyName(property);
|
|
1285
|
+
if (key === null) throw new RecipeReadError(`${what} cannot use computed property keys`, property);
|
|
1286
|
+
return {
|
|
1287
|
+
key,
|
|
1288
|
+
value: property.value,
|
|
1289
|
+
node: property
|
|
1290
|
+
};
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
function literalValue(node, what, depth = 0) {
|
|
1294
|
+
if (depth > 16) throw new RecipeReadError(`${what} is nested too deeply`, node);
|
|
1295
|
+
const value = unwrap(node);
|
|
1296
|
+
switch (value.type) {
|
|
1297
|
+
case "Literal":
|
|
1298
|
+
if (value.regex || typeof value.value === "bigint") break;
|
|
1299
|
+
return value.value;
|
|
1300
|
+
case "TemplateLiteral": return stringValue(value, what);
|
|
1301
|
+
case "UnaryExpression": {
|
|
1302
|
+
const argument = unwrap(value.argument);
|
|
1303
|
+
if ((value.operator === "-" || value.operator === "+") && argument.type === "Literal" && typeof argument.value === "number") return value.operator === "-" ? -argument.value : argument.value;
|
|
1304
|
+
break;
|
|
1305
|
+
}
|
|
1306
|
+
case "ArrayExpression": return arrayElements(value, what).map((element) => literalValue(element, what, depth + 1));
|
|
1307
|
+
case "ObjectExpression": {
|
|
1308
|
+
const out = {};
|
|
1309
|
+
for (const entry of objectEntries(value, what)) {
|
|
1310
|
+
if (isUnsafeKey(entry.key)) throw new RecipeReadError(`${what} cannot use the key \`${entry.key}\``, entry.node);
|
|
1311
|
+
out[entry.key] = literalValue(entry.value, what, depth + 1);
|
|
1312
|
+
}
|
|
1313
|
+
return out;
|
|
1314
|
+
}
|
|
1315
|
+
default: break;
|
|
1316
|
+
}
|
|
1317
|
+
throw new RecipeReadError(`${what} must be a literal value; the scanner cannot evaluate expressions`, node);
|
|
1318
|
+
}
|
|
1319
|
+
function isUnsafeKey(key) {
|
|
1320
|
+
return key === "__proto__" || key === "constructor" || key === "prototype";
|
|
1321
|
+
}
|
|
1322
|
+
function modelRef(node, imports, declaredHere, what, sourceText) {
|
|
1323
|
+
const value = unwrap(node);
|
|
1324
|
+
const line = lineOf(sourceText, value);
|
|
1325
|
+
if (value.type === "Identifier") {
|
|
1326
|
+
const local = value.name;
|
|
1327
|
+
const binding = imports.get(local);
|
|
1328
|
+
if (binding && !binding.namespace) return {
|
|
1329
|
+
local,
|
|
1330
|
+
imported: binding.imported,
|
|
1331
|
+
source: binding.source,
|
|
1332
|
+
line
|
|
1333
|
+
};
|
|
1334
|
+
if (declaredHere.has(local)) return {
|
|
1335
|
+
local,
|
|
1336
|
+
imported: local,
|
|
1337
|
+
source: null,
|
|
1338
|
+
line
|
|
1339
|
+
};
|
|
1340
|
+
throw new RecipeReadError(`${what} \`${local}\` is neither imported nor declared in this file`, node);
|
|
1341
|
+
}
|
|
1342
|
+
if (value.type === "MemberExpression" && value.computed !== true && value.object.type === "Identifier") {
|
|
1343
|
+
const object = value.object.name;
|
|
1344
|
+
const binding = imports.get(object);
|
|
1345
|
+
const property = value.property.name;
|
|
1346
|
+
if (binding?.namespace && typeof property === "string") return {
|
|
1347
|
+
local: `${object}.${property}`,
|
|
1348
|
+
imported: property,
|
|
1349
|
+
source: binding.source,
|
|
1350
|
+
line
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1353
|
+
throw new RecipeReadError(`${what} must be a class reference (an imported or same-file class identifier), not an expression or a string`, node);
|
|
1354
|
+
}
|
|
1355
|
+
function lineOf(sourceText, node) {
|
|
1356
|
+
const start = node.start;
|
|
1357
|
+
return typeof start === "number" ? getLineColumn(sourceText, start)?.line : void 0;
|
|
1358
|
+
}
|
|
1359
|
+
function extractRecipes(input) {
|
|
1360
|
+
const body = input.body;
|
|
1361
|
+
const imports = collectImports(body);
|
|
1362
|
+
const recipes = [];
|
|
1363
|
+
const errors = [];
|
|
1364
|
+
const classes = [];
|
|
1365
|
+
const declaredHere = /* @__PURE__ */ new Set();
|
|
1366
|
+
for (const statement of body) {
|
|
1367
|
+
let node = statement;
|
|
1368
|
+
if ((node.type === "ExportNamedDeclaration" || node.type === "ExportDefaultDeclaration") && node.declaration) node = node.declaration;
|
|
1369
|
+
if (node.type === "ClassDeclaration") {
|
|
1370
|
+
classes.push(node);
|
|
1371
|
+
const name = node.id?.name;
|
|
1372
|
+
if (typeof name === "string") declaredHere.add(name);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
const recipeNames = /* @__PURE__ */ new Set();
|
|
1376
|
+
for (const node of classes) if (isRecipeBase(node.superClass, imports)) recipeNames.add(node.id?.name ?? "");
|
|
1377
|
+
const reportAt = (message, at) => {
|
|
1378
|
+
const loc = typeof at.start === "number" ? getLineColumn(input.sourceText, at.start) : void 0;
|
|
1379
|
+
errors.push({
|
|
1380
|
+
message,
|
|
1381
|
+
filePath: input.filePath,
|
|
1382
|
+
line: loc?.line,
|
|
1383
|
+
column: loc?.column,
|
|
1384
|
+
severity: "error"
|
|
1385
|
+
});
|
|
1386
|
+
};
|
|
1387
|
+
for (const node of classes) {
|
|
1388
|
+
const base = node.superClass ? unwrap(node.superClass) : void 0;
|
|
1389
|
+
if (base?.type === "Identifier" && recipeNames.has(base.name) && !isRecipeBase(base, imports)) reportAt(`Recipe ${node.id?.name ?? "AnonymousRecipe"}: extends the recipe ${base.name}; a recipe must extend SmrtRecipe directly`, node);
|
|
1390
|
+
}
|
|
1391
|
+
for (const found of findClassExpressions(body)) if (isRecipeBase(found.superClass, imports)) reportAt("Recipe declared as a class expression or inside a block; declare it as a top-level `class X extends SmrtRecipe`", found);
|
|
1392
|
+
for (const node of classes) {
|
|
1393
|
+
if (!isRecipeBase(node.superClass, imports)) continue;
|
|
1394
|
+
const className = node.id?.name ?? "AnonymousRecipe";
|
|
1395
|
+
const report = (message, at) => {
|
|
1396
|
+
const loc = typeof at?.start === "number" ? getLineColumn(input.sourceText, at.start) : void 0;
|
|
1397
|
+
errors.push({
|
|
1398
|
+
message: `Recipe ${className}: ${message}`,
|
|
1399
|
+
filePath: input.filePath,
|
|
1400
|
+
line: loc?.line,
|
|
1401
|
+
column: loc?.column,
|
|
1402
|
+
severity: "error"
|
|
1403
|
+
});
|
|
1404
|
+
};
|
|
1405
|
+
const recipe = {
|
|
1406
|
+
className,
|
|
1407
|
+
filePath: input.filePath,
|
|
1408
|
+
line: lineOf(input.sourceText, node),
|
|
1409
|
+
id: null,
|
|
1410
|
+
label: null,
|
|
1411
|
+
summary: null,
|
|
1412
|
+
synonyms: [],
|
|
1413
|
+
models: [],
|
|
1414
|
+
nav: [],
|
|
1415
|
+
requires: [],
|
|
1416
|
+
options: [],
|
|
1417
|
+
help: null
|
|
1418
|
+
};
|
|
1419
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1420
|
+
for (const member of node.body.body) {
|
|
1421
|
+
if (member.type !== "PropertyDefinition" || member.static !== true) continue;
|
|
1422
|
+
const name = keyName(member);
|
|
1423
|
+
if (!name || !member.value) continue;
|
|
1424
|
+
const value = member.value;
|
|
1425
|
+
try {
|
|
1426
|
+
switch (name) {
|
|
1427
|
+
case "id":
|
|
1428
|
+
recipe.id = stringValue(value, "static id");
|
|
1429
|
+
break;
|
|
1430
|
+
case "label":
|
|
1431
|
+
recipe.label = stringValue(value, "static label");
|
|
1432
|
+
break;
|
|
1433
|
+
case "summary":
|
|
1434
|
+
recipe.summary = stringValue(value, "static summary");
|
|
1435
|
+
break;
|
|
1436
|
+
case "synonyms":
|
|
1437
|
+
recipe.synonyms = arrayElements(value, "static synonyms").map((entry) => stringValue(entry, "static synonyms entries"));
|
|
1438
|
+
break;
|
|
1439
|
+
case "requires":
|
|
1440
|
+
recipe.requires = arrayElements(value, "static requires").map((entry) => stringValue(entry, "static requires entries"));
|
|
1441
|
+
break;
|
|
1442
|
+
case "models":
|
|
1443
|
+
recipe.models = arrayElements(value, "static models").map((entry) => modelRef(entry, imports, declaredHere, "static models entry", input.sourceText));
|
|
1444
|
+
break;
|
|
1445
|
+
case "nav":
|
|
1446
|
+
recipe.nav = arrayElements(value, "static nav").map((entry) => {
|
|
1447
|
+
const fields = objectEntries(entry, "static nav entries");
|
|
1448
|
+
const label = fields.find((field) => field.key === "label");
|
|
1449
|
+
const model = fields.find((field) => field.key === "model");
|
|
1450
|
+
const extra = fields.find((field) => field.key !== "label" && field.key !== "model");
|
|
1451
|
+
if (extra) throw new RecipeReadError(`static nav entries accept only \`label\` and \`model\`, not \`${extra.key}\``, extra.node);
|
|
1452
|
+
if (!label || !model) throw new RecipeReadError("static nav entries need both `label` and `model`", entry);
|
|
1453
|
+
return {
|
|
1454
|
+
label: stringValue(label.value, "static nav label"),
|
|
1455
|
+
model: modelRef(model.value, imports, declaredHere, "static nav model", input.sourceText),
|
|
1456
|
+
line: lineOf(input.sourceText, entry)
|
|
1457
|
+
};
|
|
1458
|
+
});
|
|
1459
|
+
break;
|
|
1460
|
+
case "options":
|
|
1461
|
+
recipe.options = objectEntries(value, "static options").map((entry) => ({
|
|
1462
|
+
key: entry.key,
|
|
1463
|
+
value: literalValue(entry.value, "static options"),
|
|
1464
|
+
line: lineOf(input.sourceText, entry.node)
|
|
1465
|
+
}));
|
|
1466
|
+
break;
|
|
1467
|
+
case "help":
|
|
1468
|
+
recipe.help = {
|
|
1469
|
+
path: stringValue(value, "static help"),
|
|
1470
|
+
line: lineOf(input.sourceText, member)
|
|
1471
|
+
};
|
|
1472
|
+
break;
|
|
1473
|
+
default: continue;
|
|
1474
|
+
}
|
|
1475
|
+
seen.add(name);
|
|
1476
|
+
} catch (error) {
|
|
1477
|
+
if (!(error instanceof RecipeReadError)) throw error;
|
|
1478
|
+
report(error.message, error.node ?? member);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
for (const required of [
|
|
1482
|
+
"id",
|
|
1483
|
+
"label",
|
|
1484
|
+
"summary",
|
|
1485
|
+
"models"
|
|
1486
|
+
]) if (!seen.has(required)) report(`must declare a readable \`static ${required}\``, node.id);
|
|
1487
|
+
recipes.push(recipe);
|
|
1488
|
+
}
|
|
1489
|
+
return {
|
|
1490
|
+
recipes,
|
|
1491
|
+
errors
|
|
1492
|
+
};
|
|
1493
|
+
}
|
|
1494
|
+
var HELP_EXTENSION = ".md";
|
|
1495
|
+
var FIELD_REF_PATTERN = /\{field:([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)?)\}/g;
|
|
1496
|
+
function deriveHelpFieldRefs(markdown) {
|
|
1497
|
+
const refs = /* @__PURE__ */ new Set();
|
|
1498
|
+
for (const block of markdown.replace(/\r\n?/g, "\n").split(/\n\s*\n/)) for (const match of block.replace(/`[^`]+`/g, "").matchAll(FIELD_REF_PATTERN)) refs.add(match[1]);
|
|
1499
|
+
return [...refs].sort();
|
|
1500
|
+
}
|
|
1501
|
+
function readHelp(help, raw) {
|
|
1502
|
+
const fail = (message) => {
|
|
1503
|
+
throw new RecipeReadError(`static help ${message}`);
|
|
1504
|
+
};
|
|
1505
|
+
if (help.path.trim() === "" || isAbsolute(help.path) || help.path.split(/[\\/]/).includes("..") || !help.path.toLowerCase().endsWith(HELP_EXTENSION)) return fail(`must be a relative path to a ${HELP_EXTENSION} file beside the recipe (got \`${help.path}\`)`);
|
|
1506
|
+
const absolute = resolve(dirname(raw.filePath), help.path);
|
|
1507
|
+
let markdown;
|
|
1508
|
+
try {
|
|
1509
|
+
markdown = readFileSync(absolute, "utf-8");
|
|
1510
|
+
} catch (error) {
|
|
1511
|
+
return fail(`file \`${help.path}\` cannot be read (${error instanceof Error ? error.message : String(error)})`);
|
|
1512
|
+
}
|
|
1513
|
+
markdown = markdown.replace(/\r\n?/g, "\n");
|
|
1514
|
+
if (markdown.trim() === "") return fail(`file \`${help.path}\` is empty`);
|
|
1515
|
+
return {
|
|
1516
|
+
markdown,
|
|
1517
|
+
fieldRefs: deriveHelpFieldRefs(markdown)
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
function findClassExpressions(body) {
|
|
1521
|
+
const found = [];
|
|
1522
|
+
const topLevel = /* @__PURE__ */ new Set();
|
|
1523
|
+
for (const statement of body) {
|
|
1524
|
+
const node = (statement.type === "ExportNamedDeclaration" || statement.type === "ExportDefaultDeclaration") && statement.declaration ? statement.declaration : statement;
|
|
1525
|
+
if (node.type === "ClassDeclaration") topLevel.add(node);
|
|
1526
|
+
}
|
|
1527
|
+
const visit = (value, depth) => {
|
|
1528
|
+
if (depth > 200 || !value || typeof value !== "object") return;
|
|
1529
|
+
if (Array.isArray(value)) {
|
|
1530
|
+
for (const entry of value) visit(entry, depth + 1);
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
const node = value;
|
|
1534
|
+
if ((node.type === "ClassExpression" || node.type === "ClassDeclaration") && !topLevel.has(node)) found.push(node);
|
|
1535
|
+
for (const [key, child] of Object.entries(node)) {
|
|
1536
|
+
if (key === "loc" || key === "range") continue;
|
|
1537
|
+
visit(child, depth + 1);
|
|
1538
|
+
}
|
|
1539
|
+
};
|
|
1540
|
+
visit(body, 0);
|
|
1541
|
+
return found;
|
|
1542
|
+
}
|
|
1543
|
+
function stripExtension(path) {
|
|
1544
|
+
return path.replace(/\.(?:[cm]?[jt]sx?|svelte)$/, "");
|
|
1545
|
+
}
|
|
1546
|
+
function isRelativeSpecifier(source) {
|
|
1547
|
+
return source.startsWith("./") || source.startsWith("../");
|
|
1548
|
+
}
|
|
1549
|
+
function isExternalPackageSpecifier(source) {
|
|
1550
|
+
if (isRelativeSpecifier(source) || source.startsWith("/")) return false;
|
|
1551
|
+
if (/^[$~#]/.test(source) || source.startsWith("@/")) return false;
|
|
1552
|
+
return true;
|
|
1553
|
+
}
|
|
1554
|
+
function resolveModel(ref, recipe, classes, what) {
|
|
1555
|
+
const byName = classes.filter((candidate) => candidate.className === ref.imported);
|
|
1556
|
+
let candidates = byName;
|
|
1557
|
+
if (ref.source === null) candidates = byName.filter((c) => c.filePath === recipe.filePath);
|
|
1558
|
+
else if (isExternalPackageSpecifier(ref.source)) throw new RecipeReadError(`${what} \`${ref.local}\` is imported from \`${ref.source}\`, another package; a recipe's models must belong to its own package`);
|
|
1559
|
+
else if (isRelativeSpecifier(ref.source)) {
|
|
1560
|
+
const target = stripExtension(resolve(dirname(recipe.filePath), ref.source));
|
|
1561
|
+
const byFile = byName.filter((c) => {
|
|
1562
|
+
const file = stripExtension(c.filePath);
|
|
1563
|
+
return file === target || file === `${target}/index`;
|
|
1564
|
+
});
|
|
1565
|
+
candidates = byFile.length > 0 ? byFile : byName;
|
|
1566
|
+
}
|
|
1567
|
+
if (candidates.length === 0) throw new RecipeReadError(`${what} \`${ref.local}\` does not resolve to a model class scanned in this package`);
|
|
1568
|
+
if (candidates.length > 1) throw new RecipeReadError(`${what} \`${ref.local}\` is ambiguous: ${candidates.length} scanned classes are named \`${ref.imported}\``);
|
|
1569
|
+
return candidates[0];
|
|
1570
|
+
}
|
|
1571
|
+
var CRUD_VERBS = [
|
|
1572
|
+
"list",
|
|
1573
|
+
"get",
|
|
1574
|
+
"create",
|
|
1575
|
+
"update",
|
|
1576
|
+
"delete"
|
|
1577
|
+
];
|
|
1578
|
+
function isNarrowing(value, operations) {
|
|
1579
|
+
if (value === false) return true;
|
|
1580
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1581
|
+
const keys = Object.keys(value);
|
|
1582
|
+
const exclude = value.exclude;
|
|
1583
|
+
return keys.length === 1 && keys[0] === "exclude" && Array.isArray(exclude) && exclude.length > 0 && exclude.every((entry) => typeof entry === "string" && operations.has(entry));
|
|
1584
|
+
}
|
|
1585
|
+
function isPlainObject(value) {
|
|
1586
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
1587
|
+
}
|
|
1588
|
+
function readModelOptions(raw, model, classes, where, bad) {
|
|
1589
|
+
if (!isPlainObject(raw)) {
|
|
1590
|
+
bad(`${where} must be an object`);
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
let ok = true;
|
|
1594
|
+
const fail = (message) => {
|
|
1595
|
+
ok = false;
|
|
1596
|
+
bad(message);
|
|
1597
|
+
};
|
|
1598
|
+
const out = {};
|
|
1599
|
+
const operations = new Set(CRUD_VERBS);
|
|
1600
|
+
const chain = /* @__PURE__ */ new Set([model.className, ...model.inheritanceChain]);
|
|
1601
|
+
for (const candidate of classes) {
|
|
1602
|
+
if (!chain.has(candidate.className)) continue;
|
|
1603
|
+
for (const method of candidate.methods) if (!method.isStatic && method.accessibility === "public" && method.name !== "constructor") operations.add(method.name);
|
|
1604
|
+
}
|
|
1605
|
+
for (const [key, value] of Object.entries(raw)) if (key === "fields") {
|
|
1606
|
+
if (!isPlainObject(value)) {
|
|
1607
|
+
fail(`${where}.fields must be an object`);
|
|
1608
|
+
continue;
|
|
1609
|
+
}
|
|
1610
|
+
const fields = {};
|
|
1611
|
+
for (const [field, hints] of Object.entries(value)) {
|
|
1612
|
+
if (!isPlainObject(hints)) {
|
|
1613
|
+
fail(`${where}.fields.${field} must be an object`);
|
|
1614
|
+
continue;
|
|
1615
|
+
}
|
|
1616
|
+
const entry = {};
|
|
1617
|
+
for (const [hint, hintValue] of Object.entries(hints)) {
|
|
1618
|
+
const at = `${where}.fields.${field}.${hint}`;
|
|
1619
|
+
if (!FIELD_OPTION_KEYS.has(hint)) fail(`${at} is not a field option; use default, label, help, order, visibility, or locked`);
|
|
1620
|
+
else if ((hint === "label" || hint === "help") && typeof hintValue !== "string") fail(`${at} must be a string`);
|
|
1621
|
+
else if (hint === "order" && (typeof hintValue !== "number" || !Number.isFinite(hintValue))) fail(`${at} must be a finite number`);
|
|
1622
|
+
else if (hint === "locked" && typeof hintValue !== "boolean") fail(`${at} must be a boolean`);
|
|
1623
|
+
else if (hint === "visibility" && !(typeof hintValue === "string" && VISIBILITIES.has(hintValue))) fail(`${at} must be one of basic, advanced, hidden`);
|
|
1624
|
+
else entry[hint] = hintValue;
|
|
1625
|
+
}
|
|
1626
|
+
fields[field] = entry;
|
|
1627
|
+
}
|
|
1628
|
+
out.fields = fields;
|
|
1629
|
+
} else if (key === "exposure") {
|
|
1630
|
+
if (!isPlainObject(value)) {
|
|
1631
|
+
fail(`${where}.exposure must be an object`);
|
|
1632
|
+
continue;
|
|
1633
|
+
}
|
|
1634
|
+
const exposure = {};
|
|
1635
|
+
for (const [transport, narrowing] of Object.entries(value)) if (!EXPOSURE_TRANSPORTS.includes(transport)) fail(`${where}.exposure.${transport} is not a transport; use api, mcp, or cli`);
|
|
1636
|
+
else if (!isNarrowing(narrowing, operations)) fail(`${where}.exposure.${transport} may only be \`false\` or a non-empty \`{ exclude: [...] }\` naming ${[...CRUD_VERBS].join("/")} or a method of \`${model.className}\`: options narrow exposure and never widen it, so \`true\` and \`include\` are rejected`);
|
|
1637
|
+
else exposure[transport] = narrowing;
|
|
1638
|
+
out.exposure = exposure;
|
|
1639
|
+
} else fail(`${where}.${key} is not a model option; use \`fields\` or \`exposure\``);
|
|
1640
|
+
return ok ? out : void 0;
|
|
1641
|
+
}
|
|
1642
|
+
function resolveRecipes(raws, classes) {
|
|
1643
|
+
const errors = [];
|
|
1644
|
+
const recipes = [];
|
|
1645
|
+
const ids = /* @__PURE__ */ new Map();
|
|
1646
|
+
for (const raw of raws) {
|
|
1647
|
+
const report = (message, line) => errors.push({
|
|
1648
|
+
message: `Recipe ${raw.className}: ${message}`,
|
|
1649
|
+
filePath: raw.filePath,
|
|
1650
|
+
line: line ?? raw.line,
|
|
1651
|
+
severity: "error"
|
|
1652
|
+
});
|
|
1653
|
+
const attempt = (work, line) => {
|
|
1654
|
+
try {
|
|
1655
|
+
return work();
|
|
1656
|
+
} catch (error) {
|
|
1657
|
+
if (!(error instanceof RecipeReadError)) throw error;
|
|
1658
|
+
report(error.message, line);
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
};
|
|
1662
|
+
if (raw.id === null || raw.label === null || raw.summary === null) continue;
|
|
1663
|
+
const id = raw.id;
|
|
1664
|
+
if (id.length > RECIPE_ID_MAX_LENGTH || !RECIPE_ID_PATTERN.test(id)) report(`id \`${id}\` must be dotted lowercase segments such as \`commerce.sales\``);
|
|
1665
|
+
const first = ids.get(id);
|
|
1666
|
+
if (first) report(`id \`${id}\` is already declared by ${first.className} (${first.filePath})`);
|
|
1667
|
+
else ids.set(id, raw);
|
|
1668
|
+
if (raw.label.trim() === "") report("label must not be empty");
|
|
1669
|
+
if (raw.models.length === 0) report("models must list at least one model");
|
|
1670
|
+
const byLocal = /* @__PURE__ */ new Map();
|
|
1671
|
+
const models = [];
|
|
1672
|
+
for (const ref of raw.models) {
|
|
1673
|
+
const model = attempt(() => resolveModel(ref, raw, classes, "static models entry"), ref.line);
|
|
1674
|
+
if (!model) continue;
|
|
1675
|
+
if (models.includes(model)) {
|
|
1676
|
+
report(`models lists \`${model.className}\` more than once`, ref.line);
|
|
1677
|
+
continue;
|
|
1678
|
+
}
|
|
1679
|
+
models.push(model);
|
|
1680
|
+
byLocal.set(ref.local, model);
|
|
1681
|
+
byLocal.set(model.className, model);
|
|
1682
|
+
}
|
|
1683
|
+
const nav = [];
|
|
1684
|
+
for (const entry of raw.nav) {
|
|
1685
|
+
const model = attempt(() => resolveModel(entry.model, raw, classes, "static nav model"), entry.line);
|
|
1686
|
+
if (!model) continue;
|
|
1687
|
+
if (!models.includes(model)) {
|
|
1688
|
+
report(`nav entry \`${entry.label}\` points at \`${model.className}\`, which is not listed in models`, entry.line);
|
|
1689
|
+
continue;
|
|
1690
|
+
}
|
|
1691
|
+
nav.push({
|
|
1692
|
+
label: entry.label,
|
|
1693
|
+
model: model.className
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1696
|
+
const requires = [];
|
|
1697
|
+
for (const required of raw.requires) if (required.length > RECIPE_ID_MAX_LENGTH || !RECIPE_ID_PATTERN.test(required)) report(`requires \`${required}\` is not a recipe id`);
|
|
1698
|
+
else if (required === id) report("a recipe cannot require itself");
|
|
1699
|
+
else if (requires.includes(required)) report(`requires lists \`${required}\` more than once`);
|
|
1700
|
+
else requires.push(required);
|
|
1701
|
+
let options;
|
|
1702
|
+
for (const entry of raw.options) {
|
|
1703
|
+
const model = byLocal.get(entry.key);
|
|
1704
|
+
if (!model) {
|
|
1705
|
+
report(`options.${entry.key} does not name one of the recipe's models`, entry.line);
|
|
1706
|
+
continue;
|
|
1707
|
+
}
|
|
1708
|
+
const read = readModelOptions(entry.value, model, classes, `options.${entry.key}`, (message) => report(message, entry.line));
|
|
1709
|
+
if (read) {
|
|
1710
|
+
options ??= {};
|
|
1711
|
+
if (options[model.className]) report(`options names \`${model.className}\` more than once`, entry.line);
|
|
1712
|
+
else options[model.className] = read;
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
const help = raw.help ? attempt(() => readHelp(raw.help, raw), raw.help.line) : void 0;
|
|
1716
|
+
recipes.push({
|
|
1717
|
+
id,
|
|
1718
|
+
className: raw.className,
|
|
1719
|
+
label: raw.label,
|
|
1720
|
+
summary: raw.summary,
|
|
1721
|
+
synonyms: raw.synonyms,
|
|
1722
|
+
models: models.map((model) => model.className),
|
|
1723
|
+
nav,
|
|
1724
|
+
requires,
|
|
1725
|
+
...options && Object.keys(options).length > 0 ? { options } : {},
|
|
1726
|
+
...help ? { help } : {}
|
|
1727
|
+
});
|
|
1728
|
+
}
|
|
1729
|
+
const byId = new Map(recipes.map((recipe) => [recipe.id, recipe]));
|
|
1730
|
+
const reported = /* @__PURE__ */ new Set();
|
|
1731
|
+
const visit = (recipe, trail) => {
|
|
1732
|
+
if (trail.includes(recipe.id)) {
|
|
1733
|
+
const cycle = [...trail.slice(trail.indexOf(recipe.id)), recipe.id];
|
|
1734
|
+
const key = [...cycle].sort().join("|");
|
|
1735
|
+
if (!reported.has(key)) {
|
|
1736
|
+
reported.add(key);
|
|
1737
|
+
const raw = ids.get(recipe.id);
|
|
1738
|
+
errors.push({
|
|
1739
|
+
message: `Recipe ${recipe.className}: requires cycle ${cycle.join(" -> ")}`,
|
|
1740
|
+
filePath: raw?.filePath ?? "",
|
|
1741
|
+
line: raw?.line,
|
|
1742
|
+
severity: "error"
|
|
1743
|
+
});
|
|
1744
|
+
}
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
for (const required of recipe.requires) {
|
|
1748
|
+
const next = byId.get(required);
|
|
1749
|
+
if (next) visit(next, [...trail, recipe.id]);
|
|
1750
|
+
}
|
|
1751
|
+
};
|
|
1752
|
+
for (const recipe of recipes) visit(recipe, []);
|
|
1753
|
+
recipes.sort((a, b) => a.id.localeCompare(b.id));
|
|
1754
|
+
return {
|
|
1755
|
+
recipes,
|
|
1756
|
+
errors
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
//#endregion
|
|
1079
1760
|
//#region src/oxc-parser.ts
|
|
1080
1761
|
function getLangFromFilename(filename) {
|
|
1081
1762
|
if (filename.endsWith(".tsx")) return "tsx";
|
|
@@ -1099,6 +1780,7 @@ function parseFile(filePath) {
|
|
|
1099
1780
|
let typeAliases = {};
|
|
1100
1781
|
let smrtImports;
|
|
1101
1782
|
let agentSurface;
|
|
1783
|
+
let recipes;
|
|
1102
1784
|
try {
|
|
1103
1785
|
const sourceText = readFileSync(filePath, "utf-8");
|
|
1104
1786
|
const result = parseSync(filePath, sourceText, {
|
|
@@ -1131,6 +1813,15 @@ function parseFile(filePath) {
|
|
|
1131
1813
|
}
|
|
1132
1814
|
reportUnresolvedSpreads(ctx.unresolved, filePath, sourceText, errors);
|
|
1133
1815
|
agentSurface = maybeExtractAgentSurface(program, sourceText, filePath);
|
|
1816
|
+
if (sourceMayDeclareRecipe(sourceText)) {
|
|
1817
|
+
const found = extractRecipes({
|
|
1818
|
+
body: program.body,
|
|
1819
|
+
sourceText,
|
|
1820
|
+
filePath
|
|
1821
|
+
});
|
|
1822
|
+
errors.push(...found.errors);
|
|
1823
|
+
if (found.recipes.length > 0) recipes = found.recipes;
|
|
1824
|
+
}
|
|
1134
1825
|
}
|
|
1135
1826
|
} catch (error) {
|
|
1136
1827
|
errors.push({
|
|
@@ -1148,6 +1839,7 @@ function parseFile(filePath) {
|
|
|
1148
1839
|
};
|
|
1149
1840
|
if (smrtImports && smrtImports.size > 0) result2.smrtImports = smrtImports;
|
|
1150
1841
|
if (agentSurface) result2.agentSurface = agentSurface;
|
|
1842
|
+
if (recipes) result2.recipes = recipes;
|
|
1151
1843
|
return result2;
|
|
1152
1844
|
}
|
|
1153
1845
|
function maybeExtractAgentSurface(program, sourceText, filePath) {
|
|
@@ -1178,6 +1870,49 @@ function parseAgentSurfaceFile(filePath) {
|
|
|
1178
1870
|
return;
|
|
1179
1871
|
}
|
|
1180
1872
|
}
|
|
1873
|
+
function parseRecipeFile(filePath) {
|
|
1874
|
+
let sourceText;
|
|
1875
|
+
try {
|
|
1876
|
+
sourceText = readFileSync(filePath, "utf-8");
|
|
1877
|
+
} catch (error) {
|
|
1878
|
+
return {
|
|
1879
|
+
recipes: [],
|
|
1880
|
+
errors: [{
|
|
1881
|
+
message: `Cannot read file for recipe discovery: ${error instanceof Error ? error.message : String(error)}`,
|
|
1882
|
+
filePath,
|
|
1883
|
+
severity: "error"
|
|
1884
|
+
}]
|
|
1885
|
+
};
|
|
1886
|
+
}
|
|
1887
|
+
if (!sourceMayDeclareRecipe(sourceText)) return {
|
|
1888
|
+
recipes: [],
|
|
1889
|
+
errors: []
|
|
1890
|
+
};
|
|
1891
|
+
try {
|
|
1892
|
+
const program = parseSync(filePath, sourceText, {
|
|
1893
|
+
lang: getLangFromFilename(filePath),
|
|
1894
|
+
preserveParens: false
|
|
1895
|
+
}).program;
|
|
1896
|
+
if (!program?.body) return {
|
|
1897
|
+
recipes: [],
|
|
1898
|
+
errors: []
|
|
1899
|
+
};
|
|
1900
|
+
return extractRecipes({
|
|
1901
|
+
body: program.body,
|
|
1902
|
+
sourceText,
|
|
1903
|
+
filePath
|
|
1904
|
+
});
|
|
1905
|
+
} catch (error) {
|
|
1906
|
+
return {
|
|
1907
|
+
recipes: [],
|
|
1908
|
+
errors: [{
|
|
1909
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1910
|
+
filePath,
|
|
1911
|
+
severity: "error"
|
|
1912
|
+
}]
|
|
1913
|
+
};
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1181
1916
|
function parseSource(sourceText, filename = "test.ts") {
|
|
1182
1917
|
const startTime = performance.now();
|
|
1183
1918
|
const errors = [];
|
|
@@ -1185,6 +1920,7 @@ function parseSource(sourceText, filename = "test.ts") {
|
|
|
1185
1920
|
let typeAliases = {};
|
|
1186
1921
|
let smrtImports;
|
|
1187
1922
|
let agentSurface;
|
|
1923
|
+
let recipes;
|
|
1188
1924
|
try {
|
|
1189
1925
|
const result = parseSync(filename, sourceText, {
|
|
1190
1926
|
lang: getLangFromFilename(filename),
|
|
@@ -1216,6 +1952,15 @@ function parseSource(sourceText, filename = "test.ts") {
|
|
|
1216
1952
|
}
|
|
1217
1953
|
reportUnresolvedSpreads(ctx.unresolved, filename, sourceText, errors);
|
|
1218
1954
|
agentSurface = maybeExtractAgentSurface(program, sourceText, filename);
|
|
1955
|
+
if (sourceMayDeclareRecipe(sourceText)) {
|
|
1956
|
+
const found = extractRecipes({
|
|
1957
|
+
body: program.body,
|
|
1958
|
+
sourceText,
|
|
1959
|
+
filePath: filename
|
|
1960
|
+
});
|
|
1961
|
+
errors.push(...found.errors);
|
|
1962
|
+
if (found.recipes.length > 0) recipes = found.recipes;
|
|
1963
|
+
}
|
|
1219
1964
|
}
|
|
1220
1965
|
} catch (error) {
|
|
1221
1966
|
errors.push({
|
|
@@ -1233,6 +1978,7 @@ function parseSource(sourceText, filename = "test.ts") {
|
|
|
1233
1978
|
};
|
|
1234
1979
|
if (smrtImports && smrtImports.size > 0) result2.smrtImports = smrtImports;
|
|
1235
1980
|
if (agentSurface) result2.agentSurface = agentSurface;
|
|
1981
|
+
if (recipes) result2.recipes = recipes;
|
|
1236
1982
|
return result2;
|
|
1237
1983
|
}
|
|
1238
1984
|
var FORBIDDEN_OBJECT_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -1301,22 +2047,39 @@ function extractTypeAliases(body) {
|
|
|
1301
2047
|
const name = enumDecl.id?.name;
|
|
1302
2048
|
const members = enumDecl.body?.members ?? enumDecl.members;
|
|
1303
2049
|
if (name && isSafeObjectKey(name) && members && members.length > 0) {
|
|
1304
|
-
const
|
|
1305
|
-
|
|
1306
|
-
const val = m.initializer.value;
|
|
1307
|
-
if (typeof val === "string") return `'${val}'`;
|
|
1308
|
-
if (typeof val === "number") return String(val);
|
|
1309
|
-
}
|
|
1310
|
-
return null;
|
|
1311
|
-
}).filter((v) => v !== null);
|
|
1312
|
-
if (values.length > 0) {
|
|
1313
|
-
if (values.every((v) => v.startsWith("'"))) aliases[name] = values.join(" | ");
|
|
1314
|
-
}
|
|
2050
|
+
const union = enumMembersToUnion(members);
|
|
2051
|
+
if (union) aliases[name] = union;
|
|
1315
2052
|
}
|
|
1316
2053
|
}
|
|
1317
2054
|
}
|
|
1318
2055
|
return aliases;
|
|
1319
2056
|
}
|
|
2057
|
+
function enumMembersToUnion(members) {
|
|
2058
|
+
const values = [];
|
|
2059
|
+
let next = 0;
|
|
2060
|
+
for (const member of members) {
|
|
2061
|
+
const init = member.initializer;
|
|
2062
|
+
if (!init) {
|
|
2063
|
+
values.push(next);
|
|
2064
|
+
next += 1;
|
|
2065
|
+
} else if (init.type === "Literal" && typeof init.value === "string") values.push(init.value);
|
|
2066
|
+
else if (init.type === "Literal" && typeof init.value === "number") {
|
|
2067
|
+
values.push(init.value);
|
|
2068
|
+
next = init.value + 1;
|
|
2069
|
+
} else if (init.type === "UnaryExpression" && init.operator === "-" && init.argument?.type === "Literal" && typeof init.argument.value === "number") {
|
|
2070
|
+
values.push(-init.argument.value);
|
|
2071
|
+
next = -init.argument.value + 1;
|
|
2072
|
+
} else if (init.type === "TemplateLiteral" && init.expressions?.length === 0 && typeof init.quasis?.[0]?.value?.cooked === "string") values.push(init.quasis[0].value.cooked);
|
|
2073
|
+
else return null;
|
|
2074
|
+
}
|
|
2075
|
+
const allStrings = values.every((v) => typeof v === "string");
|
|
2076
|
+
const allNumbers = values.every((v) => typeof v === "number");
|
|
2077
|
+
if (!allStrings && !allNumbers) return null;
|
|
2078
|
+
return values.map((v) => {
|
|
2079
|
+
if (typeof v !== "string") return String(v);
|
|
2080
|
+
return `'${JSON.stringify(v).slice(1, -1).replaceAll("'", "\\'")}'`;
|
|
2081
|
+
}).join(" | ");
|
|
2082
|
+
}
|
|
1320
2083
|
function unwrapTypeAssertion(node) {
|
|
1321
2084
|
let current = node;
|
|
1322
2085
|
for (let depth = 0; current && depth < 8; depth++) {
|
|
@@ -2170,15 +2933,38 @@ var ManifestAdapter = class ManifestAdapter {
|
|
|
2170
2933
|
const manifestKey = definition.qualifiedName || definition.name.toLowerCase();
|
|
2171
2934
|
objects[manifestKey] = definition;
|
|
2172
2935
|
}
|
|
2936
|
+
const recipes = this.qualifyRecipes(options.recipes ?? [], options.packageName);
|
|
2937
|
+
const selectorEntries = Object.entries(options.uiSelectors ?? {}).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
2938
|
+
const selectors = selectorEntries.length > 0 ? Object.fromEntries(selectorEntries.map(([id, decl]) => [id, {
|
|
2939
|
+
slotId: decl.slotId,
|
|
2940
|
+
selects: decl.selects,
|
|
2941
|
+
...decl.label !== void 0 ? { label: decl.label } : {},
|
|
2942
|
+
...decl.description !== void 0 ? { description: decl.description } : {}
|
|
2943
|
+
}])) : void 0;
|
|
2173
2944
|
return {
|
|
2174
2945
|
version: "1.0.0",
|
|
2175
2946
|
timestamp: MANIFEST_TIMESTAMP,
|
|
2176
2947
|
packageName: options.packageName,
|
|
2177
2948
|
packageVersion: options.packageVersion,
|
|
2178
2949
|
objects,
|
|
2179
|
-
moduleType: "smrt"
|
|
2950
|
+
moduleType: "smrt",
|
|
2951
|
+
...recipes.length > 0 ? { recipes } : {},
|
|
2952
|
+
...selectors ? { uiSelectors: selectors } : {}
|
|
2180
2953
|
};
|
|
2181
2954
|
}
|
|
2955
|
+
/** Qualify recipe model names (`@scope/pkg:Class`) once the package is known. */
|
|
2956
|
+
qualifyRecipes(recipes, packageName) {
|
|
2957
|
+
const qualify = (className) => packageName ? createQualifiedName(packageName, className) : className;
|
|
2958
|
+
return recipes.map((recipe) => ({
|
|
2959
|
+
...recipe,
|
|
2960
|
+
models: recipe.models.map(qualify),
|
|
2961
|
+
nav: recipe.nav.map((entry) => ({
|
|
2962
|
+
label: entry.label,
|
|
2963
|
+
model: qualify(entry.model)
|
|
2964
|
+
})),
|
|
2965
|
+
...recipe.options ? { options: Object.fromEntries(Object.entries(recipe.options).map(([model, value]) => [qualify(model), value])) } : {}
|
|
2966
|
+
}));
|
|
2967
|
+
}
|
|
2182
2968
|
/**
|
|
2183
2969
|
* Convert a single resolved class definition to a `SmartObjectDefinition`.
|
|
2184
2970
|
*
|
|
@@ -2329,6 +3115,8 @@ var ManifestAdapter = class ManifestAdapter {
|
|
|
2329
3115
|
...definition._meta,
|
|
2330
3116
|
underlyingType: inference.underlyingType
|
|
2331
3117
|
};
|
|
3118
|
+
const enumValues = this.resolveFieldEnum(field, definition.type);
|
|
3119
|
+
if (enumValues) definition.enum = enumValues;
|
|
2332
3120
|
if (isFunctionType) definition.transient = true;
|
|
2333
3121
|
if (fieldDecoratorOptions.transient === true) definition.transient = true;
|
|
2334
3122
|
if (fieldDecoratorOptions.sensitive === true) definition.sensitive = true;
|
|
@@ -2725,6 +3513,19 @@ var ManifestAdapter = class ManifestAdapter {
|
|
|
2725
3513
|
this._aliasDepth = (this._aliasDepth ?? 0) - 1;
|
|
2726
3514
|
}
|
|
2727
3515
|
}
|
|
3516
|
+
const closedSet = type ? resolveEnumValues(type, this.typeAliases) : void 0;
|
|
3517
|
+
if (closedSet?.every((v) => typeof v === "string")) return {
|
|
3518
|
+
type: "text",
|
|
3519
|
+
required: isRequired,
|
|
3520
|
+
defaultValue: this.parseDefaultValue(field.initializer, "string"),
|
|
3521
|
+
source: "annotation"
|
|
3522
|
+
};
|
|
3523
|
+
if (closedSet?.every((v) => typeof v === "number")) return {
|
|
3524
|
+
type: closedSet.every(Number.isInteger) ? "integer" : "decimal",
|
|
3525
|
+
required: isRequired,
|
|
3526
|
+
defaultValue: field.numericValue ?? void 0,
|
|
3527
|
+
source: "annotation"
|
|
3528
|
+
};
|
|
2728
3529
|
if (field.initializer?.match(/^(['"]).*\1$/)) return {
|
|
2729
3530
|
type: "text",
|
|
2730
3531
|
required: isRequired,
|
|
@@ -2738,6 +3539,18 @@ var ManifestAdapter = class ManifestAdapter {
|
|
|
2738
3539
|
};
|
|
2739
3540
|
}
|
|
2740
3541
|
/**
|
|
3542
|
+
* Allowed values of a field whose declared type is a closed literal set.
|
|
3543
|
+
* Only emitted when the final column type agrees with the value kind, so a
|
|
3544
|
+
* `@field({ type })` override can never contradict its own `enum`.
|
|
3545
|
+
*/
|
|
3546
|
+
resolveFieldEnum(field, fieldType) {
|
|
3547
|
+
if (!field.typeAnnotation) return void 0;
|
|
3548
|
+
const values = resolveEnumValues(field.typeAnnotation, this.typeAliases);
|
|
3549
|
+
if (!values) return void 0;
|
|
3550
|
+
if (fieldType === "text") return values.every((v) => typeof v === "string") ? values : void 0;
|
|
3551
|
+
if (fieldType === "integer" || fieldType === "decimal") return values.every((v) => typeof v === "number") ? values : void 0;
|
|
3552
|
+
}
|
|
3553
|
+
/**
|
|
2741
3554
|
* Parse default value from initializer string
|
|
2742
3555
|
*/
|
|
2743
3556
|
parseDefaultValue(initializer, expectedType) {
|
|
@@ -2810,8 +3623,119 @@ var ManifestAdapter = class ManifestAdapter {
|
|
|
2810
3623
|
}
|
|
2811
3624
|
};
|
|
2812
3625
|
//#endregion
|
|
3626
|
+
//#region src/ui-selectors.ts
|
|
3627
|
+
var QUALIFIED_NAME = /^@[^/\s:]+\/[^/\s:]+:[A-Za-z_$][\w$]*$/;
|
|
3628
|
+
function ordinal(a, b) {
|
|
3629
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
3630
|
+
}
|
|
3631
|
+
function langOf(filePath) {
|
|
3632
|
+
if (filePath.endsWith(".tsx")) return "tsx";
|
|
3633
|
+
if (filePath.endsWith(".ts")) return "ts";
|
|
3634
|
+
if (filePath.endsWith(".jsx")) return "jsx";
|
|
3635
|
+
return "js";
|
|
3636
|
+
}
|
|
3637
|
+
function collect(value, fallbackId, filePath, out, errors, seen) {
|
|
3638
|
+
if (!value || typeof value !== "object" || seen.has(value)) return;
|
|
3639
|
+
seen.add(value);
|
|
3640
|
+
if (Array.isArray(value)) {
|
|
3641
|
+
for (const entry of value) collect(entry, void 0, filePath, out, errors, seen);
|
|
3642
|
+
return;
|
|
3643
|
+
}
|
|
3644
|
+
const record = value;
|
|
3645
|
+
if (Object.hasOwn(record, "selects")) {
|
|
3646
|
+
const slotId = typeof record.id === "string" && record.id !== "" ? record.id : fallbackId;
|
|
3647
|
+
if (typeof record.selects !== "string" || !QUALIFIED_NAME.test(record.selects)) errors.push({
|
|
3648
|
+
message: `UI slot "${slotId ?? "?"}" declares selects ${JSON.stringify(record.selects)}; it must be a string literal qualified model name ("@scope/package:ClassName").`,
|
|
3649
|
+
filePath,
|
|
3650
|
+
severity: "error"
|
|
3651
|
+
});
|
|
3652
|
+
else if (!slotId) errors.push({
|
|
3653
|
+
message: `A UI slot selecting ${record.selects} has no string \`id\`.`,
|
|
3654
|
+
filePath,
|
|
3655
|
+
severity: "error"
|
|
3656
|
+
});
|
|
3657
|
+
else out.push({
|
|
3658
|
+
slotId,
|
|
3659
|
+
selects: record.selects,
|
|
3660
|
+
...typeof record.label === "string" ? { label: record.label } : {},
|
|
3661
|
+
...typeof record.description === "string" ? { description: record.description } : {},
|
|
3662
|
+
filePath
|
|
3663
|
+
});
|
|
3664
|
+
}
|
|
3665
|
+
for (const [key, child] of Object.entries(record)) {
|
|
3666
|
+
if (key === "selects") continue;
|
|
3667
|
+
collect(child, key, filePath, out, errors, seen);
|
|
3668
|
+
}
|
|
3669
|
+
}
|
|
3670
|
+
function parseUiSelectorsFile(filePath) {
|
|
3671
|
+
const empty = {
|
|
3672
|
+
selectors: [],
|
|
3673
|
+
errors: []
|
|
3674
|
+
};
|
|
3675
|
+
let sourceText;
|
|
3676
|
+
try {
|
|
3677
|
+
sourceText = readFileSync(filePath, "utf-8");
|
|
3678
|
+
} catch {
|
|
3679
|
+
return empty;
|
|
3680
|
+
}
|
|
3681
|
+
if (!sourceText.includes("selects")) return empty;
|
|
3682
|
+
try {
|
|
3683
|
+
const program = parseSync(filePath, sourceText, {
|
|
3684
|
+
lang: langOf(filePath),
|
|
3685
|
+
preserveParens: false
|
|
3686
|
+
}).program;
|
|
3687
|
+
if (!program?.body) return empty;
|
|
3688
|
+
const constants = extractModuleObjectConstants(program.body, sourceText);
|
|
3689
|
+
const selectors = [];
|
|
3690
|
+
const errors = [];
|
|
3691
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3692
|
+
for (const [name, constant] of constants) collect(constant.value, name, filePath, selectors, errors, seen);
|
|
3693
|
+
return {
|
|
3694
|
+
selectors,
|
|
3695
|
+
errors
|
|
3696
|
+
};
|
|
3697
|
+
} catch {
|
|
3698
|
+
return empty;
|
|
3699
|
+
}
|
|
3700
|
+
}
|
|
3701
|
+
function mergeUiSelectors(files, relativize = (p) => p) {
|
|
3702
|
+
const errors = files.flatMap((file) => file.errors);
|
|
3703
|
+
const all = files.flatMap((file) => file.selectors).sort((a, b) => ordinal(a.slotId, b.slotId) || ordinal(a.filePath, b.filePath));
|
|
3704
|
+
const selectors = {};
|
|
3705
|
+
const byModel = /* @__PURE__ */ new Map();
|
|
3706
|
+
for (const decl of all) {
|
|
3707
|
+
if (selectors[decl.slotId]) {
|
|
3708
|
+
errors.push({
|
|
3709
|
+
message: `UI slot id "${decl.slotId}" is declared more than once.`,
|
|
3710
|
+
filePath: decl.filePath,
|
|
3711
|
+
severity: "error"
|
|
3712
|
+
});
|
|
3713
|
+
continue;
|
|
3714
|
+
}
|
|
3715
|
+
const other = byModel.get(decl.selects);
|
|
3716
|
+
if (other) {
|
|
3717
|
+
errors.push({
|
|
3718
|
+
message: `UI slots "${other}" and "${decl.slotId}" both select ${decl.selects}; a model has one selector.`,
|
|
3719
|
+
filePath: decl.filePath,
|
|
3720
|
+
severity: "error"
|
|
3721
|
+
});
|
|
3722
|
+
continue;
|
|
3723
|
+
}
|
|
3724
|
+
byModel.set(decl.selects, decl.slotId);
|
|
3725
|
+
selectors[decl.slotId] = {
|
|
3726
|
+
...decl,
|
|
3727
|
+
filePath: relativize(decl.filePath)
|
|
3728
|
+
};
|
|
3729
|
+
}
|
|
3730
|
+
return {
|
|
3731
|
+
selectors,
|
|
3732
|
+
errors
|
|
3733
|
+
};
|
|
3734
|
+
}
|
|
3735
|
+
//#endregion
|
|
2813
3736
|
//#region src/scanner.ts
|
|
2814
3737
|
var DEFAULT_INCLUDE = ["**/*.ts", "**/*.tsx"];
|
|
3738
|
+
var DEFAULT_UI_SELECTOR_INCLUDE = ["src/**/ui.ts"];
|
|
2815
3739
|
var DEFAULT_SVELTE_INCLUDE = ["**/*.svelte"];
|
|
2816
3740
|
var DEFAULT_AGENT_SURFACE_INCLUDE = [
|
|
2817
3741
|
"**/*.ts",
|
|
@@ -2839,6 +3763,8 @@ var OxcScanner = class {
|
|
|
2839
3763
|
options;
|
|
2840
3764
|
resolver;
|
|
2841
3765
|
scanResults = null;
|
|
3766
|
+
recipeErrors = [];
|
|
3767
|
+
outsideRecipes = [];
|
|
2842
3768
|
/**
|
|
2843
3769
|
* Create a new `OxcScanner` with the given options.
|
|
2844
3770
|
*
|
|
@@ -2862,7 +3788,8 @@ var OxcScanner = class {
|
|
|
2862
3788
|
followSymbolicLinks: options.followSymbolicLinks ?? false,
|
|
2863
3789
|
agentSurface: options.agentSurface ?? true,
|
|
2864
3790
|
svelteInclude: options.svelteInclude || DEFAULT_SVELTE_INCLUDE,
|
|
2865
|
-
agentSurfaceInclude: options.agentSurfaceInclude || DEFAULT_AGENT_SURFACE_INCLUDE
|
|
3791
|
+
agentSurfaceInclude: options.agentSurfaceInclude || DEFAULT_AGENT_SURFACE_INCLUDE,
|
|
3792
|
+
uiSelectorInclude: options.uiSelectorInclude || DEFAULT_UI_SELECTOR_INCLUDE
|
|
2866
3793
|
};
|
|
2867
3794
|
this.resolver = new InheritanceResolver({
|
|
2868
3795
|
baseClasses: this.options.baseClasses,
|
|
@@ -2899,7 +3826,8 @@ var OxcScanner = class {
|
|
|
2899
3826
|
totalParseTimeMs: performance.now() - startTime,
|
|
2900
3827
|
fileCount: files.length,
|
|
2901
3828
|
typeAliases: {},
|
|
2902
|
-
agentSurface: emptyAgentSurface()
|
|
3829
|
+
agentSurface: emptyAgentSurface(),
|
|
3830
|
+
recipes: []
|
|
2903
3831
|
};
|
|
2904
3832
|
const surfaces = [];
|
|
2905
3833
|
for (const file of fileResults) {
|
|
@@ -2913,6 +3841,14 @@ var OxcScanner = class {
|
|
|
2913
3841
|
surfaces.push(await this.scanSvelteDeclarations());
|
|
2914
3842
|
results.agentSurface = mergeAgentSurfaces(surfaces, (filePath) => this.relativizeSourcePath(filePath));
|
|
2915
3843
|
}
|
|
3844
|
+
const outside = await this.scanRecipesOutsideClassGlob(new Set(files));
|
|
3845
|
+
this.outsideRecipes = outside.recipes;
|
|
3846
|
+
results.errors.push(...outside.errors);
|
|
3847
|
+
const uiSelectors = await this.scanUiSelectors();
|
|
3848
|
+
if (uiSelectors) {
|
|
3849
|
+
results.uiSelectors = uiSelectors.selectors;
|
|
3850
|
+
results.errors.push(...uiSelectors.errors);
|
|
3851
|
+
}
|
|
2916
3852
|
this.resolver.addClasses(results.classes);
|
|
2917
3853
|
this.scanResults = results;
|
|
2918
3854
|
return results;
|
|
@@ -2934,7 +3870,13 @@ var OxcScanner = class {
|
|
|
2934
3870
|
*/
|
|
2935
3871
|
resolve() {
|
|
2936
3872
|
if (!this.scanResults) throw new Error("Must call scan() before resolve()");
|
|
2937
|
-
|
|
3873
|
+
const resolved = this.resolver.resolveAll();
|
|
3874
|
+
const { recipes, errors } = resolveRecipes([...this.scanResults.files.flatMap((file) => file.recipes ?? []), ...this.outsideRecipes], resolved);
|
|
3875
|
+
this.scanResults.recipes = recipes;
|
|
3876
|
+
this.scanResults.errors = this.scanResults.errors.filter((error) => !this.recipeErrors.includes(error));
|
|
3877
|
+
this.recipeErrors = errors;
|
|
3878
|
+
this.scanResults.errors.push(...errors);
|
|
3879
|
+
return resolved;
|
|
2938
3880
|
}
|
|
2939
3881
|
/**
|
|
2940
3882
|
* Run both scan phases in a single call.
|
|
@@ -3092,6 +4034,59 @@ var OxcScanner = class {
|
|
|
3092
4034
|
return surfaces;
|
|
3093
4035
|
}
|
|
3094
4036
|
/**
|
|
4037
|
+
* Find `SmrtRecipe` declarations in files the CLASS scan did not cover.
|
|
4038
|
+
*
|
|
4039
|
+
* Same rule as {@link scanDeclarationsOutsideClassGlob}: the model glob is
|
|
4040
|
+
* narrowed to where models live, a recipe need not live there, and a recipe
|
|
4041
|
+
* that vanished with no diagnostic is the failure to prevent.
|
|
4042
|
+
*/
|
|
4043
|
+
async scanRecipesOutsideClassGlob(alreadyScanned) {
|
|
4044
|
+
const out = {
|
|
4045
|
+
recipes: [],
|
|
4046
|
+
errors: []
|
|
4047
|
+
};
|
|
4048
|
+
if (this.options.agentSurfaceInclude.length === 0) return out;
|
|
4049
|
+
let files;
|
|
4050
|
+
try {
|
|
4051
|
+
files = await discoverSourceFiles({
|
|
4052
|
+
cwd: this.options.cwd,
|
|
4053
|
+
include: this.options.agentSurfaceInclude,
|
|
4054
|
+
exclude: [...this.options.exclude, ...AGENT_SURFACE_PRUNE],
|
|
4055
|
+
followSymbolicLinks: this.options.followSymbolicLinks
|
|
4056
|
+
});
|
|
4057
|
+
} catch {
|
|
4058
|
+
return out;
|
|
4059
|
+
}
|
|
4060
|
+
for (const filePath of files) {
|
|
4061
|
+
if (alreadyScanned.has(filePath)) continue;
|
|
4062
|
+
if (!isAgentSurfaceSourcePath(filePath, this.options.cwd)) continue;
|
|
4063
|
+
const found = parseRecipeFile(filePath);
|
|
4064
|
+
out.recipes.push(...found.recipes);
|
|
4065
|
+
out.errors.push(...found.errors);
|
|
4066
|
+
}
|
|
4067
|
+
return out;
|
|
4068
|
+
}
|
|
4069
|
+
/**
|
|
4070
|
+
* Read selector-slot declarations (`selects`, #3599) from the package's
|
|
4071
|
+
* `ui.ts` files, independently of the class-scan glob.
|
|
4072
|
+
*/
|
|
4073
|
+
async scanUiSelectors() {
|
|
4074
|
+
if (this.options.uiSelectorInclude.length === 0) return void 0;
|
|
4075
|
+
let files;
|
|
4076
|
+
try {
|
|
4077
|
+
files = await discoverSourceFiles({
|
|
4078
|
+
cwd: this.options.cwd,
|
|
4079
|
+
include: this.options.uiSelectorInclude,
|
|
4080
|
+
exclude: [...this.options.exclude, ...AGENT_SURFACE_PRUNE],
|
|
4081
|
+
followSymbolicLinks: this.options.followSymbolicLinks
|
|
4082
|
+
});
|
|
4083
|
+
} catch {
|
|
4084
|
+
return;
|
|
4085
|
+
}
|
|
4086
|
+
const merged = mergeUiSelectors(files.sort().map((file) => parseUiSelectorsFile(file)), (filePath) => this.relativizeSourcePath(filePath));
|
|
4087
|
+
return Object.keys(merged.selectors).length > 0 || merged.errors.length > 0 ? merged : void 0;
|
|
4088
|
+
}
|
|
4089
|
+
/**
|
|
3095
4090
|
* Search `.svelte` files for declarations the scanner can never read.
|
|
3096
4091
|
*
|
|
3097
4092
|
* A `.svelte` file is not a TypeScript program and is never parsed here, so
|
|
@@ -3141,6 +4136,6 @@ var OxcScanner = class {
|
|
|
3141
4136
|
}
|
|
3142
4137
|
};
|
|
3143
4138
|
//#endregion
|
|
3144
|
-
export {
|
|
4139
|
+
export { mergeAgentSurfaces as C, isPrunedAgentSurfacePath as S, sourceMayDeclareAgentSurface as T, relativeGlobToCwd as _, parseAgentSurfaceFile as a, extractAgentSurface as b, parseSource as c, resolveRecipes as d, sourceMayDeclareRecipe as f, normalizeGlobSeparators as g, discoverSourceFiles as h, extractSmrtImports as i, deriveHelpFieldRefs as l, resolveEnumValues as m, parseUiSelectorsFile as n, parseFile as o, InheritanceResolver as p, ManifestAdapter as r, parseRecipeFile as s, OxcScanner as t, extractRecipes as u, checkAgentSurfaceToolNames as v, scanSvelteAgentSurface as w, isAgentSurfaceSourcePath as x, emptyAgentSurface as y };
|
|
3145
4140
|
|
|
3146
|
-
//# sourceMappingURL=scanner-
|
|
4141
|
+
//# sourceMappingURL=scanner-CeTWAIar.js.map
|