@stll/oxlint-config 1.3.1 → 1.4.0
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/README.md +254 -17
- package/dist/cloud-setup-cli.cjs +18 -38
- package/dist/cloud-setup-cli.cjs.map +1 -1
- package/dist/cloud-setup-cli.mjs +21 -41
- package/dist/cloud-setup-cli.mjs.map +1 -1
- package/dist/consumer-compat-cli.cjs +50 -249
- package/dist/consumer-compat-cli.cjs.map +1 -1
- package/dist/consumer-compat-cli.mjs +6 -205
- package/dist/consumer-compat-cli.mjs.map +1 -1
- package/dist/consumer-node-support.cjs +497 -0
- package/dist/consumer-node-support.cjs.map +1 -0
- package/dist/consumer-node-support.mjs +381 -0
- package/dist/consumer-node-support.mjs.map +1 -0
- package/dist/dependabot-cli.cjs +67 -0
- package/dist/dependabot-cli.cjs.map +1 -0
- package/dist/dependabot-cli.d.cts +1 -0
- package/dist/dependabot-cli.d.mts +1 -0
- package/dist/dependabot-cli.mjs +67 -0
- package/dist/dependabot-cli.mjs.map +1 -0
- package/dist/dependabot.cjs +5 -0
- package/dist/dependabot.d.cts +108 -0
- package/dist/dependabot.d.mts +108 -0
- package/dist/dependabot.mjs +3 -0
- package/dist/generated-file.cjs +61 -0
- package/dist/generated-file.cjs.map +1 -0
- package/dist/generated-file.mjs +44 -0
- package/dist/generated-file.mjs.map +1 -0
- package/dist/publish-artifact-syntax.cjs +1309 -0
- package/dist/publish-artifact-syntax.cjs.map +1 -0
- package/dist/publish-artifact-syntax.mjs +1265 -0
- package/dist/publish-artifact-syntax.mjs.map +1 -0
- package/dist/publish-contract-cli.cjs +53 -12
- package/dist/publish-contract-cli.cjs.map +1 -1
- package/dist/publish-contract-cli.mjs +49 -8
- package/dist/publish-contract-cli.mjs.map +1 -1
- package/dist/toolchain-changed.cjs +5 -1
- package/dist/toolchain-changed.cjs.map +1 -1
- package/dist/toolchain-changed.mjs +6 -2
- package/dist/toolchain-changed.mjs.map +1 -1
- package/dist/toolchain-dependabot.cjs +455 -0
- package/dist/toolchain-dependabot.cjs.map +1 -0
- package/dist/toolchain-dependabot.mjs +423 -0
- package/dist/toolchain-dependabot.mjs.map +1 -0
- package/dist/toolchain-guard.cjs +1096 -424
- package/dist/toolchain-guard.cjs.map +1 -1
- package/dist/toolchain-guard.mjs +1085 -413
- package/dist/toolchain-guard.mjs.map +1 -1
- package/dist/toolchain-inputs.cjs +113 -21
- package/dist/toolchain-inputs.cjs.map +1 -1
- package/dist/toolchain-inputs.mjs +108 -22
- package/dist/toolchain-inputs.mjs.map +1 -1
- package/dist/toolchain-schema.cjs +0 -493
- package/dist/toolchain-schema.cjs.map +1 -1
- package/dist/toolchain-schema.mjs +3 -379
- package/dist/toolchain-schema.mjs.map +1 -1
- package/dist/typecheck-parity.cjs +1 -1
- package/dist/typecheck-probe-cli.cjs +1 -1
- package/dist/typecheck-probe-cli.cjs.map +1 -1
- package/dist/typecheck-probe-cli.mjs +1 -1
- package/dist/typecheck-probe-cli.mjs.map +1 -1
- package/package.json +13 -1
- package/dist/publish-contract.cjs +0 -431
- package/dist/publish-contract.cjs.map +0 -1
- package/dist/publish-contract.mjs +0 -396
- package/dist/publish-contract.mjs.map +0 -1
package/dist/toolchain-guard.cjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
const require_rolldown_runtime = require("./rolldown-runtime.cjs");
|
|
2
2
|
const require_toolchain_inputs = require("./toolchain-inputs.cjs");
|
|
3
3
|
const require_git_tracked_output = require("./git-tracked-output.cjs");
|
|
4
|
-
const
|
|
4
|
+
const require_consumer_node_support = require("./consumer-node-support.cjs");
|
|
5
|
+
const require_toolchain_dependabot = require("./toolchain-dependabot.cjs");
|
|
6
|
+
require("./toolchain-schema.cjs");
|
|
5
7
|
let node_fs = require("node:fs");
|
|
6
8
|
let node_child_process = require("node:child_process");
|
|
7
9
|
let node_path = require("node:path");
|
|
@@ -11,11 +13,12 @@ let smol_toml = require("smol-toml");
|
|
|
11
13
|
let yaml = require("yaml");
|
|
12
14
|
let picomatch = require("picomatch");
|
|
13
15
|
picomatch = require_rolldown_runtime.__toESM(picomatch, 1);
|
|
16
|
+
let node_module = require("node:module");
|
|
14
17
|
//#endregion
|
|
15
18
|
//#region src/toolchain-consumer-checks.ts
|
|
16
|
-
const consumerRunnerVersion = "1.
|
|
19
|
+
const consumerRunnerVersion = "1.4.0";
|
|
17
20
|
const consumerCheckWorkflow = "stella/.github/.github/workflows/package-consumer-compat.yml";
|
|
18
|
-
const record$
|
|
21
|
+
const record$10 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
19
22
|
const repositoryDirectory = (value) => typeof value === "string" && value !== "" && !value.includes("\\") && !value.includes("${{") && !value.split("").some((character) => {
|
|
20
23
|
const code = character.charCodeAt(0);
|
|
21
24
|
return code < 32 || code === 127;
|
|
@@ -101,12 +104,12 @@ const validConsumerCron = (text) => {
|
|
|
101
104
|
});
|
|
102
105
|
};
|
|
103
106
|
const nightlySchedule = (value) => {
|
|
104
|
-
if (!record$
|
|
107
|
+
if (!record$10(value)) return false;
|
|
105
108
|
const schedule = value["schedule"];
|
|
106
109
|
if (!Array.isArray(schedule) || schedule.length === 0) return false;
|
|
107
110
|
const coveredDays = /* @__PURE__ */ new Set();
|
|
108
111
|
for (const entry of schedule) {
|
|
109
|
-
if (!record$
|
|
112
|
+
if (!record$10(entry) || typeof entry["cron"] !== "string" || !validConsumerCron(entry["cron"])) return false;
|
|
110
113
|
const fields = entry["cron"].trim().split(/\s+/);
|
|
111
114
|
const selections = cronFields.map((field, index) => {
|
|
112
115
|
const text = fields.at(index);
|
|
@@ -122,7 +125,7 @@ const parseConsumerChecks = (input) => {
|
|
|
122
125
|
if (!Array.isArray(input)) throw new Error("consumerChecks must be an array");
|
|
123
126
|
const targets = /* @__PURE__ */ new Set();
|
|
124
127
|
return input.map((entry) => {
|
|
125
|
-
if (!record$
|
|
128
|
+
if (!record$10(entry) || Object.keys(entry).some((key) => ![
|
|
126
129
|
"workflow",
|
|
127
130
|
"job",
|
|
128
131
|
"packages",
|
|
@@ -160,9 +163,9 @@ const checkConsumerChecks = ({ declarations, files, policy }) => {
|
|
|
160
163
|
});
|
|
161
164
|
if (document.errors.length > 0) throw new Error("invalid workflow YAML");
|
|
162
165
|
const source = document.toJS({ maxAliasCount: 100 });
|
|
163
|
-
if (!record$
|
|
166
|
+
if (!record$10(source) || !record$10(source["jobs"])) continue;
|
|
164
167
|
for (const [jobName, job] of Object.entries(source["jobs"])) {
|
|
165
|
-
if (!record$
|
|
168
|
+
if (!record$10(job) || typeof job["uses"] !== "string") continue;
|
|
166
169
|
const [workflowPath, ref] = job["uses"].split("@");
|
|
167
170
|
if (workflowPath?.toLowerCase() !== "stella/.github/.github/workflows/package-consumer-compat.yml") continue;
|
|
168
171
|
const declaration = declarations.find((entry) => entry.workflow === workflow && entry.job === jobName);
|
|
@@ -176,7 +179,7 @@ const checkConsumerChecks = ({ declarations, files, policy }) => {
|
|
|
176
179
|
const approved = Object.entries(policy.actions).find(([name]) => name.toLowerCase() === consumerCheckWorkflow)?.[1];
|
|
177
180
|
if (job["uses"].split("@").length !== 2 || ref === void 0 || !/^[a-f0-9]{40}$/.test(ref) || approved === void 0 || ref !== approved.sha || "steps" in job || "runs-on" in job) throw new Error(`${workflow}:${jobName} must invoke the approved immutable consumer workflow as a reusable job`);
|
|
178
181
|
const inputs = job["with"];
|
|
179
|
-
if (!record$
|
|
182
|
+
if (!record$10(inputs)) throw new Error(`${workflow}:${jobName} requires consumer inputs`);
|
|
180
183
|
const names = /* @__PURE__ */ new Set();
|
|
181
184
|
for (const key of Object.keys(inputs)) {
|
|
182
185
|
const normalized = key.toLowerCase();
|
|
@@ -196,8 +199,8 @@ const checkConsumerChecks = ({ declarations, files, policy }) => {
|
|
|
196
199
|
const fixtureSource = files[fixtureManifest];
|
|
197
200
|
if (fixtureSource === void 0) throw new Error(`consumerChecks fixture configuration must be tracked: ${fixtureManifest}`);
|
|
198
201
|
const fixtureConfiguration = JSON.parse(fixtureSource);
|
|
199
|
-
const fixtures =
|
|
200
|
-
|
|
202
|
+
const fixtures = require_consumer_node_support.parseConsumerFixtures(fixtureConfiguration);
|
|
203
|
+
require_consumer_node_support.assertConsumerFixtureSelection({
|
|
201
204
|
selected: declaration.packages,
|
|
202
205
|
fixtures
|
|
203
206
|
});
|
|
@@ -205,7 +208,7 @@ const checkConsumerChecks = ({ declarations, files, policy }) => {
|
|
|
205
208
|
if (typeof rawPackages !== "string") throw new Error(`${workflow}:${jobName} packages must be a static JSON array`);
|
|
206
209
|
const selected = JSON.parse(rawPackages);
|
|
207
210
|
if (!packageDirectories(selected) || selected.length !== declaration.packages.length || !selected.every((directory) => declaration.packages.includes(directory))) throw new Error(`${workflow}:${jobName} packages must exactly match consumerChecks`);
|
|
208
|
-
const workspacePackages =
|
|
211
|
+
const workspacePackages = require_consumer_node_support.discoverConsumerPackages(files);
|
|
209
212
|
const packagesByDirectory = new Map([...workspacePackages.values()].map((entry) => [entry.directory, entry]));
|
|
210
213
|
for (const directory of declaration.packages) {
|
|
211
214
|
const manifestPath = node_path.default.posix.join(directory, "package.json");
|
|
@@ -214,18 +217,18 @@ const checkConsumerChecks = ({ declarations, files, policy }) => {
|
|
|
214
217
|
if (selectedPackage === void 0) throw new Error(`consumerChecks package must be a discovered workspace member: ${manifestPath}`);
|
|
215
218
|
const fixture = fixtures.find((candidate) => candidate.package === directory);
|
|
216
219
|
if (fixture === void 0) throw new Error(`missing declared consumer fixture: ${directory}`);
|
|
217
|
-
|
|
220
|
+
require_consumer_node_support.assertConsumerFixtureKind({
|
|
218
221
|
fixture,
|
|
219
222
|
pkg: selectedPackage
|
|
220
223
|
});
|
|
221
224
|
const manifest = selectedPackage.manifest;
|
|
222
225
|
if (manifest["private"] === true || typeof manifest["name"] !== "string" || manifest["name"].trim() === "") throw new Error(`consumerChecks requires a named published package: ${manifestPath}`);
|
|
223
|
-
const closure =
|
|
226
|
+
const closure = require_consumer_node_support.consumerPackageClosure({
|
|
224
227
|
selected: selectedPackage,
|
|
225
228
|
packages: workspacePackages,
|
|
226
229
|
files
|
|
227
230
|
});
|
|
228
|
-
for (const member of closure.values())
|
|
231
|
+
for (const member of closure.values()) require_consumer_node_support.assertConsumerNodeSupport({
|
|
229
232
|
manifest: member.manifest,
|
|
230
233
|
node: policy.consumerNode,
|
|
231
234
|
label: node_path.default.posix.join(member.directory, "package.json")
|
|
@@ -240,367 +243,13 @@ const checkConsumerChecks = ({ declarations, files, policy }) => {
|
|
|
240
243
|
return diagnostics;
|
|
241
244
|
};
|
|
242
245
|
//#endregion
|
|
243
|
-
//#region src/toolchain-images.ts
|
|
244
|
-
/** Docker Hub spellings accepted by the shared runtime policy. */
|
|
245
|
-
const dockerHubRegistries = [
|
|
246
|
-
"docker.io",
|
|
247
|
-
"index.docker.io",
|
|
248
|
-
"registry-1.docker.io"
|
|
249
|
-
];
|
|
250
|
-
/** Expand the full accepted repository alias class without parsing Dockerfile instructions. */
|
|
251
|
-
const ownedDockerImageAliases = (images) => {
|
|
252
|
-
const aliases = /* @__PURE__ */ new Set();
|
|
253
|
-
for (const image of images) for (const repository of [image, `library/${image}`]) {
|
|
254
|
-
aliases.add(repository);
|
|
255
|
-
for (const registry of dockerHubRegistries) aliases.add(`${registry}/${repository}`);
|
|
256
|
-
}
|
|
257
|
-
return [...aliases].sort();
|
|
258
|
-
};
|
|
259
|
-
/** Preserve tag/digest spelling while normalizing registry and namespace aliases. */
|
|
260
|
-
const canonicalDockerRuntime = (image) => {
|
|
261
|
-
const registry = dockerHubRegistries.find((entry) => image.toLowerCase().startsWith(`${entry}/`));
|
|
262
|
-
return (registry === void 0 ? image : image.slice(registry.length + 1)).replace(/^library\//i, "");
|
|
263
|
-
};
|
|
264
|
-
/** Preserve Rust release identity across official Debian slim and Alpine variants. */
|
|
265
|
-
const rustImageVersion = (tag) => tag.replace(/-(?:alpine(?:\d+(?:\.\d+)*)?|(?:bookworm|bullseye|trixie|buster)(?:-slim)?|slim(?:-(?:bookworm|bullseye|trixie|buster))?)$/, "");
|
|
266
|
-
//#endregion
|
|
267
|
-
//#region src/toolchain-dependabot.ts
|
|
268
|
-
const dependabotRules = ["dependabot-policy"];
|
|
269
|
-
const record$6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
270
|
-
const strings = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
271
|
-
const sameStrings = (value, expected) => {
|
|
272
|
-
if (!strings(value) || value.length !== expected.length) return false;
|
|
273
|
-
const sortedExpected = [...expected].sort();
|
|
274
|
-
return [...value].sort().every((entry, index) => entry === sortedExpected[index]);
|
|
275
|
-
};
|
|
276
|
-
const pinnedVersionUpdateTypes = [
|
|
277
|
-
"version-update:semver-major",
|
|
278
|
-
"version-update:semver-minor",
|
|
279
|
-
"version-update:semver-patch"
|
|
280
|
-
];
|
|
281
|
-
const versionOnlyIgnore = (entry) => record$6(entry) && Object.keys(entry).length === 2 && typeof entry["dependency-name"] === "string" && sameStrings(entry["update-types"], pinnedVersionUpdateTypes);
|
|
282
|
-
const directoryOf = (file) => {
|
|
283
|
-
const directory = node_path.default.posix.dirname(file);
|
|
284
|
-
return directory === "." ? "/" : `/${directory}`;
|
|
285
|
-
};
|
|
286
|
-
const workspaceManifests = {
|
|
287
|
-
npm: "package.json",
|
|
288
|
-
cargo: "Cargo.toml",
|
|
289
|
-
uv: "pyproject.toml"
|
|
290
|
-
};
|
|
291
|
-
const workspaceTable = (parsed, ecosystem) => {
|
|
292
|
-
switch (ecosystem) {
|
|
293
|
-
case "npm": return parsed["workspaces"];
|
|
294
|
-
case "cargo": return parsed["workspace"];
|
|
295
|
-
case "uv": {
|
|
296
|
-
const tool = parsed["tool"];
|
|
297
|
-
const uv = record$6(tool) ? tool["uv"] : void 0;
|
|
298
|
-
return record$6(uv) ? uv["workspace"] : void 0;
|
|
299
|
-
}
|
|
300
|
-
default: throw new Error("unknown workspace ecosystem", { cause: ecosystem });
|
|
301
|
-
}
|
|
302
|
-
};
|
|
303
|
-
/** Installable workspace members share their update root; own-lock projects stay discoverable. */
|
|
304
|
-
const workspaceRoots = (files, ecosystem) => {
|
|
305
|
-
const manifests = Object.keys(files).filter((file) => node_path.default.posix.basename(file) === workspaceManifests[ecosystem] || ecosystem === "npm" && node_path.default.posix.basename(file) === "pnpm-workspace.yaml");
|
|
306
|
-
const workspaces = /* @__PURE__ */ new Map();
|
|
307
|
-
const npmWorkspaceErrors = /* @__PURE__ */ new Map();
|
|
308
|
-
for (const file of manifests) {
|
|
309
|
-
if (ecosystem === "npm") {
|
|
310
|
-
const directory = node_path.default.posix.dirname(file);
|
|
311
|
-
const workspaceFile = node_path.default.posix.join(directory, "pnpm-workspace.yaml");
|
|
312
|
-
const text = files[workspaceFile];
|
|
313
|
-
if (text !== void 0) {
|
|
314
|
-
try {
|
|
315
|
-
const document = (0, yaml.parseDocument)(text);
|
|
316
|
-
if (document.errors.length > 0) throw new Error(`${workspaceFile}:1: invalid pnpm workspace YAML`);
|
|
317
|
-
const parsed = document.toJS({ maxAliasCount: 100 });
|
|
318
|
-
if (!record$6(parsed)) throw new Error(`${workspaceFile}:1: pnpm workspace must contain an object`);
|
|
319
|
-
const patterns = parsed["packages"] ?? [];
|
|
320
|
-
if (!strings(patterns)) throw new Error(`${workspaceFile}:1: pnpm packages must be a string array`);
|
|
321
|
-
workspaces.set(directory, patterns.map((pattern) => pattern.startsWith("!") ? `!${node_path.default.posix.normalize(pattern.slice(1))}` : node_path.default.posix.normalize(pattern)));
|
|
322
|
-
} catch (error) {
|
|
323
|
-
npmWorkspaceErrors.set(directory, error instanceof Error ? error : new Error(String(error)));
|
|
324
|
-
}
|
|
325
|
-
continue;
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
try {
|
|
329
|
-
const parsed = ecosystem === "npm" ? JSON.parse(files[file] ?? "") : (0, smol_toml.parse)(files[file] ?? "");
|
|
330
|
-
if (!record$6(parsed)) continue;
|
|
331
|
-
if (ecosystem === "npm" && typeof parsed["packageManager"] === "string" && parsed["packageManager"].startsWith("pnpm@")) continue;
|
|
332
|
-
const value = workspaceTable(parsed, ecosystem);
|
|
333
|
-
const patterns = record$6(value) ? value[ecosystem === "npm" ? "packages" : "members"] : value;
|
|
334
|
-
const members = ecosystem === "cargo" && record$6(value) && patterns === void 0 ? [] : patterns;
|
|
335
|
-
if (strings(members)) {
|
|
336
|
-
const excluded = ecosystem !== "npm" && record$6(value) && strings(value["exclude"]) ? value["exclude"].map((pattern) => `!${pattern}`) : [];
|
|
337
|
-
workspaces.set(node_path.default.posix.dirname(file), [...members, ...excluded]);
|
|
338
|
-
}
|
|
339
|
-
} catch {}
|
|
340
|
-
}
|
|
341
|
-
const implicitCargoMembers = /* @__PURE__ */ new Map();
|
|
342
|
-
const declaredMember = (root, patterns, directory) => {
|
|
343
|
-
const relative = node_path.default.posix.relative(root, directory);
|
|
344
|
-
if (relative === "" || relative === ".." || relative.startsWith("../")) return false;
|
|
345
|
-
return require_toolchain_inputs.workspaceContains({
|
|
346
|
-
directory: root,
|
|
347
|
-
file: node_path.default.posix.join(directory, "package.json"),
|
|
348
|
-
patterns,
|
|
349
|
-
rootMembership: "patterns"
|
|
350
|
-
});
|
|
351
|
-
};
|
|
352
|
-
const installable = /* @__PURE__ */ new Set(["."]);
|
|
353
|
-
if (ecosystem === "npm") {
|
|
354
|
-
for (const file of manifests) {
|
|
355
|
-
const directory = node_path.default.posix.dirname(file);
|
|
356
|
-
if (require_toolchain_inputs.javascriptDependencyLockfiles.some((name) => files[node_path.default.posix.join(directory, name)] !== void 0)) installable.add(directory);
|
|
357
|
-
}
|
|
358
|
-
let changed = true;
|
|
359
|
-
while (changed) {
|
|
360
|
-
changed = false;
|
|
361
|
-
for (const [root, patterns] of workspaces) {
|
|
362
|
-
if (!installable.has(root)) continue;
|
|
363
|
-
for (const file of manifests) {
|
|
364
|
-
const directory = node_path.default.posix.dirname(file);
|
|
365
|
-
if (installable.has(directory) || !declaredMember(root, patterns, directory)) continue;
|
|
366
|
-
installable.add(directory);
|
|
367
|
-
changed = true;
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
if (ecosystem === "npm") {
|
|
373
|
-
for (const [directory, error] of npmWorkspaceErrors) if (installable.has(directory)) throw error;
|
|
374
|
-
}
|
|
375
|
-
if (ecosystem === "cargo") for (const [root, patterns] of workspaces) {
|
|
376
|
-
const includes = patterns.filter((pattern) => !pattern.startsWith("!"));
|
|
377
|
-
const excludes = patterns.filter((pattern) => pattern.startsWith("!"));
|
|
378
|
-
const memberDirectories = /* @__PURE__ */ new Set([root]);
|
|
379
|
-
for (const file of manifests) {
|
|
380
|
-
const directory = node_path.default.posix.dirname(file);
|
|
381
|
-
const relative = node_path.default.posix.relative(root, directory);
|
|
382
|
-
if (includes.some((pattern) => picomatch.default.isMatch(relative, pattern)) && !excludes.some((pattern) => picomatch.default.isMatch(relative, pattern.slice(1)))) memberDirectories.add(directory);
|
|
383
|
-
}
|
|
384
|
-
for (const directory of memberDirectories) try {
|
|
385
|
-
const parsed = (0, smol_toml.parse)(files[node_path.default.posix.join(directory, "Cargo.toml")] ?? "");
|
|
386
|
-
const workspace = (0, smol_toml.parse)(files[node_path.default.posix.join(root, "Cargo.toml")] ?? "")["workspace"];
|
|
387
|
-
const inherited = record$6(workspace) ? workspace["dependencies"] : void 0;
|
|
388
|
-
const dependencyOwners = [parsed];
|
|
389
|
-
const targets = parsed["target"];
|
|
390
|
-
if (record$6(targets)) {
|
|
391
|
-
for (const target of Object.values(targets)) if (record$6(target)) dependencyOwners.push(target);
|
|
392
|
-
}
|
|
393
|
-
for (const owner of dependencyOwners) for (const key of [
|
|
394
|
-
"dependencies",
|
|
395
|
-
"dev-dependencies",
|
|
396
|
-
"build-dependencies"
|
|
397
|
-
]) {
|
|
398
|
-
const dependencies = owner[key];
|
|
399
|
-
if (!record$6(dependencies)) continue;
|
|
400
|
-
for (const [name, dependency] of Object.entries(dependencies)) {
|
|
401
|
-
if (!record$6(dependency)) continue;
|
|
402
|
-
const isInherited = dependency["workspace"] === true;
|
|
403
|
-
const resolved = isInherited && record$6(inherited) ? inherited[name] : dependency;
|
|
404
|
-
if (!record$6(resolved) || typeof resolved["path"] !== "string") continue;
|
|
405
|
-
if (node_path.default.posix.isAbsolute(resolved["path"]) || /\\|\$|^[A-Za-z]:/.test(resolved["path"])) continue;
|
|
406
|
-
const target = node_path.default.posix.normalize(node_path.default.posix.join(isInherited ? root : directory, resolved["path"]));
|
|
407
|
-
const relative = node_path.default.posix.relative(root, target);
|
|
408
|
-
if (relative.startsWith("../") || relative === ".." || files[node_path.default.posix.join(target, "Cargo.toml")] === void 0 || excludes.some((pattern) => picomatch.default.isMatch(relative, pattern.slice(1)))) continue;
|
|
409
|
-
memberDirectories.add(target);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
} catch {}
|
|
413
|
-
implicitCargoMembers.set(root, memberDirectories);
|
|
414
|
-
}
|
|
415
|
-
return manifests.filter((file) => ecosystem !== "npm" || installable.has(node_path.default.posix.dirname(file))).filter((file) => {
|
|
416
|
-
const directory = node_path.default.posix.dirname(file);
|
|
417
|
-
return ![...workspaces].some(([root, patterns]) => {
|
|
418
|
-
if (ecosystem === "npm" && !installable.has(root)) return false;
|
|
419
|
-
const relative = node_path.default.posix.relative(root, directory);
|
|
420
|
-
if (relative === "" || relative.startsWith("../")) return false;
|
|
421
|
-
const includes = patterns.filter((pattern) => !pattern.startsWith("!"));
|
|
422
|
-
const excludes = patterns.filter((pattern) => pattern.startsWith("!"));
|
|
423
|
-
return (implicitCargoMembers.get(root)?.has(directory) === true || (ecosystem === "npm" ? declaredMember(root, patterns, directory) : includes.some((pattern) => picomatch.default.isMatch(relative, pattern)))) && !excludes.some((pattern) => picomatch.default.isMatch(relative, pattern.slice(1)));
|
|
424
|
-
});
|
|
425
|
-
}).map(directoryOf).filter((directory, index, values) => values.indexOf(directory) === index);
|
|
426
|
-
};
|
|
427
|
-
const javascriptEcosystem = (directory, files) => {
|
|
428
|
-
const root = directory === "/" ? "." : directory.slice(1);
|
|
429
|
-
if (files[node_path.default.posix.join(root, "bun.lock")] !== void 0) return "bun";
|
|
430
|
-
try {
|
|
431
|
-
const manifest = JSON.parse(files[node_path.default.posix.join(root, "package.json")] ?? "{}");
|
|
432
|
-
if (record$6(manifest) && typeof manifest["packageManager"] === "string" && manifest["packageManager"].startsWith("bun@")) return "bun";
|
|
433
|
-
} catch {}
|
|
434
|
-
return "npm";
|
|
435
|
-
};
|
|
436
|
-
const pythonEcosystem = (file, files) => {
|
|
437
|
-
const manifestKind = require_toolchain_inputs.pythonDependencyManifestKind(file);
|
|
438
|
-
if (manifestKind === "pipfile" || manifestKind === "pipfile-lock" || manifestKind === "setup") return "pip";
|
|
439
|
-
const root = node_path.default.posix.dirname(file);
|
|
440
|
-
if (files[node_path.default.posix.join(root, "uv.lock")] !== void 0 || files[node_path.default.posix.join(root, "uv.toml")] !== void 0) return "uv";
|
|
441
|
-
const pyproject = files[node_path.default.posix.join(root, "pyproject.toml")];
|
|
442
|
-
if (pyproject !== void 0) try {
|
|
443
|
-
const tool = (0, smol_toml.parse)(pyproject)["tool"];
|
|
444
|
-
if (record$6(tool) && record$6(tool["uv"])) return "uv";
|
|
445
|
-
} catch {}
|
|
446
|
-
return "pip";
|
|
447
|
-
};
|
|
448
|
-
const ecosystemRoots = (files) => {
|
|
449
|
-
const roots = /* @__PURE__ */ new Map();
|
|
450
|
-
const requireRoot = (ecosystem, directory) => {
|
|
451
|
-
const directories = roots.get(ecosystem) ?? /* @__PURE__ */ new Set();
|
|
452
|
-
directories.add(directory);
|
|
453
|
-
roots.set(ecosystem, directories);
|
|
454
|
-
};
|
|
455
|
-
for (const directory of workspaceRoots(files, "npm")) requireRoot(javascriptEcosystem(directory, files), directory);
|
|
456
|
-
for (const directory of workspaceRoots(files, "cargo")) requireRoot("cargo", directory);
|
|
457
|
-
for (const directory of workspaceRoots(files, "uv")) {
|
|
458
|
-
const root = directory === "/" ? "." : directory.slice(1);
|
|
459
|
-
requireRoot(pythonEcosystem(node_path.default.posix.join(root, "pyproject.toml"), files), directory);
|
|
460
|
-
}
|
|
461
|
-
for (const file of Object.keys(files)) {
|
|
462
|
-
if (require_toolchain_inputs.isDependabotGithubActionsPath(file)) requireRoot("github-actions", "/");
|
|
463
|
-
const name = node_path.default.posix.basename(file);
|
|
464
|
-
if (require_toolchain_inputs.isDockerDefinitionPath(file)) requireRoot("docker", directoryOf(file));
|
|
465
|
-
if (require_toolchain_inputs.isComposeDefinitionPath(file) || require_toolchain_inputs.isKubernetesDefinitionPath(file)) for (const document of (0, yaml.parseAllDocuments)(files[file] ?? "", { merge: true })) {
|
|
466
|
-
if (document.errors.length > 0) continue;
|
|
467
|
-
try {
|
|
468
|
-
const classified = require_toolchain_inputs.containerDocumentImages(document.toJS({ maxAliasCount: 100 }), file);
|
|
469
|
-
if (classified !== void 0) requireRoot(classified.ecosystem, directoryOf(file));
|
|
470
|
-
} catch {}
|
|
471
|
-
}
|
|
472
|
-
if (name === "uv.lock" || name === "uv.toml" || name !== "pyproject.toml" && require_toolchain_inputs.isPythonDependencyManifest(file)) requireRoot(pythonEcosystem(file, files), directoryOf(file));
|
|
473
|
-
}
|
|
474
|
-
return roots;
|
|
475
|
-
};
|
|
476
|
-
const ignoredFor = (ecosystem, policy) => {
|
|
477
|
-
if (ecosystem === "npm" || ecosystem === "bun") return policy.ignoredPackages;
|
|
478
|
-
if (ecosystem === "github-actions") return policy.ignoredActions;
|
|
479
|
-
if (ecosystem === "docker" || ecosystem === "docker-compose") return ownedDockerImageAliases(policy.ignoredImages);
|
|
480
|
-
return [];
|
|
481
|
-
};
|
|
482
|
-
/** Validate the shared update policy against a hermetic tracked-file snapshot. */
|
|
483
|
-
const checkDependabot = ({ files, policy }) => {
|
|
484
|
-
const roots = ecosystemRoots(files);
|
|
485
|
-
const paths = [".github/dependabot.yml", ".github/dependabot.yaml"].filter((file) => files[file] !== void 0);
|
|
486
|
-
if (roots.size === 0 && paths.length === 0) return [];
|
|
487
|
-
const file = paths.at(0) ?? ".github/dependabot.yml";
|
|
488
|
-
const diagnostics = [];
|
|
489
|
-
const add = (message, line = 1) => {
|
|
490
|
-
diagnostics.push({
|
|
491
|
-
rule: "dependabot-policy",
|
|
492
|
-
path: file,
|
|
493
|
-
line,
|
|
494
|
-
message
|
|
495
|
-
});
|
|
496
|
-
};
|
|
497
|
-
if (paths.length === 0) {
|
|
498
|
-
add("add a Dependabot configuration for the repository's package ecosystems");
|
|
499
|
-
return diagnostics;
|
|
500
|
-
}
|
|
501
|
-
if (paths.length > 1) add("keep one Dependabot configuration file");
|
|
502
|
-
const lineCounter = new yaml.LineCounter();
|
|
503
|
-
const document = (0, yaml.parseDocument)(files[file] ?? "", { lineCounter });
|
|
504
|
-
if (document.errors.length > 0) {
|
|
505
|
-
for (const error of document.errors) add(`invalid Dependabot YAML: ${error.message}`, lineCounter.linePos(error.pos[0]).line);
|
|
506
|
-
return diagnostics;
|
|
507
|
-
}
|
|
508
|
-
let parsed;
|
|
509
|
-
try {
|
|
510
|
-
parsed = document.toJS({ maxAliasCount: 100 });
|
|
511
|
-
} catch {
|
|
512
|
-
add("cannot resolve Dependabot YAML aliases");
|
|
513
|
-
return diagnostics;
|
|
514
|
-
}
|
|
515
|
-
if (!record$6(parsed) || parsed["version"] !== 2 || !Array.isArray(parsed["updates"])) {
|
|
516
|
-
add("Dependabot must declare version: 2 and an updates array");
|
|
517
|
-
return diagnostics;
|
|
518
|
-
}
|
|
519
|
-
const covered = /* @__PURE__ */ new Map();
|
|
520
|
-
let legacy;
|
|
521
|
-
try {
|
|
522
|
-
const legacyDocument = (0, yaml.parseDocument)(files[file] ?? "", {
|
|
523
|
-
version: "1.1",
|
|
524
|
-
schema: "yaml-1.1"
|
|
525
|
-
});
|
|
526
|
-
if (legacyDocument.errors.length === 0) legacy = legacyDocument.toJS({ maxAliasCount: 100 });
|
|
527
|
-
} catch {}
|
|
528
|
-
const legacyUpdates = record$6(legacy) && Array.isArray(legacy["updates"]) ? legacy["updates"] : [];
|
|
529
|
-
const rootClaims = /* @__PURE__ */ new Map();
|
|
530
|
-
const directoryClaims = /* @__PURE__ */ new Map();
|
|
531
|
-
for (const [index, update] of parsed["updates"].entries()) {
|
|
532
|
-
const lineOf = (key) => {
|
|
533
|
-
const node = document.getIn([
|
|
534
|
-
"updates",
|
|
535
|
-
index,
|
|
536
|
-
key
|
|
537
|
-
], true);
|
|
538
|
-
const offset = (0, yaml.isNode)(node) ? node.range?.[0] : void 0;
|
|
539
|
-
return offset === void 0 ? 1 : lineCounter.linePos(offset).line;
|
|
540
|
-
};
|
|
541
|
-
const report = (key, message) => add(message, lineOf(key));
|
|
542
|
-
if (!record$6(update) || typeof update["package-ecosystem"] !== "string") {
|
|
543
|
-
report("package-ecosystem", "each Dependabot update must declare a package ecosystem");
|
|
544
|
-
continue;
|
|
545
|
-
}
|
|
546
|
-
const ecosystem = update["package-ecosystem"];
|
|
547
|
-
if (update["target-branch"] !== void 0) report("target-branch", `${ecosystem}: shared updates must target the default branch`);
|
|
548
|
-
if (update["open-pull-requests-limit"] === 0) report("open-pull-requests-limit", `${ecosystem}: version updates must remain enabled`);
|
|
549
|
-
const single = update["directory"];
|
|
550
|
-
const multiple = update["directories"];
|
|
551
|
-
let directories = [];
|
|
552
|
-
if (typeof single === "string" && multiple === void 0) directories = [single];
|
|
553
|
-
else if (single === void 0 && strings(multiple)) directories = multiple;
|
|
554
|
-
const validDirectories = directories.filter((directory) => directory.startsWith("/") && !directory.split("/").includes(".."));
|
|
555
|
-
if (directories.length === 0 || validDirectories.length !== directories.length) report("directory", `${ecosystem}: declare directory or nonempty directories with absolute repository paths`);
|
|
556
|
-
const normalizedDirectories = Array.from(new Set(validDirectories.map((value) => node_path.default.posix.normalize(value).replace(/\/+$/, "") || "/")));
|
|
557
|
-
const matches = covered.get(ecosystem) ?? /* @__PURE__ */ new Set();
|
|
558
|
-
const claimedDirectories = directoryClaims.get(ecosystem) ?? /* @__PURE__ */ new Map();
|
|
559
|
-
for (const directory of normalizedDirectories) {
|
|
560
|
-
const previous = claimedDirectories.get(directory);
|
|
561
|
-
if (previous !== void 0) report(multiple === void 0 ? "directory" : "directories", `${ecosystem}: directory ${directory} overlaps update entry ${previous + 1}`);
|
|
562
|
-
else claimedDirectories.set(directory, index);
|
|
563
|
-
}
|
|
564
|
-
directoryClaims.set(ecosystem, claimedDirectories);
|
|
565
|
-
const claimedRoots = rootClaims.get(ecosystem) ?? /* @__PURE__ */ new Map();
|
|
566
|
-
for (const root of roots.get(ecosystem) ?? []) if (normalizedDirectories.some((directory) => directory === root || multiple !== void 0 && picomatch.default.isMatch(root, directory))) {
|
|
567
|
-
const previous = claimedRoots.get(root);
|
|
568
|
-
if (previous !== void 0) report(multiple === void 0 ? "directory" : "directories", `${ecosystem}: update root ${root} overlaps update entry ${previous + 1}`);
|
|
569
|
-
else claimedRoots.set(root, index);
|
|
570
|
-
matches.add(root);
|
|
571
|
-
}
|
|
572
|
-
rootClaims.set(ecosystem, claimedRoots);
|
|
573
|
-
covered.set(ecosystem, matches);
|
|
574
|
-
const schedule = update["schedule"];
|
|
575
|
-
const legacyUpdate = legacyUpdates.at(index);
|
|
576
|
-
const legacySchedule = record$6(legacyUpdate) ? legacyUpdate["schedule"] : void 0;
|
|
577
|
-
if (record$6(schedule) && schedule["time"] === policy.schedule.time && (!record$6(legacySchedule) || legacySchedule["time"] !== policy.schedule.time)) report("schedule", `${ecosystem}: quote schedule time to preserve its string value in YAML 1.1`);
|
|
578
|
-
if (!record$6(schedule) || Object.keys(schedule).length !== Object.keys(policy.schedule).length || Object.entries(policy.schedule).some(([key, value]) => schedule[key] !== value)) report("schedule", `${ecosystem}: schedule must match the shared policy`);
|
|
579
|
-
const cooldown = update["cooldown"];
|
|
580
|
-
if (!record$6(cooldown) || Object.keys(cooldown).length !== 1 || cooldown["default-days"] !== policy.cooldown.defaultDays) report("cooldown", `${ecosystem}: cooldown must contain only default-days: ${policy.cooldown.defaultDays}`);
|
|
581
|
-
const groups = update["groups"];
|
|
582
|
-
if (!record$6(groups) || Object.keys(groups).length !== Object.keys(policy.groups).length || Object.entries(policy.groups).some(([name, expected]) => {
|
|
583
|
-
const group = groups[name];
|
|
584
|
-
return !record$6(group) || Object.keys(group).length !== 2 || !sameStrings(group["patterns"], expected.patterns) || !sameStrings(group["update-types"], expected.updateTypes);
|
|
585
|
-
})) report("groups", `${ecosystem}: groups must match the shared policy`);
|
|
586
|
-
const ignored = ignoredFor(ecosystem, policy);
|
|
587
|
-
const ignore = update["ignore"];
|
|
588
|
-
if (ignore !== void 0 && (!Array.isArray(ignore) || ignore.length === 0)) report("ignore", `${ecosystem}: omit ignore or provide a nonempty array`);
|
|
589
|
-
if (Array.isArray(ignore) && ignore.some((entry) => !record$6(entry) || typeof entry["dependency-name"] !== "string" || !ignored.includes(entry["dependency-name"]))) report("ignore", `${ecosystem}: ignore only packages owned by the shared policy`);
|
|
590
|
-
if (Array.isArray(ignore) && ignore.some((entry) => !versionOnlyIgnore(entry))) report("ignore", `${ecosystem}: each shared-pin ignore must contain only dependency-name and update-types for semver-major, semver-minor, and semver-patch version updates; security updates must remain enabled`);
|
|
591
|
-
for (const name of ignored) if (!Array.isArray(ignore) || !ignore.some((entry) => record$6(entry) && entry["dependency-name"] === name && versionOnlyIgnore(entry))) report("ignore", `${ecosystem}: ignore all version updates for shared pin ${name}`);
|
|
592
|
-
}
|
|
593
|
-
for (const [ecosystem, directories] of roots) for (const directory of directories) if (!covered.get(ecosystem)?.has(directory)) add(`add a ${ecosystem} update entry covering ${directory}`);
|
|
594
|
-
return diagnostics;
|
|
595
|
-
};
|
|
596
|
-
//#endregion
|
|
597
246
|
//#region src/toolchain-dynamic-selectors.ts
|
|
598
247
|
const locatorValidators = {
|
|
599
248
|
image: /^(?:jobs\.[\w-]+\.(?:container|services\.[\w-]+)|services\.[\w.-]+\.image)$/,
|
|
600
249
|
"bun-source": /^(?:jobs\.[\w-]+|runs)\.steps\.[\w-]+$/
|
|
601
250
|
};
|
|
602
251
|
const knownKind = (value) => typeof value === "string" && Object.hasOwn(locatorValidators, value);
|
|
603
|
-
const record$
|
|
252
|
+
const record$9 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
604
253
|
/** One reviewed declaration identifies one unresolved selector, never a rule. */
|
|
605
254
|
const parseDynamicSelectors = (input) => {
|
|
606
255
|
if (input === void 0) return [];
|
|
@@ -608,7 +257,7 @@ const parseDynamicSelectors = (input) => {
|
|
|
608
257
|
const result = [];
|
|
609
258
|
const keys = /* @__PURE__ */ new Set();
|
|
610
259
|
for (const entry of input) {
|
|
611
|
-
if (!record$
|
|
260
|
+
if (!record$9(entry) || Object.keys(entry).some((key) => ![
|
|
612
261
|
"path",
|
|
613
262
|
"kind",
|
|
614
263
|
"reason",
|
|
@@ -644,7 +293,7 @@ const parseDynamicSelectors = (input) => {
|
|
|
644
293
|
};
|
|
645
294
|
//#endregion
|
|
646
295
|
//#region src/toolchain-engine-floors.ts
|
|
647
|
-
const record$
|
|
296
|
+
const record$8 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
648
297
|
const repositoryPath = (value) => typeof value === "string" && value !== "" && !/^[A-Za-z]:/.test(value) && !/[\\\s]/.test(value) && !Array.from(value).some((character) => character.charCodeAt(0) < 32) && value.split("/").every((part) => part !== "" && part !== "." && part !== "..");
|
|
649
298
|
const parseEngineFloors = (input) => {
|
|
650
299
|
if (input === void 0) return [];
|
|
@@ -652,7 +301,7 @@ const parseEngineFloors = (input) => {
|
|
|
652
301
|
const result = [];
|
|
653
302
|
const jobs = /* @__PURE__ */ new Set();
|
|
654
303
|
for (const entry of input) {
|
|
655
|
-
if (!record$
|
|
304
|
+
if (!record$8(entry) || Object.keys(entry).some((key) => ![
|
|
656
305
|
"package",
|
|
657
306
|
"workflow",
|
|
658
307
|
"job"
|
|
@@ -673,16 +322,16 @@ const resolveEngineFloor = (entry, files) => {
|
|
|
673
322
|
const workflowText = files[entry.workflow];
|
|
674
323
|
if (manifestText === void 0 || workflowText === void 0) throw new Error("engine floor package and workflow must be tracked");
|
|
675
324
|
const manifest = JSON.parse(manifestText);
|
|
676
|
-
if (!record$
|
|
325
|
+
if (!record$8(manifest) || manifest["private"] === true || typeof manifest["name"] !== "string" || manifest["name"].trim() === "" || !record$8(manifest["engines"]) || typeof manifest["engines"]["node"] !== "string") throw new Error("engine floor package must be published and declare engines.node");
|
|
677
326
|
const range = manifest["engines"]["node"];
|
|
678
327
|
const minimum = (0, semver.validRange)(range) === null ? null : (0, semver.minVersion)(range);
|
|
679
328
|
if (minimum === null || minimum.prerelease.length !== 0) throw new Error("engine floor engines.node must declare a stable minimum");
|
|
680
329
|
const document = (0, yaml.parseDocument)(workflowText, { merge: true });
|
|
681
330
|
if (document.errors.length !== 0) throw new Error("engine floor workflow must be valid YAML");
|
|
682
331
|
const workflow = document.toJS({ maxAliasCount: 100 });
|
|
683
|
-
if (!record$
|
|
332
|
+
if (!record$8(workflow) || !record$8(workflow["jobs"])) throw new Error("engine floor job must exist in its tracked workflow");
|
|
684
333
|
const job = workflow["jobs"][entry.job];
|
|
685
|
-
if (!record$
|
|
334
|
+
if (!record$8(job)) throw new Error("engine floor job must exist in its tracked workflow");
|
|
686
335
|
if (!Array.isArray(job["steps"]) || Object.hasOwn(job, "uses")) throw new Error("engine floor job must declare steps and cannot reuse a workflow");
|
|
687
336
|
return {
|
|
688
337
|
...entry,
|
|
@@ -702,7 +351,7 @@ const packageRules = [
|
|
|
702
351
|
"typescript-layout",
|
|
703
352
|
"node-engine"
|
|
704
353
|
];
|
|
705
|
-
const record$
|
|
354
|
+
const record$7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
706
355
|
const shellSubstitutions = (command) => {
|
|
707
356
|
const frames = [{
|
|
708
357
|
kind: "root",
|
|
@@ -1262,10 +911,10 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1262
911
|
const document = pnpm ? (0, yaml.parseDocument)(text, { lineCounter }) : void 0;
|
|
1263
912
|
if (document !== void 0 && document.errors.length > 0) throw new Error("invalid YAML");
|
|
1264
913
|
const json = document === void 0 ? JSON.parse(text) : document.toJS();
|
|
1265
|
-
if (!record$
|
|
914
|
+
if (!record$7(json)) throw new Error("manifest must be an object");
|
|
1266
915
|
if (pnpm) {
|
|
1267
|
-
if (json["catalog"] !== void 0 && !record$
|
|
1268
|
-
if (json["catalogs"] !== void 0 && (!record$
|
|
916
|
+
if (json["catalog"] !== void 0 && !record$7(json["catalog"])) throw new Error("catalog must be a mapping");
|
|
917
|
+
if (json["catalogs"] !== void 0 && (!record$7(json["catalogs"]) || Object.values(json["catalogs"]).some((catalog) => !record$7(catalog)))) throw new Error("named catalogs must be mappings");
|
|
1269
918
|
if (json["packages"] !== void 0 && (!Array.isArray(json["packages"]) || !json["packages"].every((pattern) => typeof pattern === "string"))) throw new Error("workspace packages must be patterns");
|
|
1270
919
|
}
|
|
1271
920
|
manifests.set(file, json);
|
|
@@ -1279,13 +928,13 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1279
928
|
catalog: json["overrides"],
|
|
1280
929
|
prefix: ["overrides"]
|
|
1281
930
|
},
|
|
1282
|
-
...record$
|
|
931
|
+
...record$7(json["catalogs"]) ? Object.entries(json["catalogs"]).map(([name, catalog]) => ({
|
|
1283
932
|
catalog,
|
|
1284
933
|
prefix: ["catalogs", name]
|
|
1285
934
|
})) : []
|
|
1286
935
|
];
|
|
1287
936
|
for (const { catalog, prefix } of catalogEntries) {
|
|
1288
|
-
if (!record$
|
|
937
|
+
if (!record$7(catalog)) continue;
|
|
1289
938
|
for (const [name, value] of Object.entries(catalog)) {
|
|
1290
939
|
const node = document.getIn([...prefix, name], true);
|
|
1291
940
|
if (!(0, yaml.isNode)(node) || node.range === void 0 || node.range === null) continue;
|
|
@@ -1304,7 +953,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1304
953
|
});
|
|
1305
954
|
}
|
|
1306
955
|
}
|
|
1307
|
-
const catalogSources = (json) => [json, ...record$
|
|
956
|
+
const catalogSources = (json) => [json, ...record$7(json["workspaces"]) ? [json["workspaces"]] : []];
|
|
1308
957
|
const pnpmWorkspaceFor = (file) => {
|
|
1309
958
|
let directory = node_path.default.posix.dirname(file);
|
|
1310
959
|
while (true) {
|
|
@@ -1327,8 +976,8 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1327
976
|
patterns: workspace["packages"],
|
|
1328
977
|
rootMembership: "implicit"
|
|
1329
978
|
})) return void 0;
|
|
1330
|
-
if (name === "" || name === "default") return workspace["catalog"] ?? (record$
|
|
1331
|
-
return record$
|
|
979
|
+
if (name === "" || name === "default") return workspace["catalog"] ?? (record$7(workspace["catalogs"]) ? workspace["catalogs"]["default"] : void 0);
|
|
980
|
+
return record$7(workspace["catalogs"]) ? workspace["catalogs"][name] : void 0;
|
|
1332
981
|
}
|
|
1333
982
|
let directory = node_path.default.posix.dirname(file);
|
|
1334
983
|
while (true) {
|
|
@@ -1336,7 +985,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1336
985
|
if (manifest !== void 0) {
|
|
1337
986
|
for (const source of catalogSources(manifest)) {
|
|
1338
987
|
if (name === "" && source["catalog"] !== void 0) return source["catalog"];
|
|
1339
|
-
if (record$
|
|
988
|
+
if (record$7(source["catalogs"]) && source["catalogs"][name] !== void 0) return source["catalogs"][name];
|
|
1340
989
|
}
|
|
1341
990
|
if (manifest["workspaces"] !== void 0) return void 0;
|
|
1342
991
|
}
|
|
@@ -1350,7 +999,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1350
999
|
if (visited.has(value)) return void 0;
|
|
1351
1000
|
visited.add(value);
|
|
1352
1001
|
const catalog = catalogFor(file, value.slice(8));
|
|
1353
|
-
value = record$
|
|
1002
|
+
value = record$7(catalog) ? catalog[name] : void 0;
|
|
1354
1003
|
}
|
|
1355
1004
|
return value;
|
|
1356
1005
|
};
|
|
@@ -1366,7 +1015,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1366
1015
|
const workspaces = manifests.get(node_path.default.posix.join(directory, "package.json"))?.["workspaces"];
|
|
1367
1016
|
if (workspaces !== void 0) return {
|
|
1368
1017
|
directory,
|
|
1369
|
-
patterns: record$
|
|
1018
|
+
patterns: record$7(workspaces) ? workspaces["packages"] : workspaces,
|
|
1370
1019
|
source: "json"
|
|
1371
1020
|
};
|
|
1372
1021
|
if (directory === ".") return void 0;
|
|
@@ -1422,7 +1071,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1422
1071
|
"peerDependencies"
|
|
1423
1072
|
]) {
|
|
1424
1073
|
const dependencies = json[section];
|
|
1425
|
-
if (!record$
|
|
1074
|
+
if (!record$7(dependencies)) continue;
|
|
1426
1075
|
for (const [name, value] of Object.entries(dependencies)) {
|
|
1427
1076
|
if (!tsSpecifiers.has(name)) continue;
|
|
1428
1077
|
const resolved = resolve(file, name, value);
|
|
@@ -1484,7 +1133,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1484
1133
|
message: `packageManager Bun version must be ${policy.bun}, found ${JSON.stringify(manager)}`
|
|
1485
1134
|
});
|
|
1486
1135
|
const checkEntries = (entries, location) => {
|
|
1487
|
-
if (!record$
|
|
1136
|
+
if (!record$7(entries)) return;
|
|
1488
1137
|
for (const [name, raw] of Object.entries(entries)) {
|
|
1489
1138
|
const value = resolve(file, name, raw);
|
|
1490
1139
|
const tsAllowed = tsSpecifiers.get(name);
|
|
@@ -1521,12 +1170,12 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1521
1170
|
"devDependencies",
|
|
1522
1171
|
"optionalDependencies"
|
|
1523
1172
|
]) checkEntries(json[key]);
|
|
1524
|
-
if (record$
|
|
1173
|
+
if (record$7(json["peerDependencies"])) {
|
|
1525
1174
|
for (const [name, value] of Object.entries(json["peerDependencies"])) if (tsSpecifiers.has(name) && !peerRange(resolve(file, name, value))) checkEntries({ [name]: value });
|
|
1526
1175
|
}
|
|
1527
1176
|
for (const source of catalogSources(json)) {
|
|
1528
1177
|
checkEntries(source["catalog"]);
|
|
1529
|
-
if (!record$
|
|
1178
|
+
if (!record$7(source["catalogs"])) continue;
|
|
1530
1179
|
for (const catalog of Object.values(source["catalogs"])) checkEntries(catalog);
|
|
1531
1180
|
}
|
|
1532
1181
|
const ownedNames = /* @__PURE__ */ new Set([...pins.keys(), ...tsSpecifiers.keys()]);
|
|
@@ -1538,7 +1187,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1538
1187
|
"optionalDependencies"
|
|
1539
1188
|
]) {
|
|
1540
1189
|
const entries = resolutionRoot[section];
|
|
1541
|
-
if (!record$
|
|
1190
|
+
if (!record$7(entries)) continue;
|
|
1542
1191
|
for (const [name, value] of Object.entries(entries)) resolutionDependencies[name] = resolve(file, name, value);
|
|
1543
1192
|
}
|
|
1544
1193
|
const affectedSpecifiers = /* @__PURE__ */ new Map();
|
|
@@ -1558,7 +1207,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1558
1207
|
"optionalDependencies"
|
|
1559
1208
|
]) {
|
|
1560
1209
|
const entries = member[section];
|
|
1561
|
-
if (!record$
|
|
1210
|
+
if (!record$7(entries)) continue;
|
|
1562
1211
|
for (const [name, value] of Object.entries(entries)) {
|
|
1563
1212
|
if (!tsSpecifiers.has(name)) continue;
|
|
1564
1213
|
const values = affectedSpecifiers.get(name) ?? /* @__PURE__ */ new Set();
|
|
@@ -1568,7 +1217,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1568
1217
|
}
|
|
1569
1218
|
}
|
|
1570
1219
|
const checkResolutionMap = (entries, parent) => {
|
|
1571
|
-
if (!record$
|
|
1220
|
+
if (!record$7(entries)) return;
|
|
1572
1221
|
for (const [selector, replacement] of Object.entries(entries)) {
|
|
1573
1222
|
const target = selector === "." ? parent : selector;
|
|
1574
1223
|
if (target === void 0) continue;
|
|
@@ -1591,7 +1240,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1591
1240
|
});
|
|
1592
1241
|
continue;
|
|
1593
1242
|
}
|
|
1594
|
-
if (record$
|
|
1243
|
+
if (record$7(replacement)) {
|
|
1595
1244
|
checkResolutionMap(replacement, target);
|
|
1596
1245
|
continue;
|
|
1597
1246
|
}
|
|
@@ -1626,7 +1275,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1626
1275
|
};
|
|
1627
1276
|
checkResolutionMap(json["overrides"]);
|
|
1628
1277
|
checkResolutionMap(json["resolutions"]);
|
|
1629
|
-
if (record$
|
|
1278
|
+
if (record$7(json["pnpm"])) checkResolutionMap(json["pnpm"]["overrides"]);
|
|
1630
1279
|
const localEntries = toolchainEntries(file, json);
|
|
1631
1280
|
const usesCompiler = policy.typescriptInstallLayouts.some((layout) => localEntries.some(({ name }) => name === layout.compilerPackage));
|
|
1632
1281
|
let selectedLayout = layoutFor(specifierMap(localEntries));
|
|
@@ -1650,7 +1299,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1650
1299
|
key: "dependencies",
|
|
1651
1300
|
message: "TypeScript compiler and compatibility packages must match one complete declared install layout"
|
|
1652
1301
|
});
|
|
1653
|
-
const command = record$
|
|
1302
|
+
const command = record$7(json["scripts"]) ? json["scripts"]["typecheck"] : void 0;
|
|
1654
1303
|
if (selectedLayout !== void 0 && typeof command === "string") {
|
|
1655
1304
|
const normalizedExpected = selectedLayout.typecheckCommand.replace(/\s+/g, " ").trim();
|
|
1656
1305
|
for (const words of compilerCommandSegments(command)) {
|
|
@@ -1668,7 +1317,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1668
1317
|
}
|
|
1669
1318
|
}
|
|
1670
1319
|
}
|
|
1671
|
-
if (record$
|
|
1320
|
+
if (record$7(json["peerDependencies"])) {
|
|
1672
1321
|
const peerLayout = selectedLayout ?? workspaceLayoutFor(file)?.layout;
|
|
1673
1322
|
for (const [name, raw] of Object.entries(json["peerDependencies"])) {
|
|
1674
1323
|
if (!tsSpecifiers.has(name)) continue;
|
|
@@ -1692,7 +1341,7 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1692
1341
|
});
|
|
1693
1342
|
}
|
|
1694
1343
|
}
|
|
1695
|
-
if (record$
|
|
1344
|
+
if (record$7(json["engines"]) && json["engines"]["node"] !== void 0 && (typeof json["engines"]["node"] !== "string" || !require_toolchain_inputs.nodeSupportRangeMatches(policy.node, json["engines"]["node"]))) add({
|
|
1696
1345
|
file,
|
|
1697
1346
|
rule: "node-engine",
|
|
1698
1347
|
key: "node",
|
|
@@ -1702,16 +1351,633 @@ const checkPackageFiles = ({ files, policy }) => {
|
|
|
1702
1351
|
}
|
|
1703
1352
|
return diagnostics;
|
|
1704
1353
|
};
|
|
1354
|
+
const extensionKinds = new Map(Object.entries({
|
|
1355
|
+
".js": "javascript",
|
|
1356
|
+
".jsx": "javascript",
|
|
1357
|
+
".mjs": "javascript",
|
|
1358
|
+
".cjs": "javascript",
|
|
1359
|
+
".ts": "typescript",
|
|
1360
|
+
".tsx": "typescript",
|
|
1361
|
+
".mts": "typescript",
|
|
1362
|
+
".cts": "typescript"
|
|
1363
|
+
}));
|
|
1364
|
+
/** Share source classification between lint ownership and script command discovery. */
|
|
1365
|
+
const scriptSourceKind = (file) => extensionKinds.get((0, node_path.extname)(file));
|
|
1366
|
+
//#endregion
|
|
1367
|
+
//#region src/toolchain-script-lifecycle.ts
|
|
1368
|
+
const record$6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1369
|
+
const declaredManager = (value) => typeof value === "string" ? /^(bun|npm|pnpm|yarn)@(\d+\.\d+\.\d+)(?:\+\S+)?$/u.exec(value) : null;
|
|
1370
|
+
const rootScriptManager = (manifest, context) => declaredManager(context.packageManager ?? (record$6(manifest) ? manifest["packageManager"] : void 0))?.at(1);
|
|
1371
|
+
const npmHooksEnabled = (file) => {
|
|
1372
|
+
if (file?.type === "unknown") throw new Error("npm lifecycle configuration must be a tracked regular file");
|
|
1373
|
+
if (file?.type !== "tracked") return true;
|
|
1374
|
+
let enabled = true;
|
|
1375
|
+
let section = false;
|
|
1376
|
+
for (const raw of file.source.split(/\r?\n/u)) {
|
|
1377
|
+
const line = raw.trim();
|
|
1378
|
+
if (line.startsWith("[")) {
|
|
1379
|
+
section = true;
|
|
1380
|
+
continue;
|
|
1381
|
+
}
|
|
1382
|
+
if (section || !/^ignore-scripts(?:\s*=|\s*$)/iu.test(line)) continue;
|
|
1383
|
+
if (!/^ignore-scripts(?:\s*=|\s*$)/u.test(line)) throw new Error("npm ignore-scripts must use its canonical lowercase key");
|
|
1384
|
+
const value = line.slice(line.indexOf("=") + 1).trim();
|
|
1385
|
+
if (value !== "true" && value !== "false") throw new Error("npm ignore-scripts must be a literal boolean for lifecycle discovery");
|
|
1386
|
+
enabled = value === "false";
|
|
1387
|
+
}
|
|
1388
|
+
return enabled;
|
|
1389
|
+
};
|
|
1390
|
+
const yamlSettings = (file, manager) => {
|
|
1391
|
+
if (file?.type === "unknown") throw new Error(`${manager} lifecycle configuration requires tracked regular root files without nested workspace overrides`);
|
|
1392
|
+
if (file?.type !== "tracked") return {};
|
|
1393
|
+
const document = (0, yaml.parseDocument)(file.source);
|
|
1394
|
+
if (document.errors.length > 0) throw new Error(`Cannot parse ${manager} lifecycle configuration`);
|
|
1395
|
+
let settings;
|
|
1396
|
+
try {
|
|
1397
|
+
settings = document.toJS();
|
|
1398
|
+
} catch {
|
|
1399
|
+
throw new Error(`Cannot parse ${manager} lifecycle configuration`);
|
|
1400
|
+
}
|
|
1401
|
+
if (!record$6(settings)) throw new Error(`${manager} lifecycle configuration must be a mapping`);
|
|
1402
|
+
return settings;
|
|
1403
|
+
};
|
|
1404
|
+
const pnpmHooksEnabled = (manifest, context) => {
|
|
1405
|
+
const declared = declaredManager(context.packageManager ?? manifest["packageManager"]);
|
|
1406
|
+
const dependency = [manifest["dependencies"], manifest["devDependencies"]].flatMap((field) => record$6(field) ? [field["pnpm"]] : []).find((value) => typeof value === "string");
|
|
1407
|
+
if ((declared?.at(1) === "pnpm" ? declared.at(2) : dependency) !== "12.9.1") throw new Error("pnpm lifecycle discovery requires declared pnpm@12.9.1");
|
|
1408
|
+
const enabled = yamlSettings(context.pnpmWorkspace, "pnpm")["enablePrePostScripts"];
|
|
1409
|
+
if (enabled === void 0) return true;
|
|
1410
|
+
if (typeof enabled !== "boolean") throw new Error("pnpm enablePrePostScripts must be a literal boolean");
|
|
1411
|
+
return enabled;
|
|
1412
|
+
};
|
|
1413
|
+
/** Automatic pre/post stages run their bodies directly, without pre-pre recursion. */
|
|
1414
|
+
const scriptLifecycleStages = ({ name, manager, manifest, scripts, context }) => {
|
|
1415
|
+
const present = [`pre${name}`, `post${name}`].filter((hook) => scripts[hook] !== void 0 && scripts[hook] !== "");
|
|
1416
|
+
if (present.length === 0) return [name];
|
|
1417
|
+
if (present.some((hook) => typeof scripts[hook] !== "string")) throw new Error(`Lifecycle hooks for ${name} must be literal package scripts`);
|
|
1418
|
+
let enabled;
|
|
1419
|
+
switch (manager) {
|
|
1420
|
+
case "npm":
|
|
1421
|
+
enabled = npmHooksEnabled(context.npmrc);
|
|
1422
|
+
break;
|
|
1423
|
+
case "bun":
|
|
1424
|
+
enabled = true;
|
|
1425
|
+
break;
|
|
1426
|
+
case "pnpm":
|
|
1427
|
+
enabled = pnpmHooksEnabled(manifest, context);
|
|
1428
|
+
break;
|
|
1429
|
+
case "yarn": {
|
|
1430
|
+
const declaration = declaredManager(context.packageManager ?? manifest["packageManager"]);
|
|
1431
|
+
if (declaration?.at(1) !== "yarn" || !/^[234]\./u.test(declaration.at(2) ?? "")) throw new Error("Yarn lifecycle discovery requires a declared Berry version (2, 3 or 4)");
|
|
1432
|
+
const settings = yamlSettings(context.yarnrc, "Yarn");
|
|
1433
|
+
if (settings["yarnPath"] !== void 0 && settings["yarnPath"] !== null || settings["plugins"] !== void 0 && (!Array.isArray(settings["plugins"]) || settings["plugins"].length > 0)) throw new Error("Custom Yarn executables or plugins require explicit lifecycle discovery support");
|
|
1434
|
+
enabled = false;
|
|
1435
|
+
break;
|
|
1436
|
+
}
|
|
1437
|
+
default: throw new Error(`Cannot determine the package manager for lifecycle hooks of ${name}`);
|
|
1438
|
+
}
|
|
1439
|
+
if (!enabled) return [name];
|
|
1440
|
+
const source = scripts[name];
|
|
1441
|
+
const include = (hook) => present.includes(hook) && !(manager === "pnpm" && typeof source === "string" && source.includes(hook));
|
|
1442
|
+
return [
|
|
1443
|
+
...include(`pre${name}`) ? [`pre${name}`] : [],
|
|
1444
|
+
name,
|
|
1445
|
+
...include(`post${name}`) ? [`post${name}`] : []
|
|
1446
|
+
];
|
|
1447
|
+
};
|
|
1448
|
+
//#endregion
|
|
1449
|
+
//#region src/toolchain-lint-command.ts
|
|
1450
|
+
const record$5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1451
|
+
const scriptManagers = /* @__PURE__ */ new Set([
|
|
1452
|
+
"bun",
|
|
1453
|
+
"npm",
|
|
1454
|
+
"pnpm",
|
|
1455
|
+
"yarn"
|
|
1456
|
+
]);
|
|
1457
|
+
const valueFlags = /* @__PURE__ */ new Set([
|
|
1458
|
+
"-c",
|
|
1459
|
+
"--config",
|
|
1460
|
+
"--tsconfig",
|
|
1461
|
+
"--ignore-path",
|
|
1462
|
+
"--ignore-pattern",
|
|
1463
|
+
"-A",
|
|
1464
|
+
"--allow",
|
|
1465
|
+
"-W",
|
|
1466
|
+
"--warn",
|
|
1467
|
+
"-D",
|
|
1468
|
+
"--deny",
|
|
1469
|
+
"-f",
|
|
1470
|
+
"--format",
|
|
1471
|
+
"--max-warnings",
|
|
1472
|
+
"--threads",
|
|
1473
|
+
"--report-unused-disable-directives-severity"
|
|
1474
|
+
]);
|
|
1475
|
+
const booleanFlags = /* @__PURE__ */ new Set([
|
|
1476
|
+
"--no-ignore",
|
|
1477
|
+
"--quiet",
|
|
1478
|
+
"--deny-warnings",
|
|
1479
|
+
"--silent",
|
|
1480
|
+
"--no-error-on-unmatched-pattern",
|
|
1481
|
+
"--disable-nested-config",
|
|
1482
|
+
"--report-unused-disable-directives",
|
|
1483
|
+
"--disable-unicorn-plugin",
|
|
1484
|
+
"--disable-oxc-plugin",
|
|
1485
|
+
"--disable-typescript-plugin",
|
|
1486
|
+
"--import-plugin",
|
|
1487
|
+
"--react-plugin",
|
|
1488
|
+
"--jsdoc-plugin",
|
|
1489
|
+
"--jest-plugin",
|
|
1490
|
+
"--vitest-plugin",
|
|
1491
|
+
"--jsx-a11y-plugin",
|
|
1492
|
+
"--nextjs-plugin",
|
|
1493
|
+
"--react-perf-plugin",
|
|
1494
|
+
"--promise-plugin",
|
|
1495
|
+
"--node-plugin",
|
|
1496
|
+
"--vue-plugin"
|
|
1497
|
+
]);
|
|
1498
|
+
const literalWord = (raw) => {
|
|
1499
|
+
let quote;
|
|
1500
|
+
for (let index = 0; index < raw.length; index++) {
|
|
1501
|
+
const character = raw.at(index);
|
|
1502
|
+
if (character === "\\" && quote !== "'") {
|
|
1503
|
+
index++;
|
|
1504
|
+
continue;
|
|
1505
|
+
}
|
|
1506
|
+
if (character === quote) {
|
|
1507
|
+
quote = void 0;
|
|
1508
|
+
continue;
|
|
1509
|
+
}
|
|
1510
|
+
if (quote === void 0 && (character === "'" || character === "\"")) {
|
|
1511
|
+
quote = character;
|
|
1512
|
+
continue;
|
|
1513
|
+
}
|
|
1514
|
+
if (quote !== "'" && (character === "$" || character === "`")) return false;
|
|
1515
|
+
if (quote === void 0 && character !== void 0 && /[*?[\]{}~]/.test(character)) return false;
|
|
1516
|
+
}
|
|
1517
|
+
return quote === void 0;
|
|
1518
|
+
};
|
|
1519
|
+
const inventoryArguments = (raw) => {
|
|
1520
|
+
const args = [];
|
|
1521
|
+
let threadsSeen = false;
|
|
1522
|
+
for (let index = 0; index < raw.length; index++) {
|
|
1523
|
+
const word = raw.at(index);
|
|
1524
|
+
if (word === void 0) break;
|
|
1525
|
+
if (!literalWord(word)) return { error: "oxlint arguments must be literal shell words" };
|
|
1526
|
+
const value = decodeShellWord(word);
|
|
1527
|
+
if (!value.startsWith("-")) {
|
|
1528
|
+
args.push(value);
|
|
1529
|
+
continue;
|
|
1530
|
+
}
|
|
1531
|
+
if (value === "--") return { error: "oxlint argument terminators are unsupported for inventory" };
|
|
1532
|
+
if (value === "--type-aware" || value === "--type-check") continue;
|
|
1533
|
+
const equals = value.indexOf("=");
|
|
1534
|
+
const flag = equals < 0 ? value : value.slice(0, equals);
|
|
1535
|
+
if (booleanFlags.has(flag)) {
|
|
1536
|
+
if (equals >= 0) return { error: `unexpected value for oxlint flag ${flag}` };
|
|
1537
|
+
args.push(value);
|
|
1538
|
+
continue;
|
|
1539
|
+
}
|
|
1540
|
+
if (!valueFlags.has(flag)) return { error: `unsupported oxlint flag ${flag}` };
|
|
1541
|
+
let argument;
|
|
1542
|
+
if (equals >= 0) {
|
|
1543
|
+
argument = value.slice(equals + 1);
|
|
1544
|
+
if (argument === "") return { error: `missing value for oxlint flag ${flag}` };
|
|
1545
|
+
} else {
|
|
1546
|
+
const next = raw.at(++index);
|
|
1547
|
+
if (next === void 0 || !literalWord(next) || decodeShellWord(next).startsWith("-")) return { error: `missing or nonliteral value for oxlint flag ${flag}` };
|
|
1548
|
+
argument = decodeShellWord(next);
|
|
1549
|
+
}
|
|
1550
|
+
if (flag === "--threads") {
|
|
1551
|
+
if (threadsSeen) return { error: "duplicate oxlint flag --threads" };
|
|
1552
|
+
if (!/^\+?[0-9]+$/u.test(argument) || BigInt(argument) > 18446744073709551615n) return { error: "invalid integer value for oxlint flag --threads" };
|
|
1553
|
+
threadsSeen = true;
|
|
1554
|
+
continue;
|
|
1555
|
+
}
|
|
1556
|
+
if (equals >= 0) args.push(value);
|
|
1557
|
+
else args.push(value, argument);
|
|
1558
|
+
}
|
|
1559
|
+
return { args };
|
|
1560
|
+
};
|
|
1561
|
+
/** Extract runtime and arguments for the pinned inventory binary; never execute package-script shell text. */
|
|
1562
|
+
const lintInventoryCommands = (manifest, forceEnabled = false, context = {}) => {
|
|
1563
|
+
const commands = [];
|
|
1564
|
+
const errors = [];
|
|
1565
|
+
const workspaceTasks = /* @__PURE__ */ new Set();
|
|
1566
|
+
const filteredBuilds = /* @__PURE__ */ new Set();
|
|
1567
|
+
const declaresEngine = record$5(manifest) && [manifest["dependencies"], manifest["devDependencies"]].some((dependencies) => record$5(dependencies) && "oxlint-tsgolint" in dependencies);
|
|
1568
|
+
const enabled = forceEnabled || declaresEngine;
|
|
1569
|
+
if (!enabled) return {
|
|
1570
|
+
commands,
|
|
1571
|
+
errors,
|
|
1572
|
+
enabled,
|
|
1573
|
+
workspaceTask: false
|
|
1574
|
+
};
|
|
1575
|
+
if (!record$5(manifest)) return {
|
|
1576
|
+
commands,
|
|
1577
|
+
errors: ["lint inventory requires an object package manifest"],
|
|
1578
|
+
enabled,
|
|
1579
|
+
workspaceTask: false
|
|
1580
|
+
};
|
|
1581
|
+
const scripts = manifest["scripts"];
|
|
1582
|
+
if (!record$5(scripts)) return {
|
|
1583
|
+
commands,
|
|
1584
|
+
errors: ["type-aware linting requires a root lint script"],
|
|
1585
|
+
enabled,
|
|
1586
|
+
workspaceTask: false
|
|
1587
|
+
};
|
|
1588
|
+
const active = /* @__PURE__ */ new Set();
|
|
1589
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1590
|
+
const visit = (name) => {
|
|
1591
|
+
if (active.has(name)) {
|
|
1592
|
+
errors.push(`cyclic lint script alias: ${name}`);
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
if (visited.has(name)) return;
|
|
1596
|
+
const source = scripts[name];
|
|
1597
|
+
if (typeof source !== "string") {
|
|
1598
|
+
errors.push(`missing literal package script: ${name}`);
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
active.add(name);
|
|
1602
|
+
for (const raw of shellCommandSegments(source)) {
|
|
1603
|
+
if (raw.length === 0) continue;
|
|
1604
|
+
const words = raw.map(decodeShellWord);
|
|
1605
|
+
const first = words.at(0);
|
|
1606
|
+
if (first === "cd" || first === "pushd" || first === "popd") {
|
|
1607
|
+
errors.push(`${name}: changing directories is unsupported for lint inventory`);
|
|
1608
|
+
continue;
|
|
1609
|
+
}
|
|
1610
|
+
if (first === "echo" || first === "printf") continue;
|
|
1611
|
+
if (first === "turbo") {
|
|
1612
|
+
if (words.length === 3 && words.at(1) === "run" && words.at(2) === "lint" && raw.every(literalWord)) workspaceTasks.add(name);
|
|
1613
|
+
else if (words.includes("lint") || !raw.every(literalWord)) errors.push(`${name}: workspace lint delegation requires literal turbo run lint without options`);
|
|
1614
|
+
continue;
|
|
1615
|
+
}
|
|
1616
|
+
if (first === "bun" && words.length === 4 && words.at(1) === "--filter" && words.at(2) !== "" && !words.at(2)?.startsWith("-") && words.at(3) === "build" && raw.every(literalWord)) {
|
|
1617
|
+
const selector = words.at(2);
|
|
1618
|
+
try {
|
|
1619
|
+
if (selector === void 0 || context.assertFilteredBunBuild === void 0) throw new Error("Filtered Bun builds require verified workspace lifecycle context");
|
|
1620
|
+
context.assertFilteredBunBuild(selector);
|
|
1621
|
+
filteredBuilds.add(selector);
|
|
1622
|
+
} catch (error) {
|
|
1623
|
+
errors.push(error instanceof Error ? error.message : "Cannot determine filtered build lifecycle stages");
|
|
1624
|
+
}
|
|
1625
|
+
continue;
|
|
1626
|
+
}
|
|
1627
|
+
if (first !== void 0 && scriptManagers.has(first) && words.at(1) === "run") {
|
|
1628
|
+
const target = words.at(2);
|
|
1629
|
+
if (target !== void 0 && first === "bun" && scriptSourceKind(target) !== void 0 && typeof scripts[target] !== "string") {
|
|
1630
|
+
if (!raw.every(literalWord)) errors.push(`${name}: validation file commands require literal arguments`);
|
|
1631
|
+
continue;
|
|
1632
|
+
}
|
|
1633
|
+
if (raw.length !== 3 || !raw.every(literalWord)) {
|
|
1634
|
+
errors.push(`${name}: package script aliases must have one literal name`);
|
|
1635
|
+
continue;
|
|
1636
|
+
}
|
|
1637
|
+
const alias = words.at(2);
|
|
1638
|
+
if (alias !== void 0) invoke({
|
|
1639
|
+
name: alias,
|
|
1640
|
+
manager: first
|
|
1641
|
+
});
|
|
1642
|
+
continue;
|
|
1643
|
+
}
|
|
1644
|
+
let offset;
|
|
1645
|
+
if (first === "oxlint" || first === "./node_modules/.bin/oxlint" || first === "node_modules/.bin/oxlint") offset = 1;
|
|
1646
|
+
else if (first === "bun" && words.at(1) === "--bun" && words.at(2) === "oxlint") offset = 3;
|
|
1647
|
+
else if (first === "bunx" && words.at(1) === "oxlint") offset = 2;
|
|
1648
|
+
else if (first === "bunx" && words.at(1) === "--no-install" && words.at(2) === "oxlint") offset = 3;
|
|
1649
|
+
if (offset === void 0) {
|
|
1650
|
+
if (words.some((word) => /(?:^|\/)oxlint(?:@[^/]+)?$/.test(word)) || words.some((word, index) => scriptManagers.has(word) && words.slice(index + 1).includes("run")) || words.includes("turbo") && words.includes("lint") || first === "bun" && words.includes("--filter") || first !== void 0 && scriptManagers.has(first) && !(first === "bun" && [
|
|
1651
|
+
"test",
|
|
1652
|
+
"build",
|
|
1653
|
+
"check",
|
|
1654
|
+
"install",
|
|
1655
|
+
"x"
|
|
1656
|
+
].includes(words.at(1) ?? "")) && (words.slice(1).some((word) => typeof scripts[word] === "string") || !raw.every(literalWord)) || raw.at(0) !== void 0 && !literalWord(raw.at(0) ?? "")) errors.push(`${name}: unsupported or dynamic oxlint launcher`);
|
|
1657
|
+
continue;
|
|
1658
|
+
}
|
|
1659
|
+
if (!raw.slice(0, offset).every(literalWord)) {
|
|
1660
|
+
errors.push(`${name}: oxlint launcher must be literal`);
|
|
1661
|
+
continue;
|
|
1662
|
+
}
|
|
1663
|
+
const inventory = inventoryArguments(raw.slice(offset));
|
|
1664
|
+
if (inventory.error !== void 0) errors.push(`${name}: ${inventory.error}`);
|
|
1665
|
+
else commands.push({
|
|
1666
|
+
runtime: first === "bun" ? "bun" : "node",
|
|
1667
|
+
args: inventory.args
|
|
1668
|
+
});
|
|
1669
|
+
}
|
|
1670
|
+
active.delete(name);
|
|
1671
|
+
visited.add(name);
|
|
1672
|
+
};
|
|
1673
|
+
const activeInvocations = /* @__PURE__ */ new Set();
|
|
1674
|
+
const invoke = ({ name, manager }) => {
|
|
1675
|
+
const key = `${manager ?? "unknown"}:${name}`;
|
|
1676
|
+
if (activeInvocations.has(key)) {
|
|
1677
|
+
errors.push(`cyclic lint script alias: ${name}`);
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
activeInvocations.add(key);
|
|
1681
|
+
try {
|
|
1682
|
+
const stages = scriptLifecycleStages({
|
|
1683
|
+
name,
|
|
1684
|
+
manager,
|
|
1685
|
+
manifest,
|
|
1686
|
+
scripts,
|
|
1687
|
+
context
|
|
1688
|
+
});
|
|
1689
|
+
for (const stage of stages) visit(stage);
|
|
1690
|
+
} catch (error) {
|
|
1691
|
+
errors.push(error instanceof Error ? error.message : "Cannot determine script lifecycle stages");
|
|
1692
|
+
} finally {
|
|
1693
|
+
activeInvocations.delete(key);
|
|
1694
|
+
}
|
|
1695
|
+
};
|
|
1696
|
+
invoke({
|
|
1697
|
+
name: context.entryScript ?? "lint",
|
|
1698
|
+
manager: rootScriptManager(manifest, context)
|
|
1699
|
+
});
|
|
1700
|
+
const workspaceTask = workspaceTasks.size > 0;
|
|
1701
|
+
if (!declaresEngine && commands.length === 0 && !workspaceTask && filteredBuilds.size === 0 && errors.length === 0) return {
|
|
1702
|
+
commands,
|
|
1703
|
+
errors,
|
|
1704
|
+
enabled: false,
|
|
1705
|
+
workspaceTask
|
|
1706
|
+
};
|
|
1707
|
+
if (commands.length === 0 && !workspaceTask && filteredBuilds.size === 0 && context.entryScript === void 0) errors.push("root lint scripts contain no supported oxlint invocation");
|
|
1708
|
+
return {
|
|
1709
|
+
commands: errors.length === 0 ? commands : [],
|
|
1710
|
+
errors,
|
|
1711
|
+
enabled,
|
|
1712
|
+
workspaceTask: errors.length === 0 && workspaceTask
|
|
1713
|
+
};
|
|
1714
|
+
};
|
|
1715
|
+
//#endregion
|
|
1716
|
+
//#region src/toolchain-node-projects.ts
|
|
1717
|
+
const nodeProjectRule = "type-aware-projects";
|
|
1718
|
+
const checkProjectOwnership = ({ trackedFiles, lintedFiles, projectFiles }) => [...new Set(lintedFiles)].filter((file) => scriptSourceKind(file) !== void 0 && trackedFiles.has(file) && !projectFiles.has(file)).sort((left, right) => left.localeCompare(right)).map((file) => ({
|
|
1719
|
+
rule: nodeProjectRule,
|
|
1720
|
+
path: file,
|
|
1721
|
+
line: 1,
|
|
1722
|
+
message: "type-aware lint would use an inferred project; include this file in a declared tsconfig project (for Node scripts, add scripts/tsconfig.json extending @stll/typescript-config/node.json; see the Node scripts recipe in stella/tooling README)"
|
|
1723
|
+
}));
|
|
1724
|
+
const record$4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1725
|
+
const stringList = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
1726
|
+
const readCommand = ({ root, executable, args, runtime }) => {
|
|
1727
|
+
const result = (0, node_child_process.spawnSync)(runtime, [
|
|
1728
|
+
...runtime === "bun" ? ["--bun"] : [],
|
|
1729
|
+
executable,
|
|
1730
|
+
...args
|
|
1731
|
+
], {
|
|
1732
|
+
cwd: root,
|
|
1733
|
+
encoding: "utf8",
|
|
1734
|
+
timeout: 3e4,
|
|
1735
|
+
maxBuffer: 33554432
|
|
1736
|
+
});
|
|
1737
|
+
if (result.error !== void 0) throw result.error;
|
|
1738
|
+
if (result.status !== 0) throw new Error(`configuration discovery failed: ${(result.stderr + result.stdout).trim()}`);
|
|
1739
|
+
return result.stdout;
|
|
1740
|
+
};
|
|
1741
|
+
const relativePath = (root, file) => node_path.default.relative(root, node_path.default.resolve(root, file)).split(node_path.default.sep).join("/");
|
|
1742
|
+
const lifecycleFile = (root, file, tracked) => {
|
|
1743
|
+
try {
|
|
1744
|
+
const location = node_path.default.join(root, file);
|
|
1745
|
+
const metadata = (0, node_fs.lstatSync)(location);
|
|
1746
|
+
if (!tracked.has(file) || !metadata.isFile()) return { type: "unknown" };
|
|
1747
|
+
return {
|
|
1748
|
+
type: "tracked",
|
|
1749
|
+
source: (0, node_fs.readFileSync)(location, "utf8")
|
|
1750
|
+
};
|
|
1751
|
+
} catch (error) {
|
|
1752
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return { type: "absent" };
|
|
1753
|
+
return { type: "unknown" };
|
|
1754
|
+
}
|
|
1755
|
+
};
|
|
1756
|
+
/** Inventory and config expansion only: neither subprocess lints or typechecks. */
|
|
1757
|
+
const checkNodeProjects = ({ root, manifest, trackedFiles }) => {
|
|
1758
|
+
const packageManager = record$4(manifest) ? manifest["packageManager"] : void 0;
|
|
1759
|
+
const managerUsesPnpmWorkspace = packageManager === void 0 || typeof packageManager === "string" && packageManager.startsWith("pnpm@");
|
|
1760
|
+
const discovered = /* @__PURE__ */ new Map();
|
|
1761
|
+
const workspaceMembers = (pnpmMembership) => {
|
|
1762
|
+
const cached = discovered.get(pnpmMembership);
|
|
1763
|
+
if (cached !== void 0) return cached;
|
|
1764
|
+
const files = { "package.json": JSON.stringify(manifest) };
|
|
1765
|
+
for (const file of trackedFiles) {
|
|
1766
|
+
const name = node_path.default.posix.basename(file);
|
|
1767
|
+
if (file !== "package.json" && (name === "package.json" || pnpmMembership && name === "pnpm-workspace.yaml") && !file.split("/").includes("node_modules")) files[file] = (0, node_fs.readFileSync)(node_path.default.join(root, file), "utf8");
|
|
1768
|
+
}
|
|
1769
|
+
const result = require_consumer_node_support.discoverConsumerManifests(files);
|
|
1770
|
+
discovered.set(pnpmMembership, result);
|
|
1771
|
+
return result;
|
|
1772
|
+
};
|
|
1773
|
+
const filteredCommands = [];
|
|
1774
|
+
const activeBuilds = /* @__PURE__ */ new Set();
|
|
1775
|
+
const workspaceLifecycle = (directory) => {
|
|
1776
|
+
const nestedLifecycle = { ...lifecycle };
|
|
1777
|
+
for (const [key, name] of [
|
|
1778
|
+
["npmrc", ".npmrc"],
|
|
1779
|
+
["pnpmWorkspace", "pnpm-workspace.yaml"],
|
|
1780
|
+
["yarnrc", ".yarnrc.yml"]
|
|
1781
|
+
]) {
|
|
1782
|
+
let ancestor = directory;
|
|
1783
|
+
while (ancestor !== ".") {
|
|
1784
|
+
if (lifecycleFile(root, node_path.default.posix.join(ancestor, name), trackedFiles).type !== "absent") {
|
|
1785
|
+
nestedLifecycle[key] = { type: "unknown" };
|
|
1786
|
+
break;
|
|
1787
|
+
}
|
|
1788
|
+
ancestor = node_path.default.posix.dirname(ancestor);
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
return nestedLifecycle;
|
|
1792
|
+
};
|
|
1793
|
+
const lifecycle = {
|
|
1794
|
+
packageManager: record$4(manifest) ? manifest["packageManager"] : void 0,
|
|
1795
|
+
npmrc: lifecycleFile(root, ".npmrc", trackedFiles),
|
|
1796
|
+
pnpmWorkspace: lifecycleFile(root, "pnpm-workspace.yaml", trackedFiles),
|
|
1797
|
+
yarnrc: lifecycleFile(root, ".yarnrc.yml", trackedFiles),
|
|
1798
|
+
assertFilteredBunBuild: (selector) => {
|
|
1799
|
+
const selected = [...workspaceMembers(false).entries()].filter(([, pkg]) => pkg["name"] === selector);
|
|
1800
|
+
if (selected.length !== 1) throw new Error("Filtered Bun build requires one declared workspace package");
|
|
1801
|
+
const member = selected.at(0);
|
|
1802
|
+
if (member === void 0) throw new Error("Missing selected workspace");
|
|
1803
|
+
const [directory, pkg] = member;
|
|
1804
|
+
const scripts = pkg["scripts"];
|
|
1805
|
+
if (!record$4(scripts) || typeof scripts["build"] !== "string") throw new Error("Filtered Bun build requires a literal build script");
|
|
1806
|
+
if (["prebuild", "postbuild"].some((name) => scripts[name] !== void 0 && scripts[name] !== "")) throw new Error("Filtered Bun build lifecycle hooks are unsupported for lint inventory; invoke lint scopes explicitly");
|
|
1807
|
+
if (activeBuilds.has(directory)) throw new Error("Cyclic filtered Bun build delegation");
|
|
1808
|
+
activeBuilds.add(directory);
|
|
1809
|
+
try {
|
|
1810
|
+
const nested = lintInventoryCommands(pkg, true, {
|
|
1811
|
+
...workspaceLifecycle(directory),
|
|
1812
|
+
entryScript: "build"
|
|
1813
|
+
});
|
|
1814
|
+
if (nested.workspaceTask) throw new Error("Nested workspace lint delegation is unsupported in filtered builds");
|
|
1815
|
+
if (nested.errors.length > 0) throw new Error(`${directory}/package.json: ${nested.errors.join("; ")}`);
|
|
1816
|
+
for (const command of nested.commands) filteredCommands.push({
|
|
1817
|
+
cwd: node_path.default.join(root, directory),
|
|
1818
|
+
...command
|
|
1819
|
+
});
|
|
1820
|
+
} finally {
|
|
1821
|
+
activeBuilds.delete(directory);
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
};
|
|
1825
|
+
const inventory = lintInventoryCommands(manifest, false, lifecycle);
|
|
1826
|
+
if (!inventory.enabled) return [];
|
|
1827
|
+
const diagnostics = [];
|
|
1828
|
+
const addConfiguration = (file, message) => diagnostics.push({
|
|
1829
|
+
rule: nodeProjectRule,
|
|
1830
|
+
path: file,
|
|
1831
|
+
line: 1,
|
|
1832
|
+
message
|
|
1833
|
+
});
|
|
1834
|
+
for (const error of inventory.errors) addConfiguration("package.json", error);
|
|
1835
|
+
if (inventory.errors.length !== 0) return diagnostics;
|
|
1836
|
+
try {
|
|
1837
|
+
const require$1 = (0, node_module.createRequire)(node_path.default.join(root, "package.json"));
|
|
1838
|
+
const oxlint = node_path.default.join(node_path.default.dirname(require$1.resolve("oxlint/package.json")), "bin/oxlint");
|
|
1839
|
+
const compiler = node_path.default.join(node_path.default.dirname(require$1.resolve("typescript/package.json")), "bin/tsc");
|
|
1840
|
+
const commands = inventory.commands.map((command) => ({
|
|
1841
|
+
cwd: root,
|
|
1842
|
+
...command
|
|
1843
|
+
}));
|
|
1844
|
+
if (inventory.workspaceTask) {
|
|
1845
|
+
for (const [directory, workspace] of workspaceMembers(managerUsesPnpmWorkspace)) {
|
|
1846
|
+
if (directory === ".") continue;
|
|
1847
|
+
const file = `${directory}/package.json`;
|
|
1848
|
+
if (!record$4(workspace["scripts"]) || workspace["scripts"]["lint"] === void 0) continue;
|
|
1849
|
+
const nestedLifecycle = workspaceLifecycle(directory);
|
|
1850
|
+
const nested = lintInventoryCommands(workspace, true, nestedLifecycle);
|
|
1851
|
+
if (nested.workspaceTask) throw new Error(`nested workspace lint delegation is unsupported: ${file}`);
|
|
1852
|
+
if (nested.errors.length > 0) throw new Error(`${file}: ${nested.errors.join("; ")}`);
|
|
1853
|
+
for (const command of nested.commands) commands.push({
|
|
1854
|
+
cwd: node_path.default.join(root, directory),
|
|
1855
|
+
...command
|
|
1856
|
+
});
|
|
1857
|
+
}
|
|
1858
|
+
if (commands.length === 0 && filteredCommands.length === 0) throw new Error("workspace lint task has no declared oxlint commands");
|
|
1859
|
+
}
|
|
1860
|
+
commands.push(...filteredCommands);
|
|
1861
|
+
if (commands.length === 0) throw new Error("root lint scripts contain no supported oxlint invocation");
|
|
1862
|
+
const lintedFiles = commands.flatMap(({ cwd, args, runtime }) => readCommand({
|
|
1863
|
+
root: cwd,
|
|
1864
|
+
executable: oxlint,
|
|
1865
|
+
args: [
|
|
1866
|
+
...args,
|
|
1867
|
+
"--debug",
|
|
1868
|
+
"files",
|
|
1869
|
+
"--threads",
|
|
1870
|
+
"1"
|
|
1871
|
+
],
|
|
1872
|
+
runtime
|
|
1873
|
+
}).split(/\r?\n/).filter((file) => file !== "").map((file) => relativePath(root, node_path.default.resolve(cwd, file))));
|
|
1874
|
+
const nearestProject = (start) => {
|
|
1875
|
+
let directory = start;
|
|
1876
|
+
while (true) {
|
|
1877
|
+
const config = node_path.default.posix.join(directory, "tsconfig.json");
|
|
1878
|
+
if (trackedFiles.has(config)) return config;
|
|
1879
|
+
if (directory === ".") return void 0;
|
|
1880
|
+
directory = node_path.default.posix.dirname(directory);
|
|
1881
|
+
}
|
|
1882
|
+
};
|
|
1883
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1884
|
+
for (const file of new Set(lintedFiles)) {
|
|
1885
|
+
if (!trackedFiles.has(file) || scriptSourceKind(file) === void 0) continue;
|
|
1886
|
+
const config = nearestProject(node_path.default.posix.dirname(file));
|
|
1887
|
+
if (config === void 0) continue;
|
|
1888
|
+
const group = groups.get(config);
|
|
1889
|
+
if (group === void 0) groups.set(config, [file]);
|
|
1890
|
+
else group.push(file);
|
|
1891
|
+
}
|
|
1892
|
+
const projects = /* @__PURE__ */ new Map();
|
|
1893
|
+
const readProject = (config) => {
|
|
1894
|
+
const cached = projects.get(config);
|
|
1895
|
+
if (cached !== void 0) return cached;
|
|
1896
|
+
const result = {
|
|
1897
|
+
files: /* @__PURE__ */ new Set(),
|
|
1898
|
+
references: []
|
|
1899
|
+
};
|
|
1900
|
+
try {
|
|
1901
|
+
const project = JSON.parse(readCommand({
|
|
1902
|
+
root,
|
|
1903
|
+
executable: compiler,
|
|
1904
|
+
args: [
|
|
1905
|
+
"--showConfig",
|
|
1906
|
+
"--project",
|
|
1907
|
+
config
|
|
1908
|
+
],
|
|
1909
|
+
runtime: "node"
|
|
1910
|
+
}));
|
|
1911
|
+
if (!record$4(project)) throw new Error("compiler configuration must be an object");
|
|
1912
|
+
const references = project["references"] ?? [];
|
|
1913
|
+
if (!Array.isArray(references)) throw new Error("compiler configuration references must be an array");
|
|
1914
|
+
for (const reference of references) {
|
|
1915
|
+
if (!record$4(reference) || typeof reference["path"] !== "string") throw new Error("compiler project reference requires a path");
|
|
1916
|
+
const target = relativePath(root, node_path.default.resolve(root, node_path.default.dirname(config), reference["path"]));
|
|
1917
|
+
const referencedConfig = target.endsWith(".json") ? target : node_path.default.posix.join(target, "tsconfig.json");
|
|
1918
|
+
if (!trackedFiles.has(referencedConfig)) throw new Error(`project reference is not tracked: ${referencedConfig}`);
|
|
1919
|
+
result.references.push(referencedConfig);
|
|
1920
|
+
}
|
|
1921
|
+
const sources = project["files"] ?? [];
|
|
1922
|
+
if (!stringList(sources)) throw new Error("compiler configuration did not return a files array");
|
|
1923
|
+
const options = project["compilerOptions"];
|
|
1924
|
+
for (const file of sources) {
|
|
1925
|
+
if (scriptSourceKind(file) === "javascript" && (!record$4(options) || options["allowJs"] !== true && options["checkJs"] !== true)) continue;
|
|
1926
|
+
result.files.add(relativePath(root, node_path.default.resolve(root, node_path.default.dirname(config), file)));
|
|
1927
|
+
}
|
|
1928
|
+
} catch (error) {
|
|
1929
|
+
result.files.clear();
|
|
1930
|
+
result.references.length = 0;
|
|
1931
|
+
addConfiguration(config, error instanceof Error ? error.message : String(error));
|
|
1932
|
+
}
|
|
1933
|
+
projects.set(config, result);
|
|
1934
|
+
return result;
|
|
1935
|
+
};
|
|
1936
|
+
const projectFiles = /* @__PURE__ */ new Set();
|
|
1937
|
+
for (const [nearest, sources] of groups) {
|
|
1938
|
+
const remaining = new Set(sources);
|
|
1939
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1940
|
+
let ancestor = nearest;
|
|
1941
|
+
while (ancestor !== void 0 && remaining.size > 0) {
|
|
1942
|
+
const pending = [ancestor];
|
|
1943
|
+
let config = pending.pop();
|
|
1944
|
+
while (config !== void 0 && remaining.size > 0) {
|
|
1945
|
+
if (!visited.has(config)) {
|
|
1946
|
+
visited.add(config);
|
|
1947
|
+
const project = readProject(config);
|
|
1948
|
+
for (const source of remaining) {
|
|
1949
|
+
if (!project.files.has(source)) continue;
|
|
1950
|
+
projectFiles.add(source);
|
|
1951
|
+
remaining.delete(source);
|
|
1952
|
+
}
|
|
1953
|
+
pending.push(...project.references);
|
|
1954
|
+
}
|
|
1955
|
+
config = pending.pop();
|
|
1956
|
+
}
|
|
1957
|
+
const directory = node_path.default.posix.dirname(ancestor);
|
|
1958
|
+
ancestor = directory === "." ? void 0 : nearestProject(node_path.default.posix.dirname(directory));
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
diagnostics.push(...checkProjectOwnership({
|
|
1962
|
+
trackedFiles,
|
|
1963
|
+
lintedFiles,
|
|
1964
|
+
projectFiles
|
|
1965
|
+
}));
|
|
1966
|
+
} catch (error) {
|
|
1967
|
+
addConfiguration("package.json", error instanceof Error ? error.message : String(error));
|
|
1968
|
+
}
|
|
1969
|
+
return diagnostics;
|
|
1970
|
+
};
|
|
1705
1971
|
//#endregion
|
|
1706
1972
|
//#region src/toolchain-rust.ts
|
|
1707
|
-
const record$
|
|
1973
|
+
const record$3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1708
1974
|
/** Exceptions authorize one immutable release, never a channel or an entire rule. */
|
|
1709
1975
|
const parseRustExceptions = (input, today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)) => {
|
|
1710
1976
|
if (input === void 0) return [];
|
|
1711
1977
|
if (!Array.isArray(input)) throw new Error("rustExceptions must be an array");
|
|
1712
1978
|
const pins = /* @__PURE__ */ new Set();
|
|
1713
1979
|
return input.map((entry) => {
|
|
1714
|
-
if (!record$
|
|
1980
|
+
if (!record$3(entry) || Object.keys(entry).length !== 4 || Object.keys(entry).some((key) => ![
|
|
1715
1981
|
"pin",
|
|
1716
1982
|
"reason",
|
|
1717
1983
|
"tracking",
|
|
@@ -1865,18 +2131,18 @@ const checkRustReferences = (options) => {
|
|
|
1865
2131
|
if (require_toolchain_inputs.isShellScript(file, text)) shell(text);
|
|
1866
2132
|
if (/(?:^|\/)package\.json$/.test(file)) try {
|
|
1867
2133
|
const object = JSON.parse(text);
|
|
1868
|
-
if (record$
|
|
2134
|
+
if (record$3(object) && record$3(object["scripts"])) {
|
|
1869
2135
|
for (const [name, script] of Object.entries(object["scripts"])) if (typeof script === "string") shell(script, lineOf(`"${name}"`));
|
|
1870
2136
|
}
|
|
1871
2137
|
} catch {}
|
|
1872
2138
|
if (/(?:^|\/)ci-service-images\.json$/.test(file)) {
|
|
1873
2139
|
const images = (value) => {
|
|
1874
|
-
if (typeof value === "string" && /^rust(?=:|@|$)/.test(canonicalDockerRuntime(value))) {
|
|
1875
|
-
const tag = canonicalDockerRuntime(value).replace(/^rust:?/, "").split("@").at(0);
|
|
1876
|
-
const version = tag === void 0 ? void 0 : rustImageVersion(tag);
|
|
2140
|
+
if (typeof value === "string" && /^rust(?=:|@|$)/.test(require_toolchain_dependabot.canonicalDockerRuntime(value))) {
|
|
2141
|
+
const tag = require_toolchain_dependabot.canonicalDockerRuntime(value).replace(/^rust:?/, "").split("@").at(0);
|
|
2142
|
+
const version = tag === void 0 ? void 0 : require_toolchain_dependabot.rustImageVersion(tag);
|
|
1877
2143
|
select(version, "Rust service image", lineOf(value));
|
|
1878
2144
|
} else if (Array.isArray(value)) value.forEach(images);
|
|
1879
|
-
else if (record$
|
|
2145
|
+
else if (record$3(value)) {
|
|
1880
2146
|
for (const [key, entry] of Object.entries(value)) if (key !== "name") images(entry);
|
|
1881
2147
|
}
|
|
1882
2148
|
};
|
|
@@ -1900,13 +2166,13 @@ const checkRustReferences = (options) => {
|
|
|
1900
2166
|
value.forEach(visit);
|
|
1901
2167
|
return;
|
|
1902
2168
|
}
|
|
1903
|
-
if (!record$
|
|
1904
|
-
if (record$
|
|
2169
|
+
if (!record$3(value)) return;
|
|
2170
|
+
if (record$3(value["env"]) && value["env"]["RUSTUP_TOOLCHAIN"] !== void 0) select(value["env"]["RUSTUP_TOOLCHAIN"], "RUSTUP_TOOLCHAIN", lineOf("RUSTUP_TOOLCHAIN"));
|
|
1905
2171
|
if (typeof value["run"] === "string") shell(value["run"], lineOf("run:"));
|
|
1906
2172
|
const uses = value["uses"];
|
|
1907
2173
|
if (typeof uses === "string") {
|
|
1908
2174
|
const action = uses.split("@").at(0)?.toLowerCase();
|
|
1909
|
-
const inputs = record$
|
|
2175
|
+
const inputs = record$3(value["with"]) ? value["with"] : {};
|
|
1910
2176
|
if (action === "dtolnay/rust-toolchain" || action === "actions-rust-lang/setup-rust-toolchain") {
|
|
1911
2177
|
const entries = Object.entries(inputs).filter(([key]) => key.toLowerCase() === "toolchain");
|
|
1912
2178
|
if (entries.length !== 1) select(void 0, `${action} requires an explicit exact toolchain input`, lineOf(uses));
|
|
@@ -1920,8 +2186,8 @@ const checkRustReferences = (options) => {
|
|
|
1920
2186
|
}
|
|
1921
2187
|
}
|
|
1922
2188
|
for (const [key, child] of Object.entries(value)) {
|
|
1923
|
-
if (key === "workflow_call" && record$
|
|
1924
|
-
for (const [input, declaration] of Object.entries(child["inputs"])) if (["toolchain", "rust-toolchain"].includes(input) && record$
|
|
2189
|
+
if (key === "workflow_call" && record$3(child) && record$3(child["inputs"])) {
|
|
2190
|
+
for (const [input, declaration] of Object.entries(child["inputs"])) if (["toolchain", "rust-toolchain"].includes(input) && record$3(declaration) && declaration["default"] !== void 0) select(declaration["default"], `reusable workflow default ${input}`, lineOf("default:"));
|
|
1925
2191
|
}
|
|
1926
2192
|
visit(child);
|
|
1927
2193
|
}
|
|
@@ -1931,6 +2197,374 @@ const checkRustReferences = (options) => {
|
|
|
1931
2197
|
return diagnostics;
|
|
1932
2198
|
};
|
|
1933
2199
|
//#endregion
|
|
2200
|
+
//#region src/toolchain-workflow-trust.ts
|
|
2201
|
+
const record$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2202
|
+
const booleanParts = (source, operator) => {
|
|
2203
|
+
const parts = [];
|
|
2204
|
+
let depth = 0;
|
|
2205
|
+
let quoted = false;
|
|
2206
|
+
let start = 0;
|
|
2207
|
+
for (let index = 0; index < source.length; index++) {
|
|
2208
|
+
const character = source[index];
|
|
2209
|
+
if (character === "'") {
|
|
2210
|
+
if (quoted && source[index + 1] === "'") index++;
|
|
2211
|
+
else quoted = !quoted;
|
|
2212
|
+
}
|
|
2213
|
+
if (quoted) continue;
|
|
2214
|
+
if (character === "(") depth++;
|
|
2215
|
+
if (character === ")") depth--;
|
|
2216
|
+
if (depth === 0 && source.slice(index, index + 2) === operator) {
|
|
2217
|
+
parts.push(source.slice(start, index));
|
|
2218
|
+
start = index + 2;
|
|
2219
|
+
index++;
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
parts.push(source.slice(start));
|
|
2223
|
+
return parts;
|
|
2224
|
+
};
|
|
2225
|
+
const withoutOuterParentheses = (source) => {
|
|
2226
|
+
if (!source.startsWith("(") || !source.endsWith(")")) return source;
|
|
2227
|
+
let depth = 0;
|
|
2228
|
+
let quoted = false;
|
|
2229
|
+
for (let index = 0; index < source.length; index++) {
|
|
2230
|
+
const character = source[index];
|
|
2231
|
+
if (character === "'") {
|
|
2232
|
+
if (quoted && source[index + 1] === "'") index++;
|
|
2233
|
+
else quoted = !quoted;
|
|
2234
|
+
}
|
|
2235
|
+
if (quoted) continue;
|
|
2236
|
+
if (character === "(") depth++;
|
|
2237
|
+
if (character === ")") depth--;
|
|
2238
|
+
if (depth === 0 && index !== source.length - 1) return source;
|
|
2239
|
+
}
|
|
2240
|
+
return withoutOuterParentheses(source.slice(1, -1).trim());
|
|
2241
|
+
};
|
|
2242
|
+
const excludesUntrusted = ({ condition, event, allowTrust = true }) => {
|
|
2243
|
+
if (condition === false) return true;
|
|
2244
|
+
if (typeof condition !== "string") return false;
|
|
2245
|
+
const source = withoutOuterParentheses(condition.trim().replace(/^\$\{\{([\s\S]*)\}\}$/u, "$1").trim());
|
|
2246
|
+
const alternatives = booleanParts(source, "||");
|
|
2247
|
+
if (alternatives.length > 1) return alternatives.every((part) => excludesUntrusted({
|
|
2248
|
+
condition: part,
|
|
2249
|
+
event,
|
|
2250
|
+
allowTrust
|
|
2251
|
+
}));
|
|
2252
|
+
const conjuncts = booleanParts(source, "&&");
|
|
2253
|
+
if (conjuncts.length > 1) return conjuncts.some((part) => excludesUntrusted({
|
|
2254
|
+
condition: part,
|
|
2255
|
+
event,
|
|
2256
|
+
allowTrust
|
|
2257
|
+
}));
|
|
2258
|
+
const trustComparison = /^needs\.trust-check\.outputs\.trusted\s*==\s*'([^']*)'$/u.exec(source);
|
|
2259
|
+
if (allowTrust && trustComparison?.at(1)?.toLowerCase() === "true") return true;
|
|
2260
|
+
if (source === "false") return true;
|
|
2261
|
+
const comparison = /^github\.event_name\s*(==|!=)\s*'([^']+)'$/u.exec(source);
|
|
2262
|
+
if (comparison === null) return false;
|
|
2263
|
+
const equal = comparison.at(2)?.toLowerCase() === event.toLowerCase();
|
|
2264
|
+
return comparison.at(1) === "==" ? !equal : equal;
|
|
2265
|
+
};
|
|
2266
|
+
const staticData = (word) => !/[`$\\]/u.test(word) && !/[<>]/u.test(word);
|
|
2267
|
+
const metadataScript = (command) => {
|
|
2268
|
+
const interpolation = /\$\{\{([\s\S]*?)\}\}/gu;
|
|
2269
|
+
for (const match of command.matchAll(interpolation)) {
|
|
2270
|
+
const expression = match.at(1);
|
|
2271
|
+
if (expression === void 0 || ![
|
|
2272
|
+
"github.sha",
|
|
2273
|
+
"github.run_id",
|
|
2274
|
+
"github.run_attempt",
|
|
2275
|
+
"github.repository"
|
|
2276
|
+
].includes(expression.trim())) return void 0;
|
|
2277
|
+
}
|
|
2278
|
+
const source = command.replace(interpolation, "stll-metadata");
|
|
2279
|
+
return source.includes("${{") ? void 0 : source;
|
|
2280
|
+
};
|
|
2281
|
+
const metadataWord = (raw) => {
|
|
2282
|
+
if (/^'[^']*'$/u.test(raw)) return true;
|
|
2283
|
+
if ([
|
|
2284
|
+
"[",
|
|
2285
|
+
"]",
|
|
2286
|
+
"[[",
|
|
2287
|
+
"]]"
|
|
2288
|
+
].includes(raw)) return true;
|
|
2289
|
+
let quote;
|
|
2290
|
+
for (const character of raw) {
|
|
2291
|
+
if (quote === void 0 && "*?~[{$".includes(character)) return false;
|
|
2292
|
+
if (character === quote) quote = void 0;
|
|
2293
|
+
else if (quote === void 0 && (character === "'" || character === "\"")) quote = character;
|
|
2294
|
+
}
|
|
2295
|
+
if (quote !== void 0) return false;
|
|
2296
|
+
const withoutKnownExpressions = raw.replace(/\$\{[A-Za-z_][A-Za-z_0-9]*\}|\$[A-Za-z_][A-Za-z_0-9]*/gu, "value");
|
|
2297
|
+
return !/[`$\\]/u.test(withoutKnownExpressions);
|
|
2298
|
+
};
|
|
2299
|
+
const safeComparison = (words) => {
|
|
2300
|
+
const first = words.at(0);
|
|
2301
|
+
if (![
|
|
2302
|
+
"test",
|
|
2303
|
+
"[",
|
|
2304
|
+
"[["
|
|
2305
|
+
].includes(first ?? "")) return false;
|
|
2306
|
+
let end;
|
|
2307
|
+
if (first === "[[") end = "]]";
|
|
2308
|
+
else if (first === "[") end = "]";
|
|
2309
|
+
if (end !== void 0 && words.at(-1) !== end) return false;
|
|
2310
|
+
const operands = words.slice(1, end === void 0 ? void 0 : -1);
|
|
2311
|
+
if (operands.length === 1) return true;
|
|
2312
|
+
if (operands.length === 2) return ["-n", "-z"].includes(operands.at(0) ?? "");
|
|
2313
|
+
return operands.length === 3 && [
|
|
2314
|
+
"=",
|
|
2315
|
+
"==",
|
|
2316
|
+
"!="
|
|
2317
|
+
].includes(operands.at(1) ?? "");
|
|
2318
|
+
};
|
|
2319
|
+
const objectReference = (value) => /^(?:HEAD|[a-f0-9]{7,64}|(?:refs\/(?:heads|tags|remotes)\/|origin\/)[A-Za-z0-9][A-Za-z0-9._/-]*)(?:[~^][0-9]*)*$/u.test(value) && !value.includes("..") && !value.includes("//") && !value.endsWith("/");
|
|
2320
|
+
const workflowTrustBootstrapCatalog = {
|
|
2321
|
+
commands: {
|
|
2322
|
+
test: "comparison",
|
|
2323
|
+
"[": "comparison",
|
|
2324
|
+
"[[": "comparison",
|
|
2325
|
+
true: "status",
|
|
2326
|
+
false: "status",
|
|
2327
|
+
exit: "exit",
|
|
2328
|
+
echo: "output",
|
|
2329
|
+
printf: "format",
|
|
2330
|
+
git: "git",
|
|
2331
|
+
gh: "github"
|
|
2332
|
+
},
|
|
2333
|
+
git: {
|
|
2334
|
+
"rev-parse": {
|
|
2335
|
+
flags: ["--short", "--verify"],
|
|
2336
|
+
references: 1
|
|
2337
|
+
},
|
|
2338
|
+
"merge-base": {
|
|
2339
|
+
flags: ["--all", "--is-ancestor"],
|
|
2340
|
+
references: 2
|
|
2341
|
+
}
|
|
2342
|
+
},
|
|
2343
|
+
github: ["api"],
|
|
2344
|
+
actions: {
|
|
2345
|
+
"actions/checkout": [
|
|
2346
|
+
"fetch-depth",
|
|
2347
|
+
"persist-credentials",
|
|
2348
|
+
"ref",
|
|
2349
|
+
"repository",
|
|
2350
|
+
"submodules"
|
|
2351
|
+
],
|
|
2352
|
+
"oven-sh/setup-bun": ["bun-version", "no-cache"]
|
|
2353
|
+
}
|
|
2354
|
+
};
|
|
2355
|
+
const safeGit = (words) => {
|
|
2356
|
+
const selected = Object.entries(workflowTrustBootstrapCatalog.git).find(([name]) => name === words.at(1));
|
|
2357
|
+
if (selected === void 0) return false;
|
|
2358
|
+
const [, policy] = selected;
|
|
2359
|
+
const references = [];
|
|
2360
|
+
for (const argument of words.slice(2)) {
|
|
2361
|
+
if (!staticData(argument)) return false;
|
|
2362
|
+
if (!argument.startsWith("-")) {
|
|
2363
|
+
references.push(argument);
|
|
2364
|
+
continue;
|
|
2365
|
+
}
|
|
2366
|
+
if (!policy.flags.some((flag) => flag === argument)) return false;
|
|
2367
|
+
}
|
|
2368
|
+
return references.length === policy.references && references.every(objectReference);
|
|
2369
|
+
};
|
|
2370
|
+
const safeGh = (words) => {
|
|
2371
|
+
if (!words.every(staticData)) return false;
|
|
2372
|
+
if (workflowTrustBootstrapCatalog.github.some((name) => name === words.at(1))) {
|
|
2373
|
+
const arguments_ = words.slice(2);
|
|
2374
|
+
if (["--method", "-X"].includes(arguments_.at(0) ?? "") && arguments_.at(1) === "GET") arguments_.splice(0, 2);
|
|
2375
|
+
if (!arguments_.at(0) || arguments_.at(0)?.startsWith("-")) return false;
|
|
2376
|
+
for (let index = 1; index < arguments_.length; index++) {
|
|
2377
|
+
const argument = arguments_.at(index);
|
|
2378
|
+
if ((argument === "--method" || argument === "-X") && arguments_.at(index + 1) === "GET") index++;
|
|
2379
|
+
else if (["--jq", "-q"].includes(argument ?? "") && /^\.[A-Za-z0-9_.[\]" -]*$/u.test(arguments_.at(index + 1) ?? "")) index++;
|
|
2380
|
+
else return false;
|
|
2381
|
+
}
|
|
2382
|
+
return /^\/?repos\/[A-Za-z0-9_./-]+$/u.test(arguments_.at(0) ?? "");
|
|
2383
|
+
}
|
|
2384
|
+
return false;
|
|
2385
|
+
};
|
|
2386
|
+
const safeCommand = (raw) => {
|
|
2387
|
+
if (raw.length === 0) return true;
|
|
2388
|
+
if (!raw.every(metadataWord)) return false;
|
|
2389
|
+
const words = raw.map(decodeShellWord);
|
|
2390
|
+
const name = words.at(0);
|
|
2391
|
+
if (raw.at(0) !== name) return false;
|
|
2392
|
+
if (name === "if" || name === "elif") return safeComparison(words.slice(1));
|
|
2393
|
+
if (["then", "else"].includes(name ?? "")) return safeCommand(raw.slice(1));
|
|
2394
|
+
if (name === "fi") return words.length === 1;
|
|
2395
|
+
const kind = Object.entries(workflowTrustBootstrapCatalog.commands).find(([command]) => command === name)?.[1];
|
|
2396
|
+
if (kind === "comparison") return safeComparison(words);
|
|
2397
|
+
if (kind === "status") return words.length === 1;
|
|
2398
|
+
if (kind === "exit") return words.length === 1 || words.length === 2 && /^[0-9]+$/u.test(words.at(1) ?? "");
|
|
2399
|
+
if (kind === "output") return true;
|
|
2400
|
+
if (kind === "format") {
|
|
2401
|
+
const format = words.at(1);
|
|
2402
|
+
return format !== void 0 && !format.startsWith("-") && /^(?:[^%$`]|%%|%s)*$/u.test(format);
|
|
2403
|
+
}
|
|
2404
|
+
if (kind === "git") return safeGit(words);
|
|
2405
|
+
if (kind === "github") return safeGh(words);
|
|
2406
|
+
return false;
|
|
2407
|
+
};
|
|
2408
|
+
const invokesRepositoryCode = (command) => {
|
|
2409
|
+
const source = metadataScript(command);
|
|
2410
|
+
if (source === void 0) return true;
|
|
2411
|
+
const tokens = shellTokens(source);
|
|
2412
|
+
for (const [index, token] of tokens.entries()) {
|
|
2413
|
+
if (token.kind !== "redirect") continue;
|
|
2414
|
+
const target = tokens.at(index + 1);
|
|
2415
|
+
if (![">", ">>"].includes(token.value) || target?.kind !== "word" || ![
|
|
2416
|
+
"\"$GITHUB_OUTPUT\"",
|
|
2417
|
+
"\"${GITHUB_OUTPUT}\"",
|
|
2418
|
+
"/dev/null"
|
|
2419
|
+
].includes(target.value)) return true;
|
|
2420
|
+
}
|
|
2421
|
+
const branches = [];
|
|
2422
|
+
for (let words of shellCommandSegments(source, "tokens")) {
|
|
2423
|
+
if (!safeCommand(words)) return true;
|
|
2424
|
+
while (["then", "else"].includes(words.at(0) ?? "")) {
|
|
2425
|
+
const keyword = words.at(0);
|
|
2426
|
+
const previous = branches.pop();
|
|
2427
|
+
if (keyword === "then" && previous === "condition") branches.push("then");
|
|
2428
|
+
else if (keyword === "else" && previous === "then") branches.push("else");
|
|
2429
|
+
else return true;
|
|
2430
|
+
words = words.slice(1);
|
|
2431
|
+
}
|
|
2432
|
+
const keyword = words.at(0);
|
|
2433
|
+
if (keyword === "if") branches.push("condition");
|
|
2434
|
+
if (keyword === "elif") {
|
|
2435
|
+
if (branches.pop() !== "then") return true;
|
|
2436
|
+
branches.push("condition");
|
|
2437
|
+
}
|
|
2438
|
+
if (keyword === "fi") {
|
|
2439
|
+
const previous = branches.pop();
|
|
2440
|
+
if (previous !== "then" && previous !== "else") return true;
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
return branches.length !== 0;
|
|
2444
|
+
};
|
|
2445
|
+
const safeAction = (uses, inputs) => {
|
|
2446
|
+
if (typeof uses !== "string" || !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_./-]+@[a-f0-9]{40}$/u.test(uses)) return false;
|
|
2447
|
+
const repository = uses.split("@").at(0);
|
|
2448
|
+
const approved = Object.entries(workflowTrustBootstrapCatalog.actions).find(([name]) => name === repository);
|
|
2449
|
+
if (approved === void 0) return false;
|
|
2450
|
+
if (inputs === void 0) return repository === "actions/checkout";
|
|
2451
|
+
if (!record$2(inputs)) return false;
|
|
2452
|
+
if (repository !== "actions/checkout") {
|
|
2453
|
+
const version = inputs["bun-version"];
|
|
2454
|
+
if (typeof version !== "string" || !/^v?[0-9]+(?:\.(?:[0-9]+|x)){0,2}$/u.test(version)) return false;
|
|
2455
|
+
}
|
|
2456
|
+
const [, allowedInputs] = approved;
|
|
2457
|
+
return Object.entries(inputs).every(([name, value]) => allowedInputs.some((allowed) => allowed === name) && [
|
|
2458
|
+
"string",
|
|
2459
|
+
"boolean",
|
|
2460
|
+
"number"
|
|
2461
|
+
].includes(typeof value) && (name !== "submodules" || value === false || value === "false"));
|
|
2462
|
+
};
|
|
2463
|
+
const safeShell = (shell) => shell === void 0 || shell === "bash" || shell === "sh";
|
|
2464
|
+
const safeEnvironment = (value) => value === void 0 || record$2(value) && Object.keys(value).every((name) => [
|
|
2465
|
+
"HEAD_REPO",
|
|
2466
|
+
"BASE_REPO",
|
|
2467
|
+
"HAS_RUN_CI_LABEL",
|
|
2468
|
+
"PR_TITLE",
|
|
2469
|
+
"HEAD_REF",
|
|
2470
|
+
"ISSUE_BODY",
|
|
2471
|
+
"COMMENT_BODY",
|
|
2472
|
+
"GH_TOKEN",
|
|
2473
|
+
"GITHUB_TOKEN",
|
|
2474
|
+
"HAWK_RUST_TOOLCHAIN",
|
|
2475
|
+
"HAWK_VERSION",
|
|
2476
|
+
"HAWK_X86_64_UNKNOWN_LINUX_GNU_SHA256"
|
|
2477
|
+
].includes(name));
|
|
2478
|
+
const configuredShell = (value) => record$2(value) && record$2(value["run"]) ? value["run"]["shell"] : void 0;
|
|
2479
|
+
const workflowTrustRules = ["workflow-trust"];
|
|
2480
|
+
const workflowEvents = (triggers) => {
|
|
2481
|
+
if (typeof triggers === "string") return [triggers];
|
|
2482
|
+
if (Array.isArray(triggers)) return triggers;
|
|
2483
|
+
if (record$2(triggers)) return Object.keys(triggers);
|
|
2484
|
+
return [];
|
|
2485
|
+
};
|
|
2486
|
+
/** Enforce a workflow's declared trust boundary before repository dependency execution. */
|
|
2487
|
+
const checkWorkflowTrust = (workflow) => {
|
|
2488
|
+
const findings = [];
|
|
2489
|
+
if (!record$2(workflow) || !record$2(workflow["jobs"])) return findings;
|
|
2490
|
+
const jobs = workflow["jobs"];
|
|
2491
|
+
if (!record$2(jobs["trust-check"])) return findings;
|
|
2492
|
+
const triggers = workflow["on"];
|
|
2493
|
+
const pullRequests = workflowEvents(triggers).filter((event) => event === "pull_request" || event === "pull_request_target");
|
|
2494
|
+
if (pullRequests.length === 0) return findings;
|
|
2495
|
+
for (const [name, job] of Object.entries(jobs)) {
|
|
2496
|
+
if (!record$2(job)) continue;
|
|
2497
|
+
const needs = job["needs"];
|
|
2498
|
+
const ordered = name !== "trust-check" && (needs === "trust-check" || Array.isArray(needs) && needs.includes("trust-check"));
|
|
2499
|
+
const permitsExecution = (stepCondition) => {
|
|
2500
|
+
const guarded = pullRequests.every((event) => excludesUntrusted({
|
|
2501
|
+
condition: job["if"],
|
|
2502
|
+
event
|
|
2503
|
+
}) || excludesUntrusted({
|
|
2504
|
+
condition: stepCondition,
|
|
2505
|
+
event
|
|
2506
|
+
}));
|
|
2507
|
+
if (ordered && guarded) return true;
|
|
2508
|
+
return pullRequests.every((event) => excludesUntrusted({
|
|
2509
|
+
condition: job["if"],
|
|
2510
|
+
event,
|
|
2511
|
+
allowTrust: false
|
|
2512
|
+
}) || excludesUntrusted({
|
|
2513
|
+
condition: stepCondition,
|
|
2514
|
+
event,
|
|
2515
|
+
allowTrust: false
|
|
2516
|
+
}));
|
|
2517
|
+
};
|
|
2518
|
+
const message = `jobs.${name} repository commands require needs: trust-check and a condition requiring its trusted output on pull requests`;
|
|
2519
|
+
if (job["uses"] !== void 0 && !permitsExecution(void 0)) findings.push({
|
|
2520
|
+
type: "job",
|
|
2521
|
+
property: "uses",
|
|
2522
|
+
job: name,
|
|
2523
|
+
message
|
|
2524
|
+
});
|
|
2525
|
+
if (!permitsExecution(void 0)) {
|
|
2526
|
+
if (job["container"] !== void 0) findings.push({
|
|
2527
|
+
type: "job",
|
|
2528
|
+
property: "container",
|
|
2529
|
+
job: name,
|
|
2530
|
+
message
|
|
2531
|
+
});
|
|
2532
|
+
const services = job["services"];
|
|
2533
|
+
if (services !== void 0 && (!record$2(services) || Object.keys(services).length > 0)) findings.push({
|
|
2534
|
+
type: "job",
|
|
2535
|
+
property: "services",
|
|
2536
|
+
job: name,
|
|
2537
|
+
message
|
|
2538
|
+
});
|
|
2539
|
+
}
|
|
2540
|
+
if (!Array.isArray(job["steps"])) continue;
|
|
2541
|
+
for (const [index, step] of job["steps"].entries()) {
|
|
2542
|
+
if (!record$2(step)) continue;
|
|
2543
|
+
const run = step["run"];
|
|
2544
|
+
const uses = step["uses"];
|
|
2545
|
+
const unsafeRun = typeof run === "string" && (invokesRepositoryCode(run) || !safeShell(step["shell"] ?? configuredShell(job["defaults"]) ?? configuredShell(workflow["defaults"])) || ![
|
|
2546
|
+
workflow["env"],
|
|
2547
|
+
job["env"],
|
|
2548
|
+
step["env"]
|
|
2549
|
+
].every(safeEnvironment));
|
|
2550
|
+
const unsafeAction = uses !== void 0 && (!safeAction(uses, step["with"]) || ![
|
|
2551
|
+
workflow["env"],
|
|
2552
|
+
job["env"],
|
|
2553
|
+
step["env"]
|
|
2554
|
+
].every(safeEnvironment));
|
|
2555
|
+
if (!unsafeRun && !unsafeAction) continue;
|
|
2556
|
+
if (permitsExecution(unsafeAction ? void 0 : step["if"])) continue;
|
|
2557
|
+
findings.push({
|
|
2558
|
+
type: "step",
|
|
2559
|
+
job: name,
|
|
2560
|
+
step: index,
|
|
2561
|
+
message
|
|
2562
|
+
});
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
return findings;
|
|
2566
|
+
};
|
|
2567
|
+
//#endregion
|
|
1934
2568
|
//#region src/toolchain-runtime.ts
|
|
1935
2569
|
const runtimeRules = [
|
|
1936
2570
|
"bun-pins",
|
|
@@ -1940,7 +2574,8 @@ const runtimeRules = [
|
|
|
1940
2574
|
"runtime-manager",
|
|
1941
2575
|
"runtime-docker",
|
|
1942
2576
|
"runtime-workflow",
|
|
1943
|
-
"action-pins"
|
|
2577
|
+
"action-pins",
|
|
2578
|
+
...workflowTrustRules
|
|
1944
2579
|
];
|
|
1945
2580
|
const record$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1946
2581
|
const dockerEnvironmentAssignments = ({ source, escape }) => {
|
|
@@ -2306,7 +2941,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2306
2941
|
});
|
|
2307
2942
|
const checkRuntimeImage = ({ image, line, label, at }) => {
|
|
2308
2943
|
const unknownRuntime = () => {
|
|
2309
|
-
const rustImage = typeof image === "string" && /^rust(?=:|@|$)/i.test(canonicalDockerRuntime(image));
|
|
2944
|
+
const rustImage = typeof image === "string" && /^rust(?=:|@|$)/i.test(require_toolchain_dependabot.canonicalDockerRuntime(image));
|
|
2310
2945
|
if (!rustImage && dynamic(image) && authorizeDynamic("image", line, at)) return;
|
|
2311
2946
|
add({
|
|
2312
2947
|
rule: rustImage ? "rust-version" : "runtime-docker",
|
|
@@ -2318,7 +2953,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2318
2953
|
unknownRuntime();
|
|
2319
2954
|
return;
|
|
2320
2955
|
}
|
|
2321
|
-
const canonical = canonicalDockerRuntime(image);
|
|
2956
|
+
const canonical = require_toolchain_dependabot.canonicalDockerRuntime(image);
|
|
2322
2957
|
const structuralImage = canonical.replace(/\$\{[^}]*\}|\$[A-Za-z_][A-Za-z_\d]*/g, (variable) => "x".repeat(variable.length));
|
|
2323
2958
|
const digestSeparator = structuralImage.indexOf("@");
|
|
2324
2959
|
const reference = digestSeparator < 0 ? canonical : canonical.slice(0, digestSeparator);
|
|
@@ -2351,7 +2986,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2351
2986
|
}
|
|
2352
2987
|
const tag = hasTag ? reference.slice(tagSeparator + 1) : void 0;
|
|
2353
2988
|
const variantSuffix = tool === "oven/bun" ? /-(?:alpine|slim|debian)$/ : /-(?:alpine(?:\d+(?:\.\d+)*)?|(?:bookworm|bullseye|trixie|buster)(?:-slim)?|slim)$/;
|
|
2354
|
-
const version = tool === "rust" && tag !== void 0 ? rustImageVersion(tag) : tag?.replace(variantSuffix, "");
|
|
2989
|
+
const version = tool === "rust" && tag !== void 0 ? require_toolchain_dependabot.rustImageVersion(tag) : tag?.replace(variantSuffix, "");
|
|
2355
2990
|
let expected = policy.bun;
|
|
2356
2991
|
if (tool === "node") expected = policy.node;
|
|
2357
2992
|
else if (tool === "python") expected = policy.python;
|
|
@@ -3174,8 +3809,29 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
3174
3809
|
}
|
|
3175
3810
|
};
|
|
3176
3811
|
try {
|
|
3177
|
-
document.toJS({ maxAliasCount: 100 });
|
|
3812
|
+
const workflow = document.toJS({ maxAliasCount: 100 });
|
|
3178
3813
|
if (automationKind === "workflow") {
|
|
3814
|
+
for (const finding of checkWorkflowTrust(workflow)) {
|
|
3815
|
+
const jobNode = getNode(getNode(document.contents, "jobs"), finding.job);
|
|
3816
|
+
let source;
|
|
3817
|
+
switch (finding.type) {
|
|
3818
|
+
case "job":
|
|
3819
|
+
source = getNode(jobNode, finding.property);
|
|
3820
|
+
break;
|
|
3821
|
+
case "step": {
|
|
3822
|
+
const steps = resolveNode(getNode(jobNode, "steps"));
|
|
3823
|
+
const step = (0, yaml.isSeq)(steps) ? steps.items.at(finding.step) : void 0;
|
|
3824
|
+
source = getNode(step, "run") ?? getNode(step, "uses");
|
|
3825
|
+
break;
|
|
3826
|
+
}
|
|
3827
|
+
default: throw new Error("unknown workflow finding", { cause: finding });
|
|
3828
|
+
}
|
|
3829
|
+
add({
|
|
3830
|
+
rule: "workflow-trust",
|
|
3831
|
+
line: nodeLine(source ?? jobNode),
|
|
3832
|
+
message: finding.message
|
|
3833
|
+
});
|
|
3834
|
+
}
|
|
3179
3835
|
const jobs = getNode(document.contents, "jobs");
|
|
3180
3836
|
if ((0, yaml.isMap)(jobs)) for (const key of keysOf(jobs)) {
|
|
3181
3837
|
checkoutBindings = [];
|
|
@@ -3228,7 +3884,8 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
3228
3884
|
const toolchainRules = [.../* @__PURE__ */ new Set([
|
|
3229
3885
|
...packageRules,
|
|
3230
3886
|
...runtimeRules,
|
|
3231
|
-
...dependabotRules,
|
|
3887
|
+
...require_toolchain_dependabot.dependabotRules,
|
|
3888
|
+
nodeProjectRule,
|
|
3232
3889
|
"cloud-setup-drift"
|
|
3233
3890
|
])];
|
|
3234
3891
|
const record = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -3243,7 +3900,9 @@ const parseToolchainConfiguration = (input) => {
|
|
|
3243
3900
|
if (!record(entry) || Object.keys(entry).some((key) => key !== "rule" && key !== "reason") || typeof entry["rule"] !== "string" || !toolchainRules.some((rule) => rule === entry["rule"]) || typeof entry["reason"] !== "string" || entry["reason"].trim() === "" || disabled.has(entry["rule"])) throw new Error("each opt-out requires a unique known rule and a nonempty reason");
|
|
3244
3901
|
if (entry["rule"] === "rust-version") throw new Error("rust-version requires expiring exact-pin rustExceptions, not an opt-out");
|
|
3245
3902
|
if (entry["rule"] === "node-engine") throw new Error("node-engine cannot be opted out; engines.node must support the shared Node series");
|
|
3903
|
+
if (entry["rule"] === "workflow-trust") throw new Error("workflow-trust cannot be opted out; repository commands must follow the declared trust decision");
|
|
3246
3904
|
if (entry["rule"] === "cloud-setup-drift") throw new Error("cloud-setup-drift cannot be opted out; generated setup must match shared policy");
|
|
3905
|
+
if (entry["rule"] === "type-aware-projects") throw new Error("type-aware-projects cannot be opted out; linted files require declared TypeScript projects");
|
|
3247
3906
|
disabled.add(entry["rule"]);
|
|
3248
3907
|
}
|
|
3249
3908
|
return {
|
|
@@ -3266,12 +3925,13 @@ const readToolchainInputs = (root) => {
|
|
|
3266
3925
|
const kind = require_toolchain_inputs.toolchainInputKind(file);
|
|
3267
3926
|
const shebangCandidate = kind === void 0 && node_path.default.posix.extname(file) === "" && !file.split("/").some((part) => part === "vendor" || part === "node_modules");
|
|
3268
3927
|
if (kind === void 0 && !shebangCandidate) continue;
|
|
3269
|
-
if (kind === "presence") {
|
|
3270
|
-
files[file] = "";
|
|
3271
|
-
continue;
|
|
3272
|
-
}
|
|
3273
3928
|
try {
|
|
3274
3929
|
const inputPath = node_path.default.join(resolvedRoot, file);
|
|
3930
|
+
if (kind === "presence") {
|
|
3931
|
+
(0, node_fs.lstatSync)(inputPath);
|
|
3932
|
+
files[file] = "";
|
|
3933
|
+
continue;
|
|
3934
|
+
}
|
|
3275
3935
|
if (shebangCandidate) {
|
|
3276
3936
|
const input = (0, node_fs.lstatSync)(inputPath);
|
|
3277
3937
|
if (!input.isFile() && !input.isSymbolicLink()) continue;
|
|
@@ -3292,7 +3952,8 @@ const readToolchainInputs = (root) => {
|
|
|
3292
3952
|
if (require_toolchain_inputs.toolchainInputKind(file, prefix.toString("utf8", 0, count)) === void 0) continue;
|
|
3293
3953
|
}
|
|
3294
3954
|
files[file] = (0, node_fs.readFileSync)(resolvedFile, "utf8");
|
|
3295
|
-
} catch {
|
|
3955
|
+
} catch (error) {
|
|
3956
|
+
if (kind === "presence" && error instanceof Error && "code" in error && error.code === "ENOENT") continue;
|
|
3296
3957
|
if (shebangCandidate) continue;
|
|
3297
3958
|
diagnostics.push({
|
|
3298
3959
|
rule: "configuration",
|
|
@@ -3399,6 +4060,17 @@ const checkToolchain = ({ root, policy, onDelegated }) => {
|
|
|
3399
4060
|
files,
|
|
3400
4061
|
policy
|
|
3401
4062
|
}));
|
|
4063
|
+
if (files["package.json"] !== void 0) {
|
|
4064
|
+
let manifest;
|
|
4065
|
+
try {
|
|
4066
|
+
manifest = JSON.parse(files["package.json"]);
|
|
4067
|
+
} catch {}
|
|
4068
|
+
diagnostics.push(...checkNodeProjects({
|
|
4069
|
+
root,
|
|
4070
|
+
manifest,
|
|
4071
|
+
trackedFiles
|
|
4072
|
+
}));
|
|
4073
|
+
}
|
|
3402
4074
|
for (const [file, text] of Object.entries(files)) diagnostics.push(...checkRuntimeFile({
|
|
3403
4075
|
file,
|
|
3404
4076
|
text,
|
|
@@ -3434,7 +4106,7 @@ const checkToolchain = ({ root, policy, onDelegated }) => {
|
|
|
3434
4106
|
line: 1,
|
|
3435
4107
|
message: `dynamic selector ${entry.path}:${entry.at ?? entry.line} must match exactly one unresolved ${entry.kind} selector`
|
|
3436
4108
|
});
|
|
3437
|
-
diagnostics.push(...checkDependabot({
|
|
4109
|
+
diagnostics.push(...require_toolchain_dependabot.checkDependabot({
|
|
3438
4110
|
files,
|
|
3439
4111
|
policy: policy.dependabot
|
|
3440
4112
|
}));
|