@happyvertical/smrt-scanner 0.55.6 → 0.55.8
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/dist/chunks/{scanner-CeTWAIar.js → scanner-BbpPS8Aq.js} +794 -21
- package/dist/chunks/scanner-BbpPS8Aq.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/dist/manifest-adapter.d.ts +2 -0
- package/dist/manifest-adapter.d.ts.map +1 -1
- package/dist/oxc-parser.d.ts +10 -0
- package/dist/oxc-parser.d.ts.map +1 -1
- package/dist/recipe-widgets.d.ts +30 -0
- package/dist/recipe-widgets.d.ts.map +1 -0
- package/dist/recipes.d.ts +43 -7
- package/dist/recipes.d.ts.map +1 -1
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/dist/chunks/scanner-CeTWAIar.js.map +0 -1
|
@@ -78,7 +78,7 @@ var DATA_SURFACE_KINDS = /* @__PURE__ */ new Set([
|
|
|
78
78
|
]);
|
|
79
79
|
var PLAYBOOK_PLANES = /* @__PURE__ */ new Set(["browser", "server"]);
|
|
80
80
|
var FAILURE_POLICIES = /* @__PURE__ */ new Set(["abort", "continue"]);
|
|
81
|
-
var QUALIFIED_MODEL_PATTERN = /^\S+:\S+$/;
|
|
81
|
+
var QUALIFIED_MODEL_PATTERN$1 = /^\S+:\S+$/;
|
|
82
82
|
function intentToolName(id) {
|
|
83
83
|
return id.replace(/[.-]/g, "_");
|
|
84
84
|
}
|
|
@@ -424,7 +424,7 @@ function playbookDeclarationProblem(declaration, key, steps) {
|
|
|
424
424
|
if (enabled !== void 0 && typeof enabled !== "boolean") return `playbook '${key}' declares a non-boolean \`enabled\`, which \`definePlaybook\` rejects rather than coercing \u2014 a truthy '"false"' would otherwise read as enabled.`;
|
|
425
425
|
for (const step of declaration.steps ?? []) {
|
|
426
426
|
const record = step;
|
|
427
|
-
if (record.kind === "operation" && !QUALIFIED_MODEL_PATTERN.test(String(record.model))) return `playbook '${key}' has a step whose model '${String(record.model)}' is not a qualified pair such as '@happyvertical/smrt-commerce:Order'.`;
|
|
427
|
+
if (record.kind === "operation" && !QUALIFIED_MODEL_PATTERN$1.test(String(record.model))) return `playbook '${key}' has a step whose model '${String(record.model)}' is not a qualified pair such as '@happyvertical/smrt-commerce:Order'.`;
|
|
428
428
|
}
|
|
429
429
|
}
|
|
430
430
|
function intentIdentityProblem(id) {
|
|
@@ -1157,8 +1157,314 @@ var InheritanceResolver = class {
|
|
|
1157
1157
|
}
|
|
1158
1158
|
};
|
|
1159
1159
|
//#endregion
|
|
1160
|
+
//#region src/recipe-widgets.ts
|
|
1161
|
+
var RECIPE_WIDGET_OPTION_TYPES = [
|
|
1162
|
+
"text",
|
|
1163
|
+
"markdown",
|
|
1164
|
+
"identifier",
|
|
1165
|
+
"model",
|
|
1166
|
+
"integer",
|
|
1167
|
+
"number",
|
|
1168
|
+
"boolean",
|
|
1169
|
+
"enum"
|
|
1170
|
+
];
|
|
1171
|
+
var RECIPE_WIDGET_KEYS = [
|
|
1172
|
+
"kind",
|
|
1173
|
+
"type",
|
|
1174
|
+
"export",
|
|
1175
|
+
"label",
|
|
1176
|
+
"description",
|
|
1177
|
+
"icon",
|
|
1178
|
+
"version",
|
|
1179
|
+
"migrate",
|
|
1180
|
+
"options",
|
|
1181
|
+
"data",
|
|
1182
|
+
"allowedIn",
|
|
1183
|
+
"defaultSpan",
|
|
1184
|
+
"minSpan",
|
|
1185
|
+
"maxSpan"
|
|
1186
|
+
];
|
|
1187
|
+
var WIDGET_TYPE_PATTERN = /^[a-z][a-z0-9-]{0,31}$/;
|
|
1188
|
+
var OVERVIEW_PAGE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,95}$/;
|
|
1189
|
+
var OPTION_KEY_PATTERN = /^[A-Za-z][A-Za-z0-9]{0,31}$/;
|
|
1190
|
+
var IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_.-]{0,63}$/;
|
|
1191
|
+
var MODEL_PATTERN = /^(?:(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*:)?[A-Za-z][A-Za-z0-9_]{0,63}$/;
|
|
1192
|
+
var QUALIFIED_MODEL_PATTERN = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*:[A-Za-z][A-Za-z0-9_]{0,63}$/;
|
|
1193
|
+
var ICON_PATTERN$1 = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
1194
|
+
var MARKDOWN_MAX = 1e4;
|
|
1195
|
+
var OPTION_KEYS = /* @__PURE__ */ new Set([
|
|
1196
|
+
"key",
|
|
1197
|
+
"type",
|
|
1198
|
+
"label",
|
|
1199
|
+
"help",
|
|
1200
|
+
"required",
|
|
1201
|
+
"default",
|
|
1202
|
+
"min",
|
|
1203
|
+
"max",
|
|
1204
|
+
"maxLength",
|
|
1205
|
+
"choices"
|
|
1206
|
+
]);
|
|
1207
|
+
function isRecord(value) {
|
|
1208
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
1209
|
+
}
|
|
1210
|
+
function nonEmpty(value) {
|
|
1211
|
+
return typeof value === "string" && value.trim() !== "";
|
|
1212
|
+
}
|
|
1213
|
+
function readOption(raw, at, fail) {
|
|
1214
|
+
if (!isRecord(raw)) {
|
|
1215
|
+
fail(`${at} must be an object literal`);
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
let ok = true;
|
|
1219
|
+
const bad = (message) => {
|
|
1220
|
+
fail(`${at}${message}`);
|
|
1221
|
+
ok = false;
|
|
1222
|
+
};
|
|
1223
|
+
for (const key2 of Object.keys(raw)) if (!OPTION_KEYS.has(key2)) bad(` does not accept \`${key2}\``);
|
|
1224
|
+
const { key, type } = raw;
|
|
1225
|
+
if (typeof key !== "string" || !OPTION_KEY_PATTERN.test(key)) bad(".key must be a camelCase name of at most 32 letters and digits");
|
|
1226
|
+
if (typeof type !== "string" || !RECIPE_WIDGET_OPTION_TYPES.includes(type)) {
|
|
1227
|
+
bad(`.type must be one of ${RECIPE_WIDGET_OPTION_TYPES.join(", ")} (got \`${String(type)}\`)`);
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
if (!nonEmpty(raw.label)) bad(".label must be a non-empty string");
|
|
1231
|
+
if (raw.help !== void 0 && !nonEmpty(raw.help)) bad(".help must be a non-empty string");
|
|
1232
|
+
if (raw.required !== void 0 && typeof raw.required !== "boolean") bad(".required must be a boolean");
|
|
1233
|
+
const numeric = type === "integer" || type === "number";
|
|
1234
|
+
for (const bound of ["min", "max"]) {
|
|
1235
|
+
const value2 = raw[bound];
|
|
1236
|
+
if (value2 === void 0) continue;
|
|
1237
|
+
if (!numeric) bad(`.${bound} applies only to integer and number options`);
|
|
1238
|
+
else if (typeof value2 !== "number" || !Number.isFinite(value2)) bad(`.${bound} must be a finite number`);
|
|
1239
|
+
}
|
|
1240
|
+
if (typeof raw.min === "number" && typeof raw.max === "number" && raw.min > raw.max) bad(".min cannot exceed .max");
|
|
1241
|
+
const textual = type === "text" || type === "markdown";
|
|
1242
|
+
if (raw.maxLength !== void 0) {
|
|
1243
|
+
if (!textual) bad(".maxLength applies only to text and markdown options");
|
|
1244
|
+
else if (typeof raw.maxLength !== "number" || !Number.isSafeInteger(raw.maxLength) || raw.maxLength < 1) bad(".maxLength must be a positive integer");
|
|
1245
|
+
}
|
|
1246
|
+
let choiceValues;
|
|
1247
|
+
if (type === "enum") {
|
|
1248
|
+
const choices = raw.choices;
|
|
1249
|
+
if (!Array.isArray(choices) || choices.length === 0) bad(".choices must be a non-empty list of { value, label }");
|
|
1250
|
+
else {
|
|
1251
|
+
choiceValues = [];
|
|
1252
|
+
choices.forEach((choice, index) => {
|
|
1253
|
+
const where = `.choices[${index}]`;
|
|
1254
|
+
if (!isRecord(choice) || Object.keys(choice).some((k) => k !== "value" && k !== "label")) bad(`${where} must be { value, label } and nothing else`);
|
|
1255
|
+
else if (!nonEmpty(choice.value) || !nonEmpty(choice.label)) bad(`${where} needs a non-empty value and label`);
|
|
1256
|
+
else if (choiceValues?.includes(choice.value)) bad(`${where} repeats the value \`${choice.value}\``);
|
|
1257
|
+
else choiceValues?.push(choice.value);
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
} else if (raw.choices !== void 0) bad(".choices applies only to enum options");
|
|
1261
|
+
const value = raw.default;
|
|
1262
|
+
if (value !== void 0) {
|
|
1263
|
+
const mismatch = (what) => bad(`.default must be ${what}`);
|
|
1264
|
+
switch (type) {
|
|
1265
|
+
case "text":
|
|
1266
|
+
case "markdown": {
|
|
1267
|
+
const max = typeof raw.maxLength === "number" ? raw.maxLength : type === "markdown" ? MARKDOWN_MAX : 200;
|
|
1268
|
+
if (typeof value !== "string") mismatch("a string");
|
|
1269
|
+
else if (value.length > max) mismatch(`at most ${max} characters`);
|
|
1270
|
+
break;
|
|
1271
|
+
}
|
|
1272
|
+
case "identifier":
|
|
1273
|
+
if (typeof value !== "string" || !IDENTIFIER_PATTERN.test(value)) mismatch("an identifier (letters, digits, `_`, `.`, `-`)");
|
|
1274
|
+
break;
|
|
1275
|
+
case "model":
|
|
1276
|
+
if (typeof value !== "string" || value.length > 160 || !MODEL_PATTERN.test(value)) mismatch("a model name (`Class`, `pkg:Class` or `@scope/pkg:Class`)");
|
|
1277
|
+
break;
|
|
1278
|
+
case "enum":
|
|
1279
|
+
if (typeof value !== "string" || choiceValues && !choiceValues.includes(value)) mismatch("one of the choices");
|
|
1280
|
+
break;
|
|
1281
|
+
case "boolean":
|
|
1282
|
+
if (typeof value !== "boolean") mismatch("a boolean");
|
|
1283
|
+
break;
|
|
1284
|
+
default: if (typeof value !== "number" || !Number.isFinite(value)) mismatch("a finite number");
|
|
1285
|
+
else if (type === "integer" && !Number.isSafeInteger(value)) mismatch("a safe integer");
|
|
1286
|
+
else if (typeof raw.min === "number" && value < raw.min || typeof raw.max === "number" && value > raw.max) mismatch("within min and max");
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
if (!ok) return void 0;
|
|
1290
|
+
const out = {};
|
|
1291
|
+
for (const name of [
|
|
1292
|
+
"key",
|
|
1293
|
+
"type",
|
|
1294
|
+
"label",
|
|
1295
|
+
"help",
|
|
1296
|
+
"required",
|
|
1297
|
+
"default",
|
|
1298
|
+
"min",
|
|
1299
|
+
"max",
|
|
1300
|
+
"maxLength",
|
|
1301
|
+
"choices"
|
|
1302
|
+
]) if (raw[name] !== void 0) out[name] = raw[name];
|
|
1303
|
+
if (type === "enum") out.choices = raw.choices.map((choice) => ({
|
|
1304
|
+
value: choice.value,
|
|
1305
|
+
label: choice.label
|
|
1306
|
+
}));
|
|
1307
|
+
return out;
|
|
1308
|
+
}
|
|
1309
|
+
function readOptions(raw, at, fail) {
|
|
1310
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
1311
|
+
fail(`${at}.options must be a non-empty list of option objects`);
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
const out = [];
|
|
1315
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1316
|
+
let ok = true;
|
|
1317
|
+
raw.forEach((entry, index) => {
|
|
1318
|
+
const option = readOption(entry, `${at}.options[${index}]`, fail);
|
|
1319
|
+
if (!option) {
|
|
1320
|
+
ok = false;
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
if (keys.has(option.key)) {
|
|
1324
|
+
fail(`${at}.options[${index}].key \`${option.key}\` is already declared`);
|
|
1325
|
+
ok = false;
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
keys.add(option.key);
|
|
1329
|
+
out.push(option);
|
|
1330
|
+
});
|
|
1331
|
+
return ok ? out : void 0;
|
|
1332
|
+
}
|
|
1333
|
+
function readData(raw, at, fail, readRef) {
|
|
1334
|
+
if (!isRecord(raw)) {
|
|
1335
|
+
fail(`${at}.data must be an object literal`);
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
let ok = true;
|
|
1339
|
+
for (const key of Object.keys(raw)) if (key !== "load" && key !== "models") {
|
|
1340
|
+
fail(`${at}.data does not accept \`${key}\``);
|
|
1341
|
+
ok = false;
|
|
1342
|
+
}
|
|
1343
|
+
const out = {};
|
|
1344
|
+
if (raw.load !== void 0) {
|
|
1345
|
+
const ref = readRef(raw.load, `${at}.data`, "load");
|
|
1346
|
+
if (ref === void 0) ok = false;
|
|
1347
|
+
else out.load = ref;
|
|
1348
|
+
}
|
|
1349
|
+
if (raw.models !== void 0) {
|
|
1350
|
+
const models = raw.models;
|
|
1351
|
+
if (!Array.isArray(models) || models.length === 0) {
|
|
1352
|
+
fail(`${at}.data.models must be a non-empty list of qualified models`);
|
|
1353
|
+
ok = false;
|
|
1354
|
+
} else {
|
|
1355
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1356
|
+
for (const [index, model] of models.entries()) if (typeof model !== "string" || model.length > 160 || !QUALIFIED_MODEL_PATTERN.test(model)) {
|
|
1357
|
+
fail(`${at}.data.models[${index}] must be a qualified class name like \`@scope/pkg:Class\``);
|
|
1358
|
+
ok = false;
|
|
1359
|
+
} else if (seen.has(model)) {
|
|
1360
|
+
fail(`${at}.data.models[${index}] repeats \`${model}\``);
|
|
1361
|
+
ok = false;
|
|
1362
|
+
} else seen.add(model);
|
|
1363
|
+
out.models = [...seen];
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
if (ok && !out.load && !out.models) {
|
|
1367
|
+
fail(`${at}.data needs \`load\` or \`models\``);
|
|
1368
|
+
ok = false;
|
|
1369
|
+
}
|
|
1370
|
+
return ok ? out : void 0;
|
|
1371
|
+
}
|
|
1372
|
+
function readWidgetSurface(entry, at, fail, readRef) {
|
|
1373
|
+
let ok = true;
|
|
1374
|
+
const bad = (message) => {
|
|
1375
|
+
fail(`${at}${message}`);
|
|
1376
|
+
ok = false;
|
|
1377
|
+
};
|
|
1378
|
+
const out = {};
|
|
1379
|
+
const { type } = entry;
|
|
1380
|
+
if (typeof type !== "string" || !WIDGET_TYPE_PATTERN.test(type)) bad(".type must be a lowercase kebab id of at most 32 characters");
|
|
1381
|
+
else out.type = type;
|
|
1382
|
+
if (entry.description !== void 0) if (nonEmpty(entry.description)) out.description = entry.description;
|
|
1383
|
+
else bad(".description must be a non-empty string");
|
|
1384
|
+
if (entry.icon !== void 0) if (typeof entry.icon === "string" && ICON_PATTERN$1.test(entry.icon)) out.icon = entry.icon;
|
|
1385
|
+
else bad(".icon must be an icon name");
|
|
1386
|
+
let version = 1;
|
|
1387
|
+
if (entry.version !== void 0) if (typeof entry.version === "number" && Number.isSafeInteger(entry.version) && entry.version >= 1) {
|
|
1388
|
+
version = entry.version;
|
|
1389
|
+
out.version = entry.version;
|
|
1390
|
+
} else bad(".version must be a positive integer");
|
|
1391
|
+
if (entry.migrate !== void 0) {
|
|
1392
|
+
const ref = readRef(entry.migrate, at, "migrate");
|
|
1393
|
+
if (ref === void 0) ok = false;
|
|
1394
|
+
else if (version < 2) bad(".migrate needs a `version` above 1");
|
|
1395
|
+
else out.migrate = ref;
|
|
1396
|
+
}
|
|
1397
|
+
if (entry.options !== void 0) {
|
|
1398
|
+
const options = readOptions(entry.options, at, fail);
|
|
1399
|
+
if (options) out.options = options;
|
|
1400
|
+
else ok = false;
|
|
1401
|
+
}
|
|
1402
|
+
if (entry.data !== void 0) {
|
|
1403
|
+
const data = readData(entry.data, at, fail, readRef);
|
|
1404
|
+
if (data) out.data = data;
|
|
1405
|
+
else ok = false;
|
|
1406
|
+
}
|
|
1407
|
+
if (entry.allowedIn !== void 0) {
|
|
1408
|
+
const placements = entry.allowedIn;
|
|
1409
|
+
if (!Array.isArray(placements) || placements.length === 0) bad(".allowedIn must be a non-empty list of overview ids");
|
|
1410
|
+
else {
|
|
1411
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1412
|
+
placements.forEach((id, index) => {
|
|
1413
|
+
if (typeof id !== "string" || !OVERVIEW_PAGE_ID_PATTERN.test(id)) bad(`.allowedIn[${index}] must be an overview id like \`events.home\``);
|
|
1414
|
+
else if (seen.has(id)) bad(`.allowedIn[${index}] repeats \`${id}\``);
|
|
1415
|
+
else seen.add(id);
|
|
1416
|
+
});
|
|
1417
|
+
out.allowedIn = [...seen];
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
const spans = {
|
|
1421
|
+
minSpan: 1,
|
|
1422
|
+
defaultSpan: 1,
|
|
1423
|
+
maxSpan: 4
|
|
1424
|
+
};
|
|
1425
|
+
for (const name of [
|
|
1426
|
+
"defaultSpan",
|
|
1427
|
+
"minSpan",
|
|
1428
|
+
"maxSpan"
|
|
1429
|
+
]) {
|
|
1430
|
+
const value = entry[name];
|
|
1431
|
+
if (value === void 0) continue;
|
|
1432
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > 4) bad(`.${name} must be an integer from 1 to 4`);
|
|
1433
|
+
else {
|
|
1434
|
+
out[name] = value;
|
|
1435
|
+
spans[name] = value;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
if (ok && (spans.minSpan > spans.maxSpan || entry.defaultSpan !== void 0 && (spans.defaultSpan < spans.minSpan || spans.defaultSpan > spans.maxSpan))) bad(".minSpan <= .defaultSpan <= .maxSpan must hold");
|
|
1439
|
+
return ok ? out : void 0;
|
|
1440
|
+
}
|
|
1441
|
+
//#endregion
|
|
1160
1442
|
//#region src/recipes.ts
|
|
1161
1443
|
var RECIPE_BASE = "SmrtRecipe";
|
|
1444
|
+
var RECIPE_SHELL_SLOTS = [
|
|
1445
|
+
"header.start",
|
|
1446
|
+
"header.center",
|
|
1447
|
+
"header.end",
|
|
1448
|
+
"footer.start",
|
|
1449
|
+
"footer.center",
|
|
1450
|
+
"footer.end",
|
|
1451
|
+
"leftSidebar.header",
|
|
1452
|
+
"leftSidebar.footer",
|
|
1453
|
+
"rightSidebar.header",
|
|
1454
|
+
"rightSidebar.footer"
|
|
1455
|
+
];
|
|
1456
|
+
var RECIPE_SURFACE_KINDS = [
|
|
1457
|
+
"shell-widget",
|
|
1458
|
+
"route",
|
|
1459
|
+
"settings-panel",
|
|
1460
|
+
"playground",
|
|
1461
|
+
"widget"
|
|
1462
|
+
];
|
|
1463
|
+
var RECIPE_RUNTIMES = [
|
|
1464
|
+
"browser",
|
|
1465
|
+
"server",
|
|
1466
|
+
"both"
|
|
1467
|
+
];
|
|
1162
1468
|
var CORE_SPECIFIER = "@happyvertical/smrt-core";
|
|
1163
1469
|
function isCoreSpecifier(source) {
|
|
1164
1470
|
return source === CORE_SPECIFIER || source.startsWith(`${CORE_SPECIFIER}/`);
|
|
@@ -1171,8 +1477,28 @@ var FIELD_OPTION_KEYS = /* @__PURE__ */ new Set([
|
|
|
1171
1477
|
"help",
|
|
1172
1478
|
"order",
|
|
1173
1479
|
"visibility",
|
|
1174
|
-
"locked"
|
|
1480
|
+
"locked",
|
|
1481
|
+
"required"
|
|
1482
|
+
]);
|
|
1483
|
+
var NAV_EXTRA_KEYS = [
|
|
1484
|
+
"icon",
|
|
1485
|
+
"description",
|
|
1486
|
+
"key",
|
|
1487
|
+
"noun"
|
|
1488
|
+
];
|
|
1489
|
+
var GROUP_KEYS = /* @__PURE__ */ new Set([
|
|
1490
|
+
"id",
|
|
1491
|
+
"label",
|
|
1492
|
+
"summary"
|
|
1493
|
+
]);
|
|
1494
|
+
var SECTION_KEYS = /* @__PURE__ */ new Set([
|
|
1495
|
+
"id",
|
|
1496
|
+
"label",
|
|
1497
|
+
"icon",
|
|
1498
|
+
"description"
|
|
1175
1499
|
]);
|
|
1500
|
+
var SLUG_PATTERN = /^[a-z][a-z0-9_-]*$/;
|
|
1501
|
+
var ICON_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
1176
1502
|
var VISIBILITIES = /* @__PURE__ */ new Set([
|
|
1177
1503
|
"basic",
|
|
1178
1504
|
"advanced",
|
|
@@ -1413,8 +1739,15 @@ function extractRecipes(input) {
|
|
|
1413
1739
|
models: [],
|
|
1414
1740
|
nav: [],
|
|
1415
1741
|
requires: [],
|
|
1742
|
+
requiresAny: null,
|
|
1743
|
+
group: null,
|
|
1744
|
+
section: null,
|
|
1416
1745
|
options: [],
|
|
1417
|
-
help: null
|
|
1746
|
+
help: null,
|
|
1747
|
+
surfaces: null,
|
|
1748
|
+
providers: null,
|
|
1749
|
+
runtime: null,
|
|
1750
|
+
demoSeed: null
|
|
1418
1751
|
};
|
|
1419
1752
|
const seen = /* @__PURE__ */ new Set();
|
|
1420
1753
|
for (const member of node.body.body) {
|
|
@@ -1447,16 +1780,36 @@ function extractRecipes(input) {
|
|
|
1447
1780
|
const fields = objectEntries(entry, "static nav entries");
|
|
1448
1781
|
const label = fields.find((field) => field.key === "label");
|
|
1449
1782
|
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 \`
|
|
1783
|
+
const extra = fields.find((field) => field.key !== "label" && field.key !== "model" && field.key !== "filter" && !NAV_EXTRA_KEYS.includes(field.key));
|
|
1784
|
+
if (extra) throw new RecipeReadError(`static nav entries accept only \`label\`, \`model\`, \`icon\`, \`description\`, \`key\`, \`noun\` and \`filter\`, not \`${extra.key}\``, extra.node);
|
|
1785
|
+
const extras = {};
|
|
1786
|
+
for (const name2 of NAV_EXTRA_KEYS) {
|
|
1787
|
+
const found = fields.find((field) => field.key === name2);
|
|
1788
|
+
if (found) extras[name2] = stringValue(found.value, `static nav ${name2}`);
|
|
1789
|
+
}
|
|
1790
|
+
const filter = fields.find((field) => field.key === "filter");
|
|
1452
1791
|
if (!label || !model) throw new RecipeReadError("static nav entries need both `label` and `model`", entry);
|
|
1453
1792
|
return {
|
|
1454
1793
|
label: stringValue(label.value, "static nav label"),
|
|
1455
1794
|
model: modelRef(model.value, imports, declaredHere, "static nav model", input.sourceText),
|
|
1456
|
-
line: lineOf(input.sourceText, entry)
|
|
1795
|
+
line: lineOf(input.sourceText, entry),
|
|
1796
|
+
...extras,
|
|
1797
|
+
...filter ? { filter: literalValue(filter.value, "static nav filter") } : {}
|
|
1457
1798
|
};
|
|
1458
1799
|
});
|
|
1459
1800
|
break;
|
|
1801
|
+
case "requiresAny":
|
|
1802
|
+
case "group":
|
|
1803
|
+
case "section":
|
|
1804
|
+
case "surfaces":
|
|
1805
|
+
case "providers":
|
|
1806
|
+
case "runtime":
|
|
1807
|
+
case "demoSeed":
|
|
1808
|
+
recipe[name] = {
|
|
1809
|
+
value: literalValue(value, `static ${name}`),
|
|
1810
|
+
line: lineOf(input.sourceText, member)
|
|
1811
|
+
};
|
|
1812
|
+
break;
|
|
1460
1813
|
case "options":
|
|
1461
1814
|
recipe.options = objectEntries(value, "static options").map((entry) => ({
|
|
1462
1815
|
key: entry.key,
|
|
@@ -1616,10 +1969,10 @@ function readModelOptions(raw, model, classes, where, bad) {
|
|
|
1616
1969
|
const entry = {};
|
|
1617
1970
|
for (const [hint, hintValue] of Object.entries(hints)) {
|
|
1618
1971
|
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
|
|
1972
|
+
if (!FIELD_OPTION_KEYS.has(hint)) fail(`${at} is not a field option; use default, label, help, order, visibility, locked, or required`);
|
|
1620
1973
|
else if ((hint === "label" || hint === "help") && typeof hintValue !== "string") fail(`${at} must be a string`);
|
|
1621
1974
|
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`);
|
|
1975
|
+
else if ((hint === "locked" || hint === "required") && typeof hintValue !== "boolean") fail(`${at} must be a boolean`);
|
|
1623
1976
|
else if (hint === "visibility" && !(typeof hintValue === "string" && VISIBILITIES.has(hintValue))) fail(`${at} must be one of basic, advanced, hidden`);
|
|
1624
1977
|
else entry[hint] = hintValue;
|
|
1625
1978
|
}
|
|
@@ -1639,6 +1992,298 @@ function readModelOptions(raw, model, classes, where, bad) {
|
|
|
1639
1992
|
} else fail(`${where}.${key} is not a model option; use \`fields\` or \`exposure\``);
|
|
1640
1993
|
return ok ? out : void 0;
|
|
1641
1994
|
}
|
|
1995
|
+
function readNavFilter(value, fail) {
|
|
1996
|
+
if (value === void 0) return void 0;
|
|
1997
|
+
if (!isPlainObject(value)) {
|
|
1998
|
+
fail("filter must be an object `{ field, value }`");
|
|
1999
|
+
return;
|
|
2000
|
+
}
|
|
2001
|
+
const extra = Object.keys(value).find((k) => k !== "field" && k !== "value");
|
|
2002
|
+
if (extra) {
|
|
2003
|
+
fail(`filter accepts only \`field\` and \`value\`, not \`${extra}\``);
|
|
2004
|
+
return;
|
|
2005
|
+
}
|
|
2006
|
+
if (typeof value.field !== "string" || typeof value.value !== "string") {
|
|
2007
|
+
fail("filter needs a string `field` and a string `value`");
|
|
2008
|
+
return;
|
|
2009
|
+
}
|
|
2010
|
+
return {
|
|
2011
|
+
field: value.field,
|
|
2012
|
+
value: value.value
|
|
2013
|
+
};
|
|
2014
|
+
}
|
|
2015
|
+
function readRequiresAny(raw, id, report) {
|
|
2016
|
+
if (!raw.requiresAny) return void 0;
|
|
2017
|
+
const { value, line } = raw.requiresAny;
|
|
2018
|
+
if (!Array.isArray(value) || value.length === 0 || !value.every((alt) => Array.isArray(alt) && alt.length > 0)) {
|
|
2019
|
+
report("requiresAny must be a non-empty list of non-empty id lists, e.g. [['a.b', 'c.d']]", line);
|
|
2020
|
+
return;
|
|
2021
|
+
}
|
|
2022
|
+
const out = [];
|
|
2023
|
+
for (const alt of value) {
|
|
2024
|
+
const ids = [];
|
|
2025
|
+
for (const entry of alt) if (typeof entry !== "string" || entry.length > RECIPE_ID_MAX_LENGTH || !RECIPE_ID_PATTERN.test(entry)) report(`requiresAny entry \`${String(entry)}\` is not a recipe id`, line);
|
|
2026
|
+
else if (entry === id) report("a recipe cannot require itself", line);
|
|
2027
|
+
else if (ids.includes(entry)) report(`requiresAny lists \`${entry}\` more than once`, line);
|
|
2028
|
+
else ids.push(entry);
|
|
2029
|
+
out.push(ids);
|
|
2030
|
+
}
|
|
2031
|
+
return out;
|
|
2032
|
+
}
|
|
2033
|
+
function readLabelled(raw, name, allowed, report) {
|
|
2034
|
+
if (!raw) return void 0;
|
|
2035
|
+
const { value, line } = raw;
|
|
2036
|
+
if (!isPlainObject(value)) {
|
|
2037
|
+
report(`${name} must be an object literal`, line);
|
|
2038
|
+
return;
|
|
2039
|
+
}
|
|
2040
|
+
const out = {};
|
|
2041
|
+
let ok = true;
|
|
2042
|
+
for (const [key, entry] of Object.entries(value)) if (!allowed.has(key)) {
|
|
2043
|
+
report(`${name} accepts only ${[...allowed].join(", ")}, not \`${key}\``, line);
|
|
2044
|
+
ok = false;
|
|
2045
|
+
} else if (typeof entry !== "string" || entry.trim() === "") {
|
|
2046
|
+
report(`${name}.${key} must be a non-empty string`, line);
|
|
2047
|
+
ok = false;
|
|
2048
|
+
} else out[key] = entry;
|
|
2049
|
+
if (!out.id || !out.label) {
|
|
2050
|
+
if (ok) report(`${name} needs both \`id\` and \`label\``, line);
|
|
2051
|
+
return;
|
|
2052
|
+
}
|
|
2053
|
+
if (!SLUG_PATTERN.test(out.id)) {
|
|
2054
|
+
report(`${name}.id \`${out.id}\` must be lowercase letters, digits, \`-\` or \`_\``, line);
|
|
2055
|
+
return;
|
|
2056
|
+
}
|
|
2057
|
+
if (out.icon !== void 0 && !ICON_PATTERN.test(out.icon)) {
|
|
2058
|
+
report(`${name}.icon \`${out.icon}\` must be an icon name`, line);
|
|
2059
|
+
return;
|
|
2060
|
+
}
|
|
2061
|
+
return ok ? out : void 0;
|
|
2062
|
+
}
|
|
2063
|
+
var EXPORT_REF_PATTERN = /^((?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*(?:\/[A-Za-z0-9._-]+)*)#([A-Za-z_$][A-Za-z0-9_$]*)$/;
|
|
2064
|
+
var ROUTE_PATH_PATTERN = /^\/(?![/\\])[^\s?#\\]*$/;
|
|
2065
|
+
var PROVIDER_SLUG_PATTERN = /^[a-z][a-z0-9_-]*$/;
|
|
2066
|
+
var SECRET_NAME_PATTERN = /^[A-Z][A-Z0-9_]*$/;
|
|
2067
|
+
var DEMO_SEED_MAX_BYTES = 8192;
|
|
2068
|
+
function readExportRef(value, at, report, line, field = "export") {
|
|
2069
|
+
if (typeof value !== "string") {
|
|
2070
|
+
report(`${at}.${field} must be a string like \`pkg/svelte#Component\``, line);
|
|
2071
|
+
return;
|
|
2072
|
+
}
|
|
2073
|
+
const match = EXPORT_REF_PATTERN.exec(value);
|
|
2074
|
+
if (!match || match[1].split("/").some((part) => part === "." || part === ".." || part.endsWith("."))) {
|
|
2075
|
+
report(`${at}.${field} \`${value}\` must be \`<package specifier>#<ExportName>\` (a package specifier, not a relative path)`, line);
|
|
2076
|
+
return;
|
|
2077
|
+
}
|
|
2078
|
+
return value;
|
|
2079
|
+
}
|
|
2080
|
+
function readNonEmpty(value, at, report, line) {
|
|
2081
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
2082
|
+
report(`${at} must be a non-empty string`, line);
|
|
2083
|
+
return;
|
|
2084
|
+
}
|
|
2085
|
+
return value;
|
|
2086
|
+
}
|
|
2087
|
+
function readSurfaces(raw, report) {
|
|
2088
|
+
if (!raw) return void 0;
|
|
2089
|
+
const { value, line } = raw;
|
|
2090
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
2091
|
+
report("surfaces must be a non-empty array of surface objects", line);
|
|
2092
|
+
return;
|
|
2093
|
+
}
|
|
2094
|
+
const out = [];
|
|
2095
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2096
|
+
let ok = true;
|
|
2097
|
+
value.forEach((entry, index) => {
|
|
2098
|
+
const at = `surfaces[${index}]`;
|
|
2099
|
+
if (!isPlainObject(entry)) {
|
|
2100
|
+
report(`${at} must be an object literal`, line);
|
|
2101
|
+
ok = false;
|
|
2102
|
+
return;
|
|
2103
|
+
}
|
|
2104
|
+
const kind = entry.kind;
|
|
2105
|
+
if (typeof kind !== "string" || !RECIPE_SURFACE_KINDS.includes(kind)) {
|
|
2106
|
+
report(`${at}.kind must be one of ${RECIPE_SURFACE_KINDS.join(", ")} (got \`${String(kind)}\`)`, line);
|
|
2107
|
+
ok = false;
|
|
2108
|
+
return;
|
|
2109
|
+
}
|
|
2110
|
+
const allowed = {
|
|
2111
|
+
"shell-widget": [
|
|
2112
|
+
"kind",
|
|
2113
|
+
"slot",
|
|
2114
|
+
"export",
|
|
2115
|
+
"label",
|
|
2116
|
+
"icon"
|
|
2117
|
+
],
|
|
2118
|
+
route: [
|
|
2119
|
+
"kind",
|
|
2120
|
+
"path",
|
|
2121
|
+
"export",
|
|
2122
|
+
"label"
|
|
2123
|
+
],
|
|
2124
|
+
"settings-panel": [
|
|
2125
|
+
"kind",
|
|
2126
|
+
"export",
|
|
2127
|
+
"label"
|
|
2128
|
+
],
|
|
2129
|
+
playground: [
|
|
2130
|
+
"kind",
|
|
2131
|
+
"export",
|
|
2132
|
+
"label"
|
|
2133
|
+
],
|
|
2134
|
+
widget: [...RECIPE_WIDGET_KEYS]
|
|
2135
|
+
};
|
|
2136
|
+
for (const key of Object.keys(entry)) if (!allowed[kind].includes(key)) {
|
|
2137
|
+
report(`${at} (${kind}) does not accept \`${key}\``, line);
|
|
2138
|
+
ok = false;
|
|
2139
|
+
}
|
|
2140
|
+
const before = ok;
|
|
2141
|
+
const ref = readExportRef(entry.export, at, (m, l) => {
|
|
2142
|
+
report(m, l);
|
|
2143
|
+
ok = false;
|
|
2144
|
+
}, line);
|
|
2145
|
+
const fail = (message) => {
|
|
2146
|
+
report(message, line);
|
|
2147
|
+
ok = false;
|
|
2148
|
+
};
|
|
2149
|
+
let label;
|
|
2150
|
+
if (kind === "playground" && entry.label === void 0) label = void 0;
|
|
2151
|
+
else label = readNonEmpty(entry.label, `${at}.label`, fail, line);
|
|
2152
|
+
let identity = `${kind}:${ref}`;
|
|
2153
|
+
const surface = { kind };
|
|
2154
|
+
if (kind === "shell-widget") {
|
|
2155
|
+
const slot = entry.slot;
|
|
2156
|
+
if (typeof slot !== "string" || !RECIPE_SHELL_SLOTS.includes(slot)) fail(`${at}.slot \`${String(slot)}\` is not a shell slot (${RECIPE_SHELL_SLOTS.join(", ")})`);
|
|
2157
|
+
surface.slot = slot;
|
|
2158
|
+
if (entry.icon !== void 0) if (typeof entry.icon !== "string" || !ICON_PATTERN.test(entry.icon)) fail(`${at}.icon must be an icon name`);
|
|
2159
|
+
else surface.icon = entry.icon;
|
|
2160
|
+
identity = `${identity}@${String(slot)}`;
|
|
2161
|
+
} else if (kind === "route") {
|
|
2162
|
+
const path = entry.path;
|
|
2163
|
+
if (typeof path !== "string" || !ROUTE_PATH_PATTERN.test(path)) fail(`${at}.path must start with \`/\` and contain no whitespace, \`?\` or \`#\``);
|
|
2164
|
+
else if (/%2e|%2f|%5c/i.test(path) || path.slice(1).split("/").some((part, i, parts) => part === "." || part === ".." || part === "" && i < parts.length - 1)) fail(`${at}.path cannot contain \`.\`/\`..\` or empty segments, or encoded separators`);
|
|
2165
|
+
else {
|
|
2166
|
+
surface.path = path;
|
|
2167
|
+
identity = `route-path:${path}`;
|
|
2168
|
+
}
|
|
2169
|
+
} else if (kind === "widget") {
|
|
2170
|
+
const extras = readWidgetSurface(entry, at, fail, (value2, where, field) => readExportRef(value2, where, (m, l) => {
|
|
2171
|
+
report(m, l);
|
|
2172
|
+
ok = false;
|
|
2173
|
+
}, line, field));
|
|
2174
|
+
if (extras) Object.assign(surface, extras);
|
|
2175
|
+
if (typeof entry.type === "string") identity = `widget-type:${entry.type}`;
|
|
2176
|
+
}
|
|
2177
|
+
if (ref !== void 0) {
|
|
2178
|
+
if (seen.has(identity)) fail(`${at} repeats an earlier surface`);
|
|
2179
|
+
seen.add(identity);
|
|
2180
|
+
}
|
|
2181
|
+
if (ok === before && ref !== void 0) {
|
|
2182
|
+
surface.export = ref;
|
|
2183
|
+
if (label !== void 0) surface.label = label;
|
|
2184
|
+
const ordered = { kind };
|
|
2185
|
+
const keyOrder = kind === "widget" ? RECIPE_WIDGET_KEYS : [
|
|
2186
|
+
"slot",
|
|
2187
|
+
"path",
|
|
2188
|
+
"export",
|
|
2189
|
+
"label",
|
|
2190
|
+
"icon"
|
|
2191
|
+
];
|
|
2192
|
+
for (const key of keyOrder) if (surface[key] !== void 0) ordered[key] = surface[key];
|
|
2193
|
+
out.push(ordered);
|
|
2194
|
+
}
|
|
2195
|
+
});
|
|
2196
|
+
return ok ? out : void 0;
|
|
2197
|
+
}
|
|
2198
|
+
function readProviders(raw, report) {
|
|
2199
|
+
if (!raw) return void 0;
|
|
2200
|
+
const { value, line } = raw;
|
|
2201
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
2202
|
+
report("providers must be a non-empty array of provider objects", line);
|
|
2203
|
+
return;
|
|
2204
|
+
}
|
|
2205
|
+
const out = [];
|
|
2206
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2207
|
+
let ok = true;
|
|
2208
|
+
const fail = (message) => {
|
|
2209
|
+
report(message, line);
|
|
2210
|
+
ok = false;
|
|
2211
|
+
};
|
|
2212
|
+
value.forEach((entry, index) => {
|
|
2213
|
+
const at = `providers[${index}]`;
|
|
2214
|
+
if (!isPlainObject(entry)) {
|
|
2215
|
+
fail(`${at} must be an object literal`);
|
|
2216
|
+
return;
|
|
2217
|
+
}
|
|
2218
|
+
for (const key of Object.keys(entry)) if (![
|
|
2219
|
+
"id",
|
|
2220
|
+
"kind",
|
|
2221
|
+
"options",
|
|
2222
|
+
"required",
|
|
2223
|
+
"secrets",
|
|
2224
|
+
"browserOptions"
|
|
2225
|
+
].includes(key)) fail(`${at} does not accept \`${key}\``);
|
|
2226
|
+
const { id, kind, options, required, secrets, browserOptions } = entry;
|
|
2227
|
+
if (typeof id !== "string" || !PROVIDER_SLUG_PATTERN.test(id)) fail(`${at}.id must be a lowercase slug`);
|
|
2228
|
+
else if (ids.has(id)) fail(`${at}.id \`${id}\` is declared more than once`);
|
|
2229
|
+
else ids.add(id);
|
|
2230
|
+
if (typeof kind !== "string" || !PROVIDER_SLUG_PATTERN.test(kind)) fail(`${at}.kind must be a lowercase slug such as email, oauth, storage`);
|
|
2231
|
+
if (!Array.isArray(options) || options.length === 0 || !options.every((o) => typeof o === "string" && PROVIDER_SLUG_PATTERN.test(o)) || new Set(options).size !== options.length) fail(`${at}.options must be a non-empty list of distinct lowercase slugs`);
|
|
2232
|
+
if (typeof required !== "boolean") fail(`${at}.required must be written as true or false`);
|
|
2233
|
+
if (secrets !== void 0 && (!Array.isArray(secrets) || !secrets.every((n) => typeof n === "string" && SECRET_NAME_PATTERN.test(n)) || new Set(secrets).size !== secrets.length)) fail(`${at}.secrets must be distinct UPPER_SNAKE names (names, not values)`);
|
|
2234
|
+
if (browserOptions !== void 0 && (!Array.isArray(browserOptions) || browserOptions.length === 0 || new Set(browserOptions).size !== browserOptions.length || !browserOptions.every((o) => typeof o === "string" && Array.isArray(options) && options.includes(o)))) fail(`${at}.browserOptions must be a non-empty list of distinct entries of options`);
|
|
2235
|
+
if (ok) out.push({
|
|
2236
|
+
id,
|
|
2237
|
+
kind,
|
|
2238
|
+
options,
|
|
2239
|
+
required,
|
|
2240
|
+
...secrets ? { secrets } : {},
|
|
2241
|
+
...browserOptions ? { browserOptions } : {}
|
|
2242
|
+
});
|
|
2243
|
+
});
|
|
2244
|
+
return ok ? out : void 0;
|
|
2245
|
+
}
|
|
2246
|
+
function readRuntime(raw, report) {
|
|
2247
|
+
if (!raw) return void 0;
|
|
2248
|
+
const { value, line } = raw;
|
|
2249
|
+
if (typeof value !== "string" || !RECIPE_RUNTIMES.includes(value)) {
|
|
2250
|
+
report(`runtime must be one of ${RECIPE_RUNTIMES.join(", ")}`, line);
|
|
2251
|
+
return;
|
|
2252
|
+
}
|
|
2253
|
+
return value;
|
|
2254
|
+
}
|
|
2255
|
+
function readDemoSeed(raw, report) {
|
|
2256
|
+
if (!raw) return void 0;
|
|
2257
|
+
const { value, line } = raw;
|
|
2258
|
+
if (!isPlainObject(value)) {
|
|
2259
|
+
report("demoSeed must be { export } or { data }", line);
|
|
2260
|
+
return;
|
|
2261
|
+
}
|
|
2262
|
+
const keys = Object.keys(value);
|
|
2263
|
+
if (keys.length !== 1 || keys[0] !== "export" && keys[0] !== "data") {
|
|
2264
|
+
report("demoSeed must have exactly one of `export` or `data`", line);
|
|
2265
|
+
return;
|
|
2266
|
+
}
|
|
2267
|
+
if (keys[0] === "export") {
|
|
2268
|
+
const ref = readExportRef(value.export, "demoSeed", report, line);
|
|
2269
|
+
return ref ? { export: ref } : void 0;
|
|
2270
|
+
}
|
|
2271
|
+
const finite = (node) => typeof node === "number" ? Number.isFinite(node) : Array.isArray(node) ? node.every(finite) : isPlainObject(node) ? Object.values(node).every(finite) : true;
|
|
2272
|
+
if (!finite(value.data)) {
|
|
2273
|
+
report("demoSeed.data cannot contain non-finite numbers", line);
|
|
2274
|
+
return;
|
|
2275
|
+
}
|
|
2276
|
+
const size = Buffer.byteLength(JSON.stringify(value.data ?? null));
|
|
2277
|
+
if (value.data === void 0 || value.data === null) {
|
|
2278
|
+
report("demoSeed.data must be JSON data", line);
|
|
2279
|
+
return;
|
|
2280
|
+
}
|
|
2281
|
+
if (size > DEMO_SEED_MAX_BYTES) {
|
|
2282
|
+
report(`demoSeed.data is ${size} bytes; inline seeds are limited to ${DEMO_SEED_MAX_BYTES}, reference a fixture export instead`, line);
|
|
2283
|
+
return;
|
|
2284
|
+
}
|
|
2285
|
+
return { data: value.data };
|
|
2286
|
+
}
|
|
1642
2287
|
function resolveRecipes(raws, classes) {
|
|
1643
2288
|
const errors = [];
|
|
1644
2289
|
const recipes = [];
|
|
@@ -1688,16 +2333,35 @@ function resolveRecipes(raws, classes) {
|
|
|
1688
2333
|
report(`nav entry \`${entry.label}\` points at \`${model.className}\`, which is not listed in models`, entry.line);
|
|
1689
2334
|
continue;
|
|
1690
2335
|
}
|
|
2336
|
+
const filter = readNavFilter(entry.filter, (message) => report(`nav entry \`${entry.label}\`: ${message}`, entry.line));
|
|
2337
|
+
if (entry.key !== void 0 && !SLUG_PATTERN.test(entry.key)) report(`nav entry \`${entry.label}\`: key \`${entry.key}\` must be lowercase letters, digits, \`-\` or \`_\``, entry.line);
|
|
2338
|
+
if (entry.icon !== void 0 && !ICON_PATTERN.test(entry.icon)) report(`nav entry \`${entry.label}\`: icon \`${entry.icon}\` must be an icon name`, entry.line);
|
|
2339
|
+
for (const text of ["description", "noun"]) if (entry[text] !== void 0 && entry[text]?.trim() === "") report(`nav entry \`${entry.label}\`: ${text} must not be empty`, entry.line);
|
|
2340
|
+
if (filter && entry.key === void 0) report(`nav entry \`${entry.label}\`: a filter needs a key, which names its view`, entry.line);
|
|
1691
2341
|
nav.push({
|
|
1692
2342
|
label: entry.label,
|
|
1693
|
-
model: model.className
|
|
2343
|
+
model: model.className,
|
|
2344
|
+
...entry.icon !== void 0 ? { icon: entry.icon } : {},
|
|
2345
|
+
...entry.description !== void 0 ? { description: entry.description } : {},
|
|
2346
|
+
...entry.key !== void 0 ? { key: entry.key } : {},
|
|
2347
|
+
...entry.noun !== void 0 ? { noun: entry.noun } : {},
|
|
2348
|
+
...filter ? { filter } : {}
|
|
1694
2349
|
});
|
|
1695
2350
|
}
|
|
2351
|
+
const viewKeys = /* @__PURE__ */ new Set();
|
|
2352
|
+
for (const entry of nav) {
|
|
2353
|
+
const view = `${entry.model}:${entry.key ?? ""}`;
|
|
2354
|
+
if (viewKeys.has(view)) report(`nav entry \`${entry.label}\` repeats the view of ${entry.model}${entry.key ? ` key \`${entry.key}\`` : ""}; give each extra entry over a model its own key`);
|
|
2355
|
+
viewKeys.add(view);
|
|
2356
|
+
}
|
|
1696
2357
|
const requires = [];
|
|
1697
2358
|
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
2359
|
else if (required === id) report("a recipe cannot require itself");
|
|
1699
2360
|
else if (requires.includes(required)) report(`requires lists \`${required}\` more than once`);
|
|
1700
2361
|
else requires.push(required);
|
|
2362
|
+
const requiresAny = readRequiresAny(raw, id, report);
|
|
2363
|
+
const group = readLabelled(raw.group, "group", GROUP_KEYS, report);
|
|
2364
|
+
const section = readLabelled(raw.section, "section", SECTION_KEYS, report);
|
|
1701
2365
|
let options;
|
|
1702
2366
|
for (const entry of raw.options) {
|
|
1703
2367
|
const model = byLocal.get(entry.key);
|
|
@@ -1712,6 +2376,10 @@ function resolveRecipes(raws, classes) {
|
|
|
1712
2376
|
else options[model.className] = read;
|
|
1713
2377
|
}
|
|
1714
2378
|
}
|
|
2379
|
+
const surfaces = readSurfaces(raw.surfaces, report);
|
|
2380
|
+
const providers = readProviders(raw.providers, report);
|
|
2381
|
+
const runtime = readRuntime(raw.runtime, report);
|
|
2382
|
+
const demoSeed = readDemoSeed(raw.demoSeed, report);
|
|
1715
2383
|
const help = raw.help ? attempt(() => readHelp(raw.help, raw), raw.help.line) : void 0;
|
|
1716
2384
|
recipes.push({
|
|
1717
2385
|
id,
|
|
@@ -1722,9 +2390,52 @@ function resolveRecipes(raws, classes) {
|
|
|
1722
2390
|
models: models.map((model) => model.className),
|
|
1723
2391
|
nav,
|
|
1724
2392
|
requires,
|
|
2393
|
+
...requiresAny ? { requiresAny } : {},
|
|
2394
|
+
...group ? { group } : {},
|
|
2395
|
+
...section ? { section } : {},
|
|
1725
2396
|
...options && Object.keys(options).length > 0 ? { options } : {},
|
|
1726
|
-
...help ? { help } : {}
|
|
2397
|
+
...help ? { help } : {},
|
|
2398
|
+
...surfaces ? { surfaces } : {},
|
|
2399
|
+
...providers ? { providers } : {},
|
|
2400
|
+
...runtime ? { runtime } : {},
|
|
2401
|
+
...demoSeed ? { demoSeed } : {}
|
|
2402
|
+
});
|
|
2403
|
+
}
|
|
2404
|
+
recipes.sort((a, b) => a.id.localeCompare(b.id));
|
|
2405
|
+
const navKeys = /* @__PURE__ */ new Map();
|
|
2406
|
+
const widgetTypes = /* @__PURE__ */ new Map();
|
|
2407
|
+
const labelled = /* @__PURE__ */ new Map();
|
|
2408
|
+
for (const recipe of recipes) {
|
|
2409
|
+
const raw = ids.get(recipe.id);
|
|
2410
|
+
const note = (message) => errors.push({
|
|
2411
|
+
message: `Recipe ${recipe.className}: ${message}`,
|
|
2412
|
+
filePath: raw?.filePath ?? "",
|
|
2413
|
+
line: raw?.line,
|
|
2414
|
+
severity: "error"
|
|
1727
2415
|
});
|
|
2416
|
+
for (const entry of recipe.nav) {
|
|
2417
|
+
if (!entry.key) continue;
|
|
2418
|
+
const slot = `${entry.model}:${entry.key}`;
|
|
2419
|
+
const owner = navKeys.get(slot);
|
|
2420
|
+
if (owner && owner !== recipe.id) note(`nav key \`${entry.key}\` over ${entry.model} is already used by recipe ${owner}; layout ids must be unique`);
|
|
2421
|
+
navKeys.set(slot, owner ?? recipe.id);
|
|
2422
|
+
}
|
|
2423
|
+
for (const surface of recipe.surfaces ?? []) {
|
|
2424
|
+
if (surface.kind !== "widget") continue;
|
|
2425
|
+
const owner = widgetTypes.get(surface.type);
|
|
2426
|
+
if (owner && owner !== recipe.id) note(`widget type \`${surface.type}\` is already used by recipe ${owner}; widget types must be unique`);
|
|
2427
|
+
widgetTypes.set(surface.type, owner ?? recipe.id);
|
|
2428
|
+
}
|
|
2429
|
+
for (const [kind, value] of [["group", recipe.group], ["section", recipe.section]]) {
|
|
2430
|
+
if (!value) continue;
|
|
2431
|
+
const slot = `${kind}:${value.id}`;
|
|
2432
|
+
const first = labelled.get(slot);
|
|
2433
|
+
if (first && first.label !== value.label) note(`${kind} \`${value.id}\` is labelled "${value.label}" here but "${first.label}" in recipe ${first.recipe}`);
|
|
2434
|
+
else if (!first) labelled.set(slot, {
|
|
2435
|
+
label: value.label,
|
|
2436
|
+
recipe: recipe.id
|
|
2437
|
+
});
|
|
2438
|
+
}
|
|
1728
2439
|
}
|
|
1729
2440
|
const byId = new Map(recipes.map((recipe) => [recipe.id, recipe]));
|
|
1730
2441
|
const reported = /* @__PURE__ */ new Set();
|
|
@@ -2306,13 +3017,13 @@ function reportUnresolvedSpreads(unresolved, filePath, sourceText, errors) {
|
|
|
2306
3017
|
});
|
|
2307
3018
|
}
|
|
2308
3019
|
}
|
|
2309
|
-
function extractClassFromNode(node, filePath, sourceText, importAliases, ctx) {
|
|
2310
|
-
if (node.type === "ExportNamedDeclaration" && node.declaration) return extractClassFromNode(node.declaration, filePath, sourceText, importAliases, ctx);
|
|
2311
|
-
if (node.type === "ExportDefaultDeclaration" && node.declaration) return extractClassFromNode(node.declaration, filePath, sourceText, importAliases, ctx);
|
|
2312
|
-
if (node.type === "ClassDeclaration") return extractClassDeclaration(node, filePath, sourceText, importAliases, ctx);
|
|
3020
|
+
function extractClassFromNode(node, filePath, sourceText, importAliases, ctx, outerStart) {
|
|
3021
|
+
if (node.type === "ExportNamedDeclaration" && node.declaration) return extractClassFromNode(node.declaration, filePath, sourceText, importAliases, ctx, node.start ?? outerStart);
|
|
3022
|
+
if (node.type === "ExportDefaultDeclaration" && node.declaration) return extractClassFromNode(node.declaration, filePath, sourceText, importAliases, ctx, node.start ?? outerStart);
|
|
3023
|
+
if (node.type === "ClassDeclaration") return extractClassDeclaration(node, filePath, sourceText, importAliases, ctx, outerStart);
|
|
2313
3024
|
return null;
|
|
2314
3025
|
}
|
|
2315
|
-
function extractClassDeclaration(node, filePath, sourceText, importAliases, ctx) {
|
|
3026
|
+
function extractClassDeclaration(node, filePath, sourceText, importAliases, ctx, outerStart) {
|
|
2316
3027
|
const className = node.id?.name || "AnonymousClass";
|
|
2317
3028
|
const decorators = node.decorators || [];
|
|
2318
3029
|
const smrtDecorator = decorators.find((d) => isSmrtDecorator(d, importAliases));
|
|
@@ -2338,6 +3049,11 @@ function extractClassDeclaration(node, filePath, sourceText, importAliases, ctx)
|
|
|
2338
3049
|
const method = extractMethodDefinition(member, sourceText, ctx);
|
|
2339
3050
|
if (method) methods.push(method);
|
|
2340
3051
|
}
|
|
3052
|
+
const description = (typeof smrtConfig?.description === "string" ? normalizeModelDescription(smrtConfig.description) : void 0) ?? extractClassJsDocDescription(sourceText, className, [
|
|
3053
|
+
node.start,
|
|
3054
|
+
outerStart,
|
|
3055
|
+
...decorators.map((d) => d.start)
|
|
3056
|
+
].filter((n) => typeof n === "number"));
|
|
2341
3057
|
return {
|
|
2342
3058
|
className,
|
|
2343
3059
|
filePath,
|
|
@@ -2345,6 +3061,7 @@ function extractClassDeclaration(node, filePath, sourceText, importAliases, ctx)
|
|
|
2345
3061
|
extendsTypeArg,
|
|
2346
3062
|
decoratorConfig,
|
|
2347
3063
|
hasSmartDecorator,
|
|
3064
|
+
...description ? { description } : {},
|
|
2348
3065
|
fields,
|
|
2349
3066
|
methods,
|
|
2350
3067
|
startLine: node.loc?.start.line || 1,
|
|
@@ -2352,6 +3069,59 @@ function extractClassDeclaration(node, filePath, sourceText, importAliases, ctx)
|
|
|
2352
3069
|
...typeof node.body?.start === "number" ? { bodyStart: node.body.start } : {}
|
|
2353
3070
|
};
|
|
2354
3071
|
}
|
|
3072
|
+
var MODEL_DESCRIPTION_MAX = 200;
|
|
3073
|
+
function normalizeModelDescription(raw) {
|
|
3074
|
+
const text = raw.replace(/\s+/g, " ").trim();
|
|
3075
|
+
if (!text) return void 0;
|
|
3076
|
+
if (text.length <= MODEL_DESCRIPTION_MAX) return text;
|
|
3077
|
+
const head = text.slice(0, MODEL_DESCRIPTION_MAX);
|
|
3078
|
+
const sentenceEnd = Math.max(head.lastIndexOf(". "), head.lastIndexOf("! "), head.lastIndexOf("? "));
|
|
3079
|
+
if (sentenceEnd > 0) return head.slice(0, sentenceEnd + 1);
|
|
3080
|
+
const wordEnd = head.lastIndexOf(" ");
|
|
3081
|
+
return `${(wordEnd > 0 ? head.slice(0, wordEnd) : head).replace(/[,;:\s]+$/, "")}\u2026`;
|
|
3082
|
+
}
|
|
3083
|
+
function extractClassJsDocDescription(sourceText, className, anchors) {
|
|
3084
|
+
if (anchors.length === 0) return void 0;
|
|
3085
|
+
const before = sourceText.slice(0, Math.min(...anchors)).trimEnd();
|
|
3086
|
+
if (!before.endsWith("*/")) return void 0;
|
|
3087
|
+
const open = before.lastIndexOf("/**");
|
|
3088
|
+
if (open < 0 || before.indexOf("*/", open) !== before.length - 2) return;
|
|
3089
|
+
return summarizeJsDoc(before.slice(open, before.length), className);
|
|
3090
|
+
}
|
|
3091
|
+
function summarizeJsDoc(block, className) {
|
|
3092
|
+
const lines = block.replace(/^\/\*\*/, "").replace(/\*\/$/, "").split("\n").map((line) => line.replace(/^\s*\*\s?/, "").trimEnd());
|
|
3093
|
+
const summary = [];
|
|
3094
|
+
let internal = false;
|
|
3095
|
+
let inFence = false;
|
|
3096
|
+
for (const line of lines) {
|
|
3097
|
+
if (/^\s*@internal\b/.test(line)) internal = true;
|
|
3098
|
+
if (/^\s*```/.test(line)) {
|
|
3099
|
+
inFence = !inFence;
|
|
3100
|
+
continue;
|
|
3101
|
+
}
|
|
3102
|
+
if (inFence) continue;
|
|
3103
|
+
if (/^\s*@\w+/.test(line)) {
|
|
3104
|
+
summary.push("\0");
|
|
3105
|
+
continue;
|
|
3106
|
+
}
|
|
3107
|
+
summary.push(line);
|
|
3108
|
+
}
|
|
3109
|
+
if (internal) return void 0;
|
|
3110
|
+
const paragraph = [];
|
|
3111
|
+
for (const line of summary) {
|
|
3112
|
+
if (line === "\0") break;
|
|
3113
|
+
if (line.trim() === "") {
|
|
3114
|
+
if (paragraph.length > 0) break;
|
|
3115
|
+
continue;
|
|
3116
|
+
}
|
|
3117
|
+
paragraph.push(line.trim());
|
|
3118
|
+
}
|
|
3119
|
+
let text = paragraph.join(" ");
|
|
3120
|
+
const escaped = className.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3121
|
+
text = text.replace(new RegExp(`^${escaped}\\s*(?:[-\u2013\u2014:]+)\\s*`), "").replace(/\{@(?:link|linkcode|linkplain)\s+([^}|\s]+)(?:[|\s]+([^}]*))?\}/g, (_m, target, label) => label?.trim() ? label.trim() : target).replace(/\{@\w+\s*([^}]*)\}/g, "$1").replace(/\[([^\]]+)\]\([^)]*\)/g, "$1").replace(/`([^`]*)`/g, "$1").replace(/<\/?[a-zA-Z][^>]*>/g, "").replace(/(\*\*|__)(.+?)\1/g, "$2");
|
|
3122
|
+
if (text.length > 0) text = text[0].toUpperCase() + text.slice(1);
|
|
3123
|
+
return normalizeModelDescription(text);
|
|
3124
|
+
}
|
|
2355
3125
|
function isSmrtDecorator(decorator, importAliases) {
|
|
2356
3126
|
return isNamedDecorator(decorator, "smrt", importAliases);
|
|
2357
3127
|
}
|
|
@@ -2959,7 +3729,7 @@ var ManifestAdapter = class ManifestAdapter {
|
|
|
2959
3729
|
...recipe,
|
|
2960
3730
|
models: recipe.models.map(qualify),
|
|
2961
3731
|
nav: recipe.nav.map((entry) => ({
|
|
2962
|
-
|
|
3732
|
+
...entry,
|
|
2963
3733
|
model: qualify(entry.model)
|
|
2964
3734
|
})),
|
|
2965
3735
|
...recipe.options ? { options: Object.fromEntries(Object.entries(recipe.options).map(([model, value]) => [qualify(model), value])) } : {}
|
|
@@ -3023,7 +3793,7 @@ var ManifestAdapter = class ManifestAdapter {
|
|
|
3023
3793
|
const converted = this.convertMethod(method);
|
|
3024
3794
|
if (converted) methods[method.name] = converted;
|
|
3025
3795
|
}
|
|
3026
|
-
const collection = this.pluralize(classDef.className);
|
|
3796
|
+
const collection = typeof classDef.decoratorConfig?.collection === "string" ? classDef.decoratorConfig.collection : this.pluralize(classDef.className);
|
|
3027
3797
|
const packageName = options.packageName || classDef.packageName;
|
|
3028
3798
|
const qualifiedName = packageName ? createQualifiedName(packageName, classDef.className) : void 0;
|
|
3029
3799
|
return {
|
|
@@ -3031,6 +3801,7 @@ var ManifestAdapter = class ManifestAdapter {
|
|
|
3031
3801
|
className: classDef.className,
|
|
3032
3802
|
qualifiedName,
|
|
3033
3803
|
collection,
|
|
3804
|
+
...classDef.description ? { description: classDef.description } : {},
|
|
3034
3805
|
filePath: classDef.filePath,
|
|
3035
3806
|
packageName: packageName || void 0,
|
|
3036
3807
|
fields,
|
|
@@ -3234,7 +4005,8 @@ var ManifestAdapter = class ManifestAdapter {
|
|
|
3234
4005
|
"description",
|
|
3235
4006
|
"default",
|
|
3236
4007
|
"constraint",
|
|
3237
|
-
"onDelete"
|
|
4008
|
+
"onDelete",
|
|
4009
|
+
"onUpdate"
|
|
3238
4010
|
];
|
|
3239
4011
|
if (parsedOptions) {
|
|
3240
4012
|
for (const key of META_KEYS) if (parsedOptions[key] !== void 0) meta[key] = parsedOptions[key];
|
|
@@ -3283,7 +4055,8 @@ var ManifestAdapter = class ManifestAdapter {
|
|
|
3283
4055
|
"default",
|
|
3284
4056
|
"indexed",
|
|
3285
4057
|
"idType",
|
|
3286
|
-
"onDelete"
|
|
4058
|
+
"onDelete",
|
|
4059
|
+
"onUpdate"
|
|
3287
4060
|
];
|
|
3288
4061
|
if (parsedOptions) {
|
|
3289
4062
|
for (const key of META_KEYS) if (parsedOptions[key] !== void 0) meta[key] = parsedOptions[key];
|
|
@@ -4138,4 +4911,4 @@ var OxcScanner = class {
|
|
|
4138
4911
|
//#endregion
|
|
4139
4912
|
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 };
|
|
4140
4913
|
|
|
4141
|
-
//# sourceMappingURL=scanner-
|
|
4914
|
+
//# sourceMappingURL=scanner-BbpPS8Aq.js.map
|