@askrjs/cli 0.0.16 → 0.0.17
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/analyze.js
CHANGED
|
@@ -71,7 +71,7 @@ async function runAnalyzeCli(args = process.argv.slice(2), io = console, runtime
|
|
|
71
71
|
io.log(helpText.trimEnd());
|
|
72
72
|
return 0;
|
|
73
73
|
}
|
|
74
|
-
const report = await (runtime.analyze ?? (await import("./runner-
|
|
74
|
+
const report = await (runtime.analyze ?? (await import("./runner-SJnfoNvK.js")).runAnalysis)({
|
|
75
75
|
cwd: parsed.cwd,
|
|
76
76
|
workspacePatterns: parsed.workspacePatterns,
|
|
77
77
|
check: parsed.check
|
package/dist/cli.js
CHANGED
|
@@ -96,7 +96,7 @@ async function runCli(args = process.argv.slice(2), io = console) {
|
|
|
96
96
|
return runAnalyzeCli(args.slice(1), io);
|
|
97
97
|
}
|
|
98
98
|
if (command === "check" || command === "doctor" || command === "repair") {
|
|
99
|
-
const { runGuardrailCli } = await import("./guardrails-
|
|
99
|
+
const { runGuardrailCli } = await import("./guardrails-Dq4-Rglh.js");
|
|
100
100
|
return runGuardrailCli(command, args.slice(1), io);
|
|
101
101
|
}
|
|
102
102
|
if (command === "generate") {
|
|
@@ -98,7 +98,7 @@ async function runGuardrailCli(command, args = process.argv.slice(2), io = conso
|
|
|
98
98
|
cwd: parsed.cwd,
|
|
99
99
|
workspacePatterns: parsed.workspacePatterns
|
|
100
100
|
};
|
|
101
|
-
const { runCheck, runDoctor, runRepair } = await import("./runner-
|
|
101
|
+
const { runCheck, runDoctor, runRepair } = await import("./runner-H-TW4lHZ.js");
|
|
102
102
|
const report = command === "doctor" ? await runDoctor(options, runtime) : command === "repair" ? await runRepair(options) : await runCheck(options, runtime);
|
|
103
103
|
if (parsed.json) io.log(JSON.stringify(report));
|
|
104
104
|
else if (report.command === "doctor") printDoctor(report, io);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { t as inspectBundledSkills } from "./skills-CSGdAZHN.js";
|
|
2
2
|
import { discoverWorkspaceProject } from "./discovery-DUDrZCIC.js";
|
|
3
|
-
import { analysisHasBlockingFindings, runAnalysis } from "./runner-
|
|
3
|
+
import { analysisHasBlockingFindings, runAnalysis } from "./runner-SJnfoNvK.js";
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
@@ -168,7 +168,8 @@ const ASKR_CONCEPTS = {
|
|
|
168
168
|
"task",
|
|
169
169
|
"timer",
|
|
170
170
|
"stream",
|
|
171
|
-
"on"
|
|
171
|
+
"on",
|
|
172
|
+
"onRouteChange"
|
|
172
173
|
],
|
|
173
174
|
data: [
|
|
174
175
|
"createQuery",
|
|
@@ -565,6 +566,12 @@ const controlContractRule = {
|
|
|
565
566
|
const parentOpening = ts.isJsxElement(parentElement) ? parentElement.openingElement : null;
|
|
566
567
|
if (!parentOpening || canonicalJsxName(parentOpening.tagName, bindings) !== "Case") diagnostics.push(diagnostic(context, node.tagName, this, "<Match> may only be used as a direct child of <Case>.", "Move this branch directly inside a <Case> boundary."));
|
|
567
568
|
}
|
|
569
|
+
if (name === "Case" && ts.isJsxOpeningElement(node) && ts.isJsxElement(node.parent)) for (const child of node.parent.children) {
|
|
570
|
+
if (ts.isJsxText(child) && child.text.trim() === "") continue;
|
|
571
|
+
if (ts.isJsxExpression(child) && (!child.expression || child.expression.kind === ts.SyntaxKind.NullKeyword || child.expression.kind === ts.SyntaxKind.FalseKeyword)) continue;
|
|
572
|
+
if (ts.isJsxElement(child) && canonicalJsxName(child.openingElement.tagName, bindings) === "Match") continue;
|
|
573
|
+
diagnostics.push(diagnostic(context, child, this, "<Case> may only contain direct <Match> branches, null, false, or whitespace.", "Move non-branch content into a <Match> child."));
|
|
574
|
+
}
|
|
568
575
|
});
|
|
569
576
|
}
|
|
570
577
|
return diagnostics;
|
|
@@ -926,6 +933,7 @@ const dataContractRule = {
|
|
|
926
933
|
const key = optionExpression(options, "key");
|
|
927
934
|
if (!key || literalString(key) !== null && literalString(key)?.trim() === "") diagnostics.push(diagnostic(context, key ?? options, this, "createQuery() requires a non-empty key.", "Pass a stable query key."));
|
|
928
935
|
else if (ts.isLiteralExpression(key) && !ts.isStringLiteralLike(key)) diagnostics.push(diagnostic(context, key, this, "createQuery() key must be a string or key function."));
|
|
936
|
+
else if (ts.isObjectLiteralExpression(key) || ts.isArrayLiteralExpression(key)) diagnostics.push(diagnostic(context, key, this, "createQuery() key must not be an object or array allocation.", "Use a stable string or a key function that returns the runtime-supported key shape."));
|
|
929
937
|
const fetcher = optionExpression(options, "fetch");
|
|
930
938
|
if (!fetcher || isProvablyNonFunction(fetcher)) diagnostics.push(diagnostic(context, fetcher ?? options, this, "createQuery() requires a fetch function.", "Pass a cancellable fetch function."));
|
|
931
939
|
} else {
|
|
@@ -1201,31 +1209,448 @@ function hasDependency(manifest, packageName) {
|
|
|
1201
1209
|
"optionalDependencies"
|
|
1202
1210
|
].some((section) => packageName in dependencyRecord(manifest, section));
|
|
1203
1211
|
}
|
|
1204
|
-
const
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
remediation: "Fix the syntax error so framework rules can inspect this file reliably."
|
|
1212
|
+
const frameworkConfigRule = {
|
|
1213
|
+
id: "askr/framework-config",
|
|
1214
|
+
category: "configuration",
|
|
1215
|
+
severity: "error",
|
|
1216
|
+
description: "TypeScript and Vite must use Askr's JSX/runtime wiring.",
|
|
1217
|
+
analyze(context) {
|
|
1218
|
+
const diagnostics = [];
|
|
1219
|
+
const tsx = context.sourceFiles.find((sourceFile) => sourceFile.fileName.endsWith(".tsx"));
|
|
1220
|
+
if (!tsx || !hasDependency(context.workspace.manifest, "@askrjs/askr")) return diagnostics;
|
|
1221
|
+
const tsconfigPath = path.join(context.workspace.directory, "tsconfig.json");
|
|
1222
|
+
const tsconfigSource = ts.sys.readFile(tsconfigPath);
|
|
1223
|
+
if (context.program.getCompilerOptions().jsxImportSource !== "@askrjs/askr") {
|
|
1224
|
+
let fix;
|
|
1225
|
+
if (tsconfigSource) try {
|
|
1226
|
+
const parsed = JSON.parse(tsconfigSource);
|
|
1227
|
+
parsed.compilerOptions = {
|
|
1228
|
+
...parsed.compilerOptions && typeof parsed.compilerOptions === "object" && !Array.isArray(parsed.compilerOptions) ? parsed.compilerOptions : {},
|
|
1229
|
+
jsx: "react-jsx",
|
|
1230
|
+
jsxImportSource: "@askrjs/askr"
|
|
1224
1231
|
};
|
|
1232
|
+
fix = {
|
|
1233
|
+
description: "Configure TypeScript to use the Askr JSX runtime",
|
|
1234
|
+
filePath: tsconfigPath,
|
|
1235
|
+
start: 0,
|
|
1236
|
+
end: tsconfigSource.length,
|
|
1237
|
+
replacement: `${JSON.stringify(parsed, null, 2)}\n`
|
|
1238
|
+
};
|
|
1239
|
+
} catch {}
|
|
1240
|
+
diagnostics.push(diagnostic(context, tsx, this, "TSX is present but compilerOptions.jsxImportSource is not '@askrjs/askr'.", "Set jsx to react-jsx and jsxImportSource to @askrjs/askr.", fix));
|
|
1241
|
+
}
|
|
1242
|
+
const viteConfig = context.sourceFiles.find((sourceFile) => /(?:^|\/)vite\.config\.[cm]?[jt]s$/.test(sourceFile.fileName.split(path.sep).join("/")));
|
|
1243
|
+
if (viteConfig) {
|
|
1244
|
+
let pluginLocalName = null;
|
|
1245
|
+
for (const statement of viteConfig.statements) {
|
|
1246
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== "@askrjs/vite" || !statement.importClause?.namedBindings || !ts.isNamedImports(statement.importClause.namedBindings)) continue;
|
|
1247
|
+
pluginLocalName = statement.importClause.namedBindings.elements.find((element) => (element.propertyName?.text ?? element.name.text) === "askr")?.name.text ?? null;
|
|
1248
|
+
}
|
|
1249
|
+
let pluginCalled = false;
|
|
1250
|
+
if (pluginLocalName) visit(viteConfig, (node) => {
|
|
1251
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === pluginLocalName) pluginCalled = true;
|
|
1225
1252
|
});
|
|
1253
|
+
const dependencyPresent = hasDependency(context.workspace.manifest, "@askrjs/vite");
|
|
1254
|
+
if (dependencyPresent && pluginCalled) return diagnostics;
|
|
1255
|
+
diagnostics.push(diagnostic(context, viteConfig, this, !dependencyPresent ? "This Askr Vite project does not declare @askrjs/vite." : "Vite is configured without calling the @askrjs/vite askr() plugin.", "Declare @askrjs/vite, import askr, and include askr() in the plugin list."));
|
|
1226
1256
|
}
|
|
1227
|
-
|
|
1257
|
+
return diagnostics;
|
|
1258
|
+
}
|
|
1259
|
+
};
|
|
1260
|
+
const parseErrorRule = {
|
|
1261
|
+
id: "askr/parse-error",
|
|
1262
|
+
category: "correctness",
|
|
1263
|
+
severity: "error",
|
|
1264
|
+
description: "Source must parse before framework analysis is reliable.",
|
|
1265
|
+
analyze(context) {
|
|
1266
|
+
return context.program.getSyntacticDiagnostics().filter((entry) => Boolean(entry.file && context.sourceFiles.some((sourceFile) => sourceFile.fileName === entry.file?.fileName))).map((entry) => {
|
|
1267
|
+
const start = entry.start ?? 0;
|
|
1268
|
+
const point = entry.file.getLineAndCharacterOfPosition(start);
|
|
1269
|
+
return {
|
|
1270
|
+
ruleId: this.id,
|
|
1271
|
+
category: this.category,
|
|
1272
|
+
severity: this.severity,
|
|
1273
|
+
message: ts.flattenDiagnosticMessageText(entry.messageText, "\n"),
|
|
1274
|
+
workspace: context.workspace.name,
|
|
1275
|
+
file: workspaceRelativeFile(context, entry.file.fileName),
|
|
1276
|
+
line: point.line + 1,
|
|
1277
|
+
column: point.character + 1,
|
|
1278
|
+
remediation: "Fix the syntax error so framework rules can inspect this file reliably."
|
|
1279
|
+
};
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
};
|
|
1283
|
+
function nearestComponent(node) {
|
|
1284
|
+
for (let current = node.parent; current; current = current.parent) if (ts.isFunctionLike(current)) {
|
|
1285
|
+
const name = functionName(current);
|
|
1286
|
+
if (name && /^[A-Z]/.test(name) || containsJsx(current)) return current;
|
|
1287
|
+
}
|
|
1288
|
+
return null;
|
|
1289
|
+
}
|
|
1290
|
+
function isInsideNestedFunction(node, owner) {
|
|
1291
|
+
for (let current = node.parent; current && current !== owner; current = current.parent) if (ts.isFunctionLike(current)) return true;
|
|
1292
|
+
return false;
|
|
1293
|
+
}
|
|
1294
|
+
function jsxExpression(attribute) {
|
|
1295
|
+
if (!attribute || !ts.isJsxAttribute(attribute) || !attribute.initializer || !ts.isJsxExpression(attribute.initializer)) return null;
|
|
1296
|
+
return attribute.initializer.expression ?? null;
|
|
1297
|
+
}
|
|
1298
|
+
const stableControlBoundaryRule = {
|
|
1299
|
+
id: "askr/stable-control-boundary",
|
|
1300
|
+
category: "correctness",
|
|
1301
|
+
severity: "error",
|
|
1302
|
+
description: "Conditional control boundaries must not change identity between renders.",
|
|
1303
|
+
analyze(context) {
|
|
1304
|
+
const diagnostics = [];
|
|
1305
|
+
for (const sourceFile of context.sourceFiles) {
|
|
1306
|
+
const bindings = sourceBindings(sourceFile);
|
|
1307
|
+
const facts = sourceFacts(sourceFile);
|
|
1308
|
+
if (!facts.jsx.some((fact) => [
|
|
1309
|
+
"For",
|
|
1310
|
+
"Show",
|
|
1311
|
+
"Case"
|
|
1312
|
+
].includes(fact.name)) && !facts.calls.some((fact) => fact.name === "defineScope")) continue;
|
|
1313
|
+
visit(sourceFile, (node) => {
|
|
1314
|
+
let candidate = null;
|
|
1315
|
+
if ((ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) && [
|
|
1316
|
+
"For",
|
|
1317
|
+
"Show",
|
|
1318
|
+
"Case"
|
|
1319
|
+
].includes(canonicalJsxName(node.tagName, bindings) ?? "")) candidate = node;
|
|
1320
|
+
else if (ts.isCallExpression(node) && canonicalCallName(node.expression, bindings) === "defineScope") candidate = node;
|
|
1321
|
+
if (!candidate) return;
|
|
1322
|
+
const owner = nearestComponent(candidate);
|
|
1323
|
+
if (!owner || !isControlFlowAncestor(candidate, owner)) return;
|
|
1324
|
+
diagnostics.push(diagnostic(context, candidate, this, "An Askr control boundary is created conditionally, so its render identity is unstable.", "Create the boundary unconditionally and put the condition in <Show>, <Match>, or its inputs."));
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
return diagnostics;
|
|
1328
|
+
}
|
|
1329
|
+
};
|
|
1330
|
+
function dependencyNames(array) {
|
|
1331
|
+
const names = /* @__PURE__ */ new Set();
|
|
1332
|
+
for (const element of array.elements) {
|
|
1333
|
+
if (ts.isIdentifier(element)) names.add(element.text);
|
|
1334
|
+
if (ts.isCallExpression(element) && ts.isIdentifier(element.expression)) names.add(element.expression.text);
|
|
1335
|
+
}
|
|
1336
|
+
return names;
|
|
1337
|
+
}
|
|
1338
|
+
const exhaustiveDependenciesRule = {
|
|
1339
|
+
id: "askr/exhaustive-dependencies",
|
|
1340
|
+
category: "correctness",
|
|
1341
|
+
severity: "warning",
|
|
1342
|
+
description: "Resource and stream dependency arrays must list directly read reactive values.",
|
|
1343
|
+
analyze(context) {
|
|
1344
|
+
const diagnostics = [];
|
|
1345
|
+
for (const sourceFile of context.sourceFiles) {
|
|
1346
|
+
const bindings = sourceBindings(sourceFile);
|
|
1347
|
+
if (!sourceFacts(sourceFile).calls.some((fact) => fact.name === "resource" || fact.name === "stream")) continue;
|
|
1348
|
+
const reactive = collectStateBindings(sourceFile, bindings).getters;
|
|
1349
|
+
for (const { node, name } of sourceFacts(sourceFile).calls) {
|
|
1350
|
+
if (name !== "resource" && name !== "stream") continue;
|
|
1351
|
+
const loader = node.arguments[0];
|
|
1352
|
+
const deps = name === "resource" ? node.arguments[1] : node.arguments[1] && ts.isObjectLiteralExpression(node.arguments[1]) ? optionExpression(node.arguments[1], "deps") : node.arguments[1];
|
|
1353
|
+
if (!loader || !ts.isArrowFunction(loader) && !ts.isFunctionExpression(loader) || !deps || !ts.isArrayLiteralExpression(deps) || deps.elements.some(ts.isSpreadElement)) continue;
|
|
1354
|
+
const declared = dependencyNames(deps);
|
|
1355
|
+
const missing = /* @__PURE__ */ new Set();
|
|
1356
|
+
const walk = (candidate) => {
|
|
1357
|
+
if (candidate !== loader && ts.isFunctionLike(candidate)) return;
|
|
1358
|
+
if (ts.isCallExpression(candidate) && ts.isIdentifier(candidate.expression) && reactive.has(candidate.expression.text) && !declared.has(candidate.expression.text)) missing.add(candidate.expression.text);
|
|
1359
|
+
ts.forEachChild(candidate, walk);
|
|
1360
|
+
};
|
|
1361
|
+
walk(loader.body);
|
|
1362
|
+
for (const name of [...missing].sort()) diagnostics.push(diagnostic(context, loader, this, `${name}() is read by ${canonicalCallName(node.expression, bindings)}() but is missing from its dependency array.`, `Add ${name}() to the literal dependency array.`));
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
return diagnostics;
|
|
1366
|
+
}
|
|
1367
|
+
};
|
|
1368
|
+
const forRowClosureCaptureRule = {
|
|
1369
|
+
id: "askr/for-row-closure-capture",
|
|
1370
|
+
category: "correctness",
|
|
1371
|
+
severity: "warning",
|
|
1372
|
+
description: "For row renderers must not capture changing component reactive values.",
|
|
1373
|
+
analyze(context) {
|
|
1374
|
+
const diagnostics = [];
|
|
1375
|
+
for (const sourceFile of context.sourceFiles) {
|
|
1376
|
+
const bindings = sourceBindings(sourceFile);
|
|
1377
|
+
if (!sourceFacts(sourceFile).jsx.some((fact) => fact.name === "For")) continue;
|
|
1378
|
+
const reactive = collectStateBindings(sourceFile, bindings).getters;
|
|
1379
|
+
const snapshots = /* @__PURE__ */ new Set();
|
|
1380
|
+
visit(sourceFile, (candidate) => {
|
|
1381
|
+
if (ts.isVariableDeclaration(candidate) && ts.isIdentifier(candidate.name) && candidate.initializer && ts.isCallExpression(candidate.initializer) && ts.isIdentifier(candidate.initializer.expression) && reactive.has(candidate.initializer.expression.text)) snapshots.add(candidate.name.text);
|
|
1382
|
+
});
|
|
1383
|
+
visit(sourceFile, (node) => {
|
|
1384
|
+
if (!ts.isJsxElement(node) || canonicalJsxName(node.openingElement.tagName, bindings) !== "For") return;
|
|
1385
|
+
for (const child of node.children) {
|
|
1386
|
+
if (!ts.isJsxExpression(child) || !child.expression || !ts.isArrowFunction(child.expression) && !ts.isFunctionExpression(child.expression)) continue;
|
|
1387
|
+
const renderer = child.expression;
|
|
1388
|
+
const captured = /* @__PURE__ */ new Set();
|
|
1389
|
+
const walk = (candidate) => {
|
|
1390
|
+
for (let current = candidate.parent; current && current !== renderer; current = current.parent) if (ts.isJsxAttribute(current)) return;
|
|
1391
|
+
if (ts.isCallExpression(candidate) && ts.isIdentifier(candidate.expression) && reactive.has(candidate.expression.text)) captured.add(candidate.expression.text);
|
|
1392
|
+
if (ts.isIdentifier(candidate) && snapshots.has(candidate.text) && !(ts.isPropertyAccessExpression(candidate.parent) && candidate.parent.name === candidate)) captured.add(candidate.text);
|
|
1393
|
+
ts.forEachChild(candidate, walk);
|
|
1394
|
+
};
|
|
1395
|
+
walk(renderer.body);
|
|
1396
|
+
for (const name of [...captured].sort()) diagnostics.push(diagnostic(context, renderer, this, `<For> row rendering captures reactive value '${name}' from its component closure.`, "Read changing values through row data, a selector predicate, or a function-valued JSX prop."));
|
|
1397
|
+
}
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
return diagnostics;
|
|
1401
|
+
}
|
|
1402
|
+
};
|
|
1403
|
+
const renderScopeRequiredRule = {
|
|
1404
|
+
id: "askr/render-scope-required",
|
|
1405
|
+
category: "correctness",
|
|
1406
|
+
severity: "error",
|
|
1407
|
+
description: "Render-scoped APIs cannot be created in callbacks that execute outside rendering.",
|
|
1408
|
+
analyze(context) {
|
|
1409
|
+
const diagnostics = [];
|
|
1410
|
+
const callbackCalls = /* @__PURE__ */ new Set([
|
|
1411
|
+
"setTimeout",
|
|
1412
|
+
"setInterval",
|
|
1413
|
+
"queueMicrotask",
|
|
1414
|
+
"then",
|
|
1415
|
+
"catch",
|
|
1416
|
+
"finally"
|
|
1417
|
+
]);
|
|
1418
|
+
for (const sourceFile of context.sourceFiles) {
|
|
1419
|
+
const bindings = sourceBindings(sourceFile);
|
|
1420
|
+
if (!sourceFacts(sourceFile).calls.some((fact) => RENDER_SCOPED_CONCEPTS.has(fact.name))) continue;
|
|
1421
|
+
visit(sourceFile, (node) => {
|
|
1422
|
+
if (!ts.isCallExpression(node)) return;
|
|
1423
|
+
const name = canonicalCallName(node.expression, bindings);
|
|
1424
|
+
if (!name || !RENDER_SCOPED_CONCEPTS.has(name) || name === "readScope") return;
|
|
1425
|
+
for (let current = node.parent; current; current = current.parent) {
|
|
1426
|
+
if (!ts.isFunctionLike(current)) continue;
|
|
1427
|
+
const parent = current.parent;
|
|
1428
|
+
if (!(ts.isCallExpression(parent) && (ts.isIdentifier(parent.expression) && callbackCalls.has(parent.expression.text) || ts.isPropertyAccessExpression(parent.expression) && callbackCalls.has(parent.expression.name.text)) || ts.isJsxExpression(parent) && ts.isJsxAttribute(parent.parent) || ts.isPropertyAssignment(parent) && parent.name.getText() === "body")) continue;
|
|
1429
|
+
diagnostics.push(diagnostic(context, node.expression, this, `${name}() is created in a callback that is statically outside component rendering.`, `Create ${name}() at component render scope and use its value from the callback.`));
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
return diagnostics;
|
|
1435
|
+
}
|
|
1436
|
+
};
|
|
1437
|
+
const stableModuleIdentityRule = {
|
|
1438
|
+
id: "askr/stable-module-identity",
|
|
1439
|
+
category: "correctness",
|
|
1440
|
+
severity: "error",
|
|
1441
|
+
description: "Lazy modules and scopes must have stable identity across renders.",
|
|
1442
|
+
analyze(context) {
|
|
1443
|
+
const diagnostics = [];
|
|
1444
|
+
for (const sourceFile of context.sourceFiles) for (const { node, name } of sourceFacts(sourceFile).calls) {
|
|
1445
|
+
if (name !== "lazy" && name !== "defineScope") continue;
|
|
1446
|
+
const owner = nearestComponent(node);
|
|
1447
|
+
if (!owner || isInsideNestedFunction(node, owner)) continue;
|
|
1448
|
+
diagnostics.push(diagnostic(context, node.expression, this, `${name}() is created during component rendering and receives a new identity each render.`, `Move ${name}() to module scope.`));
|
|
1449
|
+
}
|
|
1450
|
+
return diagnostics;
|
|
1451
|
+
}
|
|
1452
|
+
};
|
|
1453
|
+
const queryKeyContractRule = {
|
|
1454
|
+
id: "askr/query-key-contract",
|
|
1455
|
+
category: "correctness",
|
|
1456
|
+
severity: "error",
|
|
1457
|
+
description: "Query keys and scopes must be deterministic and serializable.",
|
|
1458
|
+
analyze(context) {
|
|
1459
|
+
const diagnostics = [];
|
|
1460
|
+
const nondeterministic = /* @__PURE__ */ new Set([
|
|
1461
|
+
"random",
|
|
1462
|
+
"now",
|
|
1463
|
+
"randomUUID"
|
|
1464
|
+
]);
|
|
1465
|
+
for (const sourceFile of context.sourceFiles) {
|
|
1466
|
+
const bindings = sourceBindings(sourceFile);
|
|
1467
|
+
if (!sourceFacts(sourceFile).calls.some((fact) => fact.name === "createQuery" || fact.name === "queryScope")) continue;
|
|
1468
|
+
for (const call of sourceFacts(sourceFile).allCalls) {
|
|
1469
|
+
const name = canonicalCallName(call.expression, bindings);
|
|
1470
|
+
let expression;
|
|
1471
|
+
if (name === "createQuery") {
|
|
1472
|
+
const options = call.arguments[0];
|
|
1473
|
+
if (options && ts.isObjectLiteralExpression(options)) expression = optionExpression(options, "key");
|
|
1474
|
+
} else if (name === "queryScope") expression = call.arguments[0];
|
|
1475
|
+
else continue;
|
|
1476
|
+
if (!expression) continue;
|
|
1477
|
+
let invalid = null;
|
|
1478
|
+
const walk = (node) => {
|
|
1479
|
+
if (invalid) return;
|
|
1480
|
+
if (ts.isCallExpression(node) && (ts.isPropertyAccessExpression(node.expression) && nondeterministic.has(node.expression.name.text) || ts.isIdentifier(node.expression) && node.expression.text === "Symbol")) {
|
|
1481
|
+
invalid = node;
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
ts.forEachChild(node, walk);
|
|
1485
|
+
};
|
|
1486
|
+
walk(expression);
|
|
1487
|
+
if (!invalid) continue;
|
|
1488
|
+
diagnostics.push(diagnostic(context, invalid, this, `${name}() contains a directly provable nondeterministic or Symbol key part.`, "Use stable serializable primitives derived from route, props, or state."));
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
return diagnostics;
|
|
1492
|
+
}
|
|
1493
|
+
};
|
|
1494
|
+
const routeScopeStructureRule = {
|
|
1495
|
+
id: "askr/route-scope-structure",
|
|
1496
|
+
category: "correctness",
|
|
1497
|
+
severity: "error",
|
|
1498
|
+
description: "Nested page route scopes must have one index and relative child routes.",
|
|
1499
|
+
analyze(context) {
|
|
1500
|
+
const diagnostics = [];
|
|
1501
|
+
for (const sourceFile of context.sourceFiles) {
|
|
1502
|
+
const bindings = sourceBindings(sourceFile);
|
|
1503
|
+
if (!sourceFacts(sourceFile).calls.some((fact) => fact.name === "page")) continue;
|
|
1504
|
+
const inspectBody = (body) => {
|
|
1505
|
+
let indexes = 0;
|
|
1506
|
+
const walk = (node) => {
|
|
1507
|
+
if (node !== body && ts.isFunctionLike(node)) return;
|
|
1508
|
+
if (!ts.isCallExpression(node)) {
|
|
1509
|
+
ts.forEachChild(node, walk);
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
const name = canonicalCallName(node.expression, bindings);
|
|
1513
|
+
if (name === "index") {
|
|
1514
|
+
indexes += 1;
|
|
1515
|
+
if (indexes > 1) diagnostics.push(diagnostic(context, node.expression, this, "A page scope declares more than one index route.", "Keep exactly one index() declaration in a page scope."));
|
|
1516
|
+
}
|
|
1517
|
+
if (name === "route") {
|
|
1518
|
+
const routePath = node.arguments[0];
|
|
1519
|
+
if (routePath && ts.isStringLiteral(routePath) && routePath.text.startsWith("/") && routePath.text.length > 1) {
|
|
1520
|
+
const replacement = routePath.text.replace(/^\/+/, "");
|
|
1521
|
+
diagnostics.push(diagnostic(context, routePath, this, `Child route '${routePath.text}' is absolute inside a page scope.`, `Use the relative child path '${replacement}'.`, {
|
|
1522
|
+
description: "Strip the leading slash from a proven child route",
|
|
1523
|
+
filePath: sourceFile.fileName,
|
|
1524
|
+
start: routePath.getStart(sourceFile),
|
|
1525
|
+
end: routePath.getEnd(),
|
|
1526
|
+
replacement: JSON.stringify(replacement)
|
|
1527
|
+
}));
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
ts.forEachChild(node, walk);
|
|
1531
|
+
};
|
|
1532
|
+
walk(body);
|
|
1533
|
+
};
|
|
1534
|
+
for (const { node, name } of sourceFacts(sourceFile).calls) {
|
|
1535
|
+
if (name !== "page") continue;
|
|
1536
|
+
const callback = node.arguments.find((argument) => ts.isArrowFunction(argument) || ts.isFunctionExpression(argument));
|
|
1537
|
+
if (callback) inspectBody(callback.body);
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
return diagnostics;
|
|
1541
|
+
}
|
|
1542
|
+
};
|
|
1543
|
+
const IMPORT_SUBPATHS = {
|
|
1544
|
+
ActionForm: "actions",
|
|
1545
|
+
action: "actions",
|
|
1546
|
+
defineAction: "actions",
|
|
1547
|
+
createSPA: "boot",
|
|
1548
|
+
hydrateSPA: "boot",
|
|
1549
|
+
createIsland: "boot",
|
|
1550
|
+
createIslands: "boot",
|
|
1551
|
+
createQuery: "data",
|
|
1552
|
+
createMutation: "data",
|
|
1553
|
+
invalidate: "data",
|
|
1554
|
+
invalidateOnInterval: "data",
|
|
1555
|
+
queryScope: "data",
|
|
1556
|
+
route: "router",
|
|
1557
|
+
page: "router",
|
|
1558
|
+
index: "router",
|
|
1559
|
+
group: "router",
|
|
1560
|
+
fallback: "router",
|
|
1561
|
+
lazy: "router",
|
|
1562
|
+
createRouteRegistry: "router",
|
|
1563
|
+
resource: "resources",
|
|
1564
|
+
task: "resources",
|
|
1565
|
+
timer: "resources",
|
|
1566
|
+
stream: "resources",
|
|
1567
|
+
on: "resources",
|
|
1568
|
+
onRouteChange: "router",
|
|
1569
|
+
renderToString: "ssr",
|
|
1570
|
+
renderToStream: "ssr",
|
|
1571
|
+
createStaticGen: "ssg"
|
|
1572
|
+
};
|
|
1573
|
+
const importSubpathRule = {
|
|
1574
|
+
id: "askr/import-subpath",
|
|
1575
|
+
category: "configuration",
|
|
1576
|
+
severity: "error",
|
|
1577
|
+
description: "Askr APIs must be imported from their public owning subpath.",
|
|
1578
|
+
analyze(context) {
|
|
1579
|
+
const diagnostics = [];
|
|
1580
|
+
for (const sourceFile of context.sourceFiles) for (const statement of sourceFile.statements) {
|
|
1581
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== "@askrjs/askr" || !statement.importClause?.namedBindings || !ts.isNamedImports(statement.importClause.namedBindings)) continue;
|
|
1582
|
+
const elements = statement.importClause.namedBindings.elements;
|
|
1583
|
+
const misplaced = elements.filter((element) => IMPORT_SUBPATHS[element.propertyName?.text ?? element.name.text]);
|
|
1584
|
+
if (misplaced.length === 0) continue;
|
|
1585
|
+
const valid = elements.filter((element) => !misplaced.includes(element));
|
|
1586
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1587
|
+
for (const element of misplaced) {
|
|
1588
|
+
const imported = element.propertyName?.text ?? element.name.text;
|
|
1589
|
+
const subpath = IMPORT_SUBPATHS[imported];
|
|
1590
|
+
const list = groups.get(subpath) ?? [];
|
|
1591
|
+
list.push(element);
|
|
1592
|
+
groups.set(subpath, list);
|
|
1593
|
+
}
|
|
1594
|
+
const clauseType = statement.importClause.isTypeOnly ? "type " : "";
|
|
1595
|
+
const lines = [];
|
|
1596
|
+
if (valid.length > 0) lines.push(`import ${clauseType}{ ${valid.map((entry) => entry.getText(sourceFile)).join(", ")} } from "@askrjs/askr";`);
|
|
1597
|
+
for (const [subpath, entries] of [...groups].sort(([left], [right]) => left.localeCompare(right))) lines.push(`import ${clauseType}{ ${entries.map((entry) => entry.getText(sourceFile)).join(", ")} } from "@askrjs/askr/${subpath}";`);
|
|
1598
|
+
diagnostics.push(diagnostic(context, misplaced[0], this, `Root import contains ${misplaced.length} API specifier${misplaced.length === 1 ? "" : "s"} owned by public subpaths.`, "Import each API from its documented public subpath.", {
|
|
1599
|
+
description: "Split misplaced Askr root imports by public subpath",
|
|
1600
|
+
filePath: sourceFile.fileName,
|
|
1601
|
+
start: statement.getStart(sourceFile),
|
|
1602
|
+
end: statement.getEnd(),
|
|
1603
|
+
replacement: lines.join("\n")
|
|
1604
|
+
}));
|
|
1605
|
+
}
|
|
1606
|
+
return diagnostics;
|
|
1607
|
+
}
|
|
1608
|
+
};
|
|
1609
|
+
function literalJsxString(attribute) {
|
|
1610
|
+
if (!attribute || !ts.isJsxAttribute(attribute) || !attribute.initializer) return null;
|
|
1611
|
+
if (ts.isStringLiteral(attribute.initializer)) return attribute.initializer.text;
|
|
1612
|
+
const expression = jsxExpression(attribute);
|
|
1613
|
+
return expression && ts.isStringLiteralLike(expression) ? expression.text : null;
|
|
1614
|
+
}
|
|
1615
|
+
const linkContractRule = {
|
|
1616
|
+
id: "askr/link-contract",
|
|
1617
|
+
category: "correctness",
|
|
1618
|
+
severity: "error",
|
|
1619
|
+
description: "Link destinations must be unambiguous and use runtime-safe schemes.",
|
|
1620
|
+
analyze(context) {
|
|
1621
|
+
const diagnostics = [];
|
|
1622
|
+
const unsafe = /^(?:javascript|data|vbscript):/i;
|
|
1623
|
+
for (const sourceFile of context.sourceFiles) {
|
|
1624
|
+
const bindings = sourceBindings(sourceFile);
|
|
1625
|
+
if (!sourceFacts(sourceFile).jsx.some((fact) => fact.name === "Link")) continue;
|
|
1626
|
+
visit(sourceFile, (node) => {
|
|
1627
|
+
if (!ts.isJsxOpeningElement(node) && !ts.isJsxSelfClosingElement(node)) return;
|
|
1628
|
+
if (canonicalJsxName(node.tagName, bindings) !== "Link") return;
|
|
1629
|
+
const attributes = jsxAttributes(node);
|
|
1630
|
+
if ([...attributes.values()].some(ts.isJsxSpreadAttribute)) return;
|
|
1631
|
+
const to = attributes.get("to");
|
|
1632
|
+
const href = attributes.get("href");
|
|
1633
|
+
if (!to && !href) diagnostics.push(diagnostic(context, node.tagName, this, "<Link> requires a to or href destination."));
|
|
1634
|
+
else if (to && href) diagnostics.push(diagnostic(context, node.tagName, this, "<Link> cannot specify both to and href."));
|
|
1635
|
+
const destination = literalJsxString(href ?? to);
|
|
1636
|
+
if (destination && unsafe.test(destination.trim())) diagnostics.push(diagnostic(context, href ?? to ?? node.tagName, this, `<Link> uses the unsafe '${destination.split(":")[0]}:' URL scheme.`, "Use http, https, mailto, tel, sms, a relative URL, or a route reference."));
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
return diagnostics;
|
|
1640
|
+
}
|
|
1641
|
+
};
|
|
1642
|
+
function packageName(manifest) {
|
|
1643
|
+
return typeof manifest.name === "string" ? manifest.name : "";
|
|
1644
|
+
}
|
|
1645
|
+
const ANALYZE_RULES = [
|
|
1646
|
+
parseErrorRule,
|
|
1647
|
+
stableControlBoundaryRule,
|
|
1228
1648
|
stableRenderRule,
|
|
1649
|
+
renderScopeRequiredRule,
|
|
1650
|
+
exhaustiveDependenciesRule,
|
|
1651
|
+
forRowClosureCaptureRule,
|
|
1652
|
+
stableModuleIdentityRule,
|
|
1653
|
+
queryKeyContractRule,
|
|
1229
1654
|
stateAccessRule,
|
|
1230
1655
|
stateRenderWriteRule,
|
|
1231
1656
|
resourceCancellationRule,
|
|
@@ -1233,6 +1658,7 @@ const ANALYZE_RULES = [
|
|
|
1233
1658
|
lifecycleContractRule,
|
|
1234
1659
|
streamContractRule,
|
|
1235
1660
|
dataContractRule,
|
|
1661
|
+
linkContractRule,
|
|
1236
1662
|
invalidationContractRule,
|
|
1237
1663
|
forContractRule,
|
|
1238
1664
|
controlContractRule,
|
|
@@ -1241,6 +1667,7 @@ const ANALYZE_RULES = [
|
|
|
1241
1667
|
asyncComponentRule,
|
|
1242
1668
|
routeRegistryRule,
|
|
1243
1669
|
routePathRule,
|
|
1670
|
+
routeScopeStructureRule,
|
|
1244
1671
|
dataCancellationRule,
|
|
1245
1672
|
bootRegistryRule,
|
|
1246
1673
|
islandContractRule,
|
|
@@ -1248,55 +1675,90 @@ const ANALYZE_RULES = [
|
|
|
1248
1675
|
actionContractRule,
|
|
1249
1676
|
actionPromiseRule,
|
|
1250
1677
|
renderAllocationRule,
|
|
1251
|
-
ssrGlobalsRule,
|
|
1252
1678
|
{
|
|
1253
|
-
id: "askr/
|
|
1254
|
-
category: "
|
|
1255
|
-
severity: "
|
|
1256
|
-
description: "
|
|
1679
|
+
id: "askr/no-hardcoded-theme-token",
|
|
1680
|
+
category: "correctness",
|
|
1681
|
+
severity: "warning",
|
|
1682
|
+
description: "Runtime UI literals should use semantic theme tokens.",
|
|
1257
1683
|
analyze(context) {
|
|
1684
|
+
if (["@askrjs/askr", "@askrjs/themes"].includes(packageName(context.workspace.manifest))) return [];
|
|
1258
1685
|
const diagnostics = [];
|
|
1259
|
-
const
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
const parsed = JSON.parse(tsconfigSource);
|
|
1267
|
-
parsed.compilerOptions = {
|
|
1268
|
-
...parsed.compilerOptions && typeof parsed.compilerOptions === "object" && !Array.isArray(parsed.compilerOptions) ? parsed.compilerOptions : {},
|
|
1269
|
-
jsx: "react-jsx",
|
|
1270
|
-
jsxImportSource: "@askrjs/askr"
|
|
1271
|
-
};
|
|
1272
|
-
fix = {
|
|
1273
|
-
description: "Configure TypeScript to use the Askr JSX runtime",
|
|
1274
|
-
filePath: tsconfigPath,
|
|
1275
|
-
start: 0,
|
|
1276
|
-
end: tsconfigSource.length,
|
|
1277
|
-
replacement: `${JSON.stringify(parsed, null, 2)}\n`
|
|
1278
|
-
};
|
|
1279
|
-
} catch {}
|
|
1280
|
-
diagnostics.push(diagnostic(context, tsx, this, "TSX is present but compilerOptions.jsxImportSource is not '@askrjs/askr'.", "Set jsx to react-jsx and jsxImportSource to @askrjs/askr.", fix));
|
|
1686
|
+
const color = /(?:#[0-9a-f]{3,8}\b|\brgba?\s*\(|\bhsla?\s*\()/i;
|
|
1687
|
+
for (const sourceFile of context.sourceFiles) {
|
|
1688
|
+
if (/(?:^|[./_-])(?:test|spec)\.[cm]?[jt]sx?$/.test(sourceFile.fileName)) continue;
|
|
1689
|
+
if (!color.test(sourceFile.text)) continue;
|
|
1690
|
+
visit(sourceFile, (node) => {
|
|
1691
|
+
if ((ts.isStringLiteralLike(node) || ts.isNoSubstitutionTemplateLiteral(node)) && color.test(node.text)) diagnostics.push(diagnostic(context, node, this, `Runtime UI literal '${node.text}' hardcodes a color instead of a theme token.`, "Use a semantic prop, theme variable, or design-system class."));
|
|
1692
|
+
});
|
|
1281
1693
|
}
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1694
|
+
return diagnostics;
|
|
1695
|
+
}
|
|
1696
|
+
},
|
|
1697
|
+
{
|
|
1698
|
+
id: "askr/no-effect-data-loading",
|
|
1699
|
+
category: "correctness",
|
|
1700
|
+
severity: "warning",
|
|
1701
|
+
description: "Fetch-to-state data loading should use resource rather than task effects.",
|
|
1702
|
+
analyze(context) {
|
|
1703
|
+
const diagnostics = [];
|
|
1704
|
+
for (const sourceFile of context.sourceFiles) {
|
|
1705
|
+
const bindings = sourceBindings(sourceFile);
|
|
1706
|
+
if (!sourceFacts(sourceFile).calls.some((fact) => fact.name === "task")) continue;
|
|
1707
|
+
const state = collectStateBindings(sourceFile, bindings);
|
|
1708
|
+
for (const { node, name } of sourceFacts(sourceFile).calls) {
|
|
1709
|
+
if (name !== "task") continue;
|
|
1710
|
+
const callback = node.arguments[0];
|
|
1711
|
+
if (!callback || !ts.isArrowFunction(callback) && !ts.isFunctionExpression(callback) || !/\bfetch\s*\(/.test(callback.body.getText())) continue;
|
|
1712
|
+
let writesState = false;
|
|
1713
|
+
const walk = (candidate) => {
|
|
1714
|
+
if (ts.isCallExpression(candidate) && ts.isIdentifier(candidate.expression) && state.setters.has(candidate.expression.text)) writesState = true;
|
|
1715
|
+
ts.forEachChild(candidate, walk);
|
|
1716
|
+
};
|
|
1717
|
+
walk(callback.body);
|
|
1718
|
+
if (!writesState) continue;
|
|
1719
|
+
diagnostics.push(diagnostic(context, callback, this, "task() fetches data and writes it into same-component state.", "Use resource() so cancellation, dependencies, loading, and errors are lifecycle-owned."));
|
|
1288
1720
|
}
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1721
|
+
}
|
|
1722
|
+
return diagnostics;
|
|
1723
|
+
}
|
|
1724
|
+
},
|
|
1725
|
+
{
|
|
1726
|
+
id: "askr/testing-contract",
|
|
1727
|
+
category: "correctness",
|
|
1728
|
+
severity: "error",
|
|
1729
|
+
description: "Canonical test dispatches must be synchronously flushed before assertions.",
|
|
1730
|
+
analyze(context) {
|
|
1731
|
+
const diagnostics = [];
|
|
1732
|
+
for (const sourceFile of context.sourceFiles) {
|
|
1733
|
+
let canonicalDispatch = false;
|
|
1734
|
+
for (const statement of sourceFile.statements) if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier) && statement.moduleSpecifier.text.endsWith("@askrjs/askr/testing")) canonicalDispatch = statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings) ? statement.importClause.namedBindings.elements.some((element) => (element.propertyName?.text ?? element.name.text) === "dispatch") : false;
|
|
1735
|
+
if (!canonicalDispatch) continue;
|
|
1736
|
+
visit(sourceFile, (node) => {
|
|
1737
|
+
if (!ts.isBlock(node)) return;
|
|
1738
|
+
let pending = null;
|
|
1739
|
+
for (const statement of node.statements) {
|
|
1740
|
+
const text = statement.getText();
|
|
1741
|
+
let dispatch = null;
|
|
1742
|
+
const findDispatch = (candidate) => {
|
|
1743
|
+
if (ts.isCallExpression(candidate) && ts.isIdentifier(candidate.expression) && candidate.expression.text === "dispatch") dispatch = candidate;
|
|
1744
|
+
ts.forEachChild(candidate, findDispatch);
|
|
1745
|
+
};
|
|
1746
|
+
findDispatch(statement);
|
|
1747
|
+
if (dispatch) pending = dispatch;
|
|
1748
|
+
if (pending && /(?:^|\.)(?:flush)\s*\(/.test(text)) pending = null;
|
|
1749
|
+
if (pending && /\b(?:expect|assert)\s*\(/.test(text)) {
|
|
1750
|
+
diagnostics.push(diagnostic(context, pending, this, "dispatch() reaches an assertion without a synchronous flush().", "Call flush() or result.flush() before the next assertion."));
|
|
1751
|
+
pending = null;
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1292
1754
|
});
|
|
1293
|
-
const dependencyPresent = hasDependency(context.workspace.manifest, "@askrjs/vite");
|
|
1294
|
-
if (dependencyPresent && pluginCalled) return diagnostics;
|
|
1295
|
-
diagnostics.push(diagnostic(context, viteConfig, this, !dependencyPresent ? "This Askr Vite project does not declare @askrjs/vite." : "Vite is configured without calling the @askrjs/vite askr() plugin.", "Declare @askrjs/vite, import askr, and include askr() in the plugin list."));
|
|
1296
1755
|
}
|
|
1297
1756
|
return diagnostics;
|
|
1298
1757
|
}
|
|
1299
|
-
}
|
|
1758
|
+
},
|
|
1759
|
+
ssrGlobalsRule,
|
|
1760
|
+
frameworkConfigRule,
|
|
1761
|
+
importSubpathRule
|
|
1300
1762
|
];
|
|
1301
1763
|
function configuredSeverity(rule, configuration) {
|
|
1302
1764
|
return configuration.rules[rule.id] ?? rule.severity;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askrjs/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.17",
|
|
4
4
|
"description": "Unified CLI for the Askr platform",
|
|
5
5
|
"homepage": "https://github.com/askrjs/askr-cli#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"@npmcli/config": "^10.12.0",
|
|
56
|
-
"js-yaml": "^5.2.
|
|
57
|
-
"minimatch": "^10.2.
|
|
56
|
+
"js-yaml": "^5.2.2",
|
|
57
|
+
"minimatch": "^10.2.6",
|
|
58
58
|
"npm-registry-fetch": "^19.1.1",
|
|
59
59
|
"semver": "^7.8.5",
|
|
60
60
|
"tsx": "^4.23.1",
|