@pho9ubenaa/siro 0.4.3 → 0.5.1
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/CHANGELOG.md +65 -0
- package/README.md +18 -3
- package/dist/cli.mjs +13 -2
- package/dist/index.d.mts +78 -44
- package/dist/index.mjs +1 -1
- package/dist/{node-io-B3Hbcwyn.mjs → node-io-BDHg4J2w.mjs} +1361 -527
- package/package.json +5 -4
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { lstatSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
import satisfies from "semver/functions/satisfies.js";
|
|
5
5
|
import * as vb from "valibot";
|
|
6
|
-
import
|
|
6
|
+
import { lt, parse } from "semver";
|
|
7
|
+
import valid from "semver/functions/valid.js";
|
|
8
|
+
import { GLOBSTAR, Minimatch, braceExpand } from "minimatch";
|
|
7
9
|
import ini from "ini";
|
|
8
|
-
import { parse } from "smol-toml";
|
|
10
|
+
import { parse as parse$1 } from "smol-toml";
|
|
9
11
|
import { parseDocument } from "yaml";
|
|
10
|
-
import
|
|
12
|
+
import pc from "picocolors";
|
|
11
13
|
//#region src/shared/errors.ts
|
|
12
14
|
var SiroError = class extends Error {
|
|
13
15
|
exitCode;
|
|
@@ -46,15 +48,22 @@ const wrapCodecError = (filePath, fn) => {
|
|
|
46
48
|
};
|
|
47
49
|
//#endregion
|
|
48
50
|
//#region src/shared/paths.ts
|
|
51
|
+
const isRelPath = (value) => typeof value === "string" && value.length > 0 && !value.includes("\0") && !/^[\\/]/u.test(value) && !/^[a-z]:/iu.test(value) && !value.split(/[\\/]/u).includes("..");
|
|
52
|
+
const asRelPath = (value) => {
|
|
53
|
+
if (!isRelPath(value)) throw new TypeError("Expected a repository-relative path without parent traversal.");
|
|
54
|
+
return value;
|
|
55
|
+
};
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/adapters/node-paths.ts
|
|
49
58
|
const isAbsPath = (value) => typeof value === "string" && !value.includes("\0") && path.isAbsolute(value);
|
|
50
|
-
const isRelPath = (value) => typeof value === "string" && value.length > 0 && !value.includes("\0") && !path.posix.isAbsolute(value) && !path.win32.isAbsolute(value) && !/^[a-z]:/iu.test(value) && !value.split(/[\\/]/u).includes("..");
|
|
51
59
|
const asAbsPath = (value) => {
|
|
52
60
|
if (!isAbsPath(value)) throw new TypeError("Expected an absolute filesystem path.");
|
|
53
61
|
return value;
|
|
54
62
|
};
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
63
|
+
const nodePaths = {
|
|
64
|
+
isAbsolute: isAbsPath,
|
|
65
|
+
resolve: (root, relative) => asAbsPath(path.join(root, asRelPath(relative))),
|
|
66
|
+
normalizePattern: (pattern) => path.posix.normalize(pattern)
|
|
58
67
|
};
|
|
59
68
|
//#endregion
|
|
60
69
|
//#region src/adapters/node-errors.ts
|
|
@@ -62,10 +71,34 @@ const asRelPath = (value) => {
|
|
|
62
71
|
const isNodeError = (value) => value instanceof Error && "code" in value && typeof value.code === "string";
|
|
63
72
|
//#endregion
|
|
64
73
|
//#region src/adapters/node-file-system.ts
|
|
74
|
+
const assertRegularFile = (filePath) => {
|
|
75
|
+
if (!statSync(filePath).isFile()) throw new ConfigError(`${filePath}: expected a regular file.`);
|
|
76
|
+
};
|
|
65
77
|
const nodeFileSystem = {
|
|
78
|
+
readDirectories(directory) {
|
|
79
|
+
return readdirSync(directory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
80
|
+
},
|
|
81
|
+
resolveDirectory(parent, name) {
|
|
82
|
+
let target;
|
|
83
|
+
try {
|
|
84
|
+
target = lstatSync(path.join(parent, name), { bigint: true });
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (isNodeError(error) && error.code === "ENOENT") return void 0;
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
if (!target.isDirectory()) return void 0;
|
|
90
|
+
const names = readdirSync(parent, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
91
|
+
if (names.includes(name)) return name;
|
|
92
|
+
const matches = names.filter((candidate) => {
|
|
93
|
+
const entry = lstatSync(path.join(parent, candidate), { bigint: true });
|
|
94
|
+
return entry.isDirectory() && entry.dev === target.dev && entry.ino === target.ino;
|
|
95
|
+
});
|
|
96
|
+
if (target.ino === 0n || matches.length !== 1) throw new ConfigError(`${parent}: cannot identify the resolved workspace directory ${name}.`);
|
|
97
|
+
return matches[0];
|
|
98
|
+
},
|
|
66
99
|
exists(filePath) {
|
|
67
100
|
try {
|
|
68
|
-
|
|
101
|
+
assertRegularFile(filePath);
|
|
69
102
|
return true;
|
|
70
103
|
} catch (error) {
|
|
71
104
|
if (isNodeError(error) && error.code === "ENOENT") return false;
|
|
@@ -74,6 +107,7 @@ const nodeFileSystem = {
|
|
|
74
107
|
},
|
|
75
108
|
readText(filePath) {
|
|
76
109
|
try {
|
|
110
|
+
assertRegularFile(filePath);
|
|
77
111
|
return readFileSync(filePath, "utf8");
|
|
78
112
|
} catch (error) {
|
|
79
113
|
if (isNodeError(error) && error.code === "ENOENT") return;
|
|
@@ -81,16 +115,20 @@ const nodeFileSystem = {
|
|
|
81
115
|
}
|
|
82
116
|
}
|
|
83
117
|
};
|
|
84
|
-
const resolveIn = (root, relPath) =>
|
|
118
|
+
const resolveIn = (root, relPath) => nodePaths.resolve(root, relPath);
|
|
85
119
|
const assertDirectory = (root) => {
|
|
86
120
|
if (!statSync(root).isDirectory()) throw new UsageError("The lint target must be a directory.");
|
|
87
121
|
};
|
|
88
122
|
//#endregion
|
|
89
123
|
//#region package.json
|
|
90
|
-
var version$1 = "0.
|
|
124
|
+
var version$1 = "0.5.1";
|
|
125
|
+
var engines = { "node": "^22.18.0 || ^24.0.0" };
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/version.ts
|
|
128
|
+
const version = version$1;
|
|
129
|
+
const SUPPORTED_NODE_RANGE = engines.node;
|
|
91
130
|
//#endregion
|
|
92
|
-
//#region src/
|
|
93
|
-
const SUPPORTED_NODE_RANGE = { "node": "^22.18.0 || ^24.0.0" }.node;
|
|
131
|
+
//#region src/adapters/node-version.ts
|
|
94
132
|
const isSupportedNodeVersion = (version) => satisfies(version, SUPPORTED_NODE_RANGE);
|
|
95
133
|
//#endregion
|
|
96
134
|
//#region src/domain/entities/pms.ts
|
|
@@ -173,19 +211,26 @@ const toParsedConfig = (value) => {
|
|
|
173
211
|
throw new TypeError("Config root must be a mapping.");
|
|
174
212
|
};
|
|
175
213
|
//#endregion
|
|
176
|
-
//#region src/domain/entities/
|
|
177
|
-
const defineRule = (rule) => rule;
|
|
178
|
-
const isOptionalString = (value) => typeof value === "undefined" || typeof value === "string";
|
|
214
|
+
//#region src/domain/entities/config-file-ref.ts
|
|
179
215
|
const CONFIG_FILE_KINDS = new Set(CODEC_KINDS);
|
|
180
216
|
const isConfigFileRefShape = (value) => {
|
|
181
217
|
if (!isPlainRecord(value) || typeof value.kind !== "string" || !isRelPath(value.path)) return false;
|
|
182
218
|
return CONFIG_FILE_KINDS.has(value.kind);
|
|
183
219
|
};
|
|
220
|
+
//#endregion
|
|
221
|
+
//#region src/domain/entities/rule.ts
|
|
222
|
+
const defineRule = (rule) => rule;
|
|
223
|
+
const isOptionalString = (value) => typeof value === "undefined" || typeof value === "string";
|
|
184
224
|
const isConfigValueShape = (value) => typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value);
|
|
185
225
|
const isDenseStringArray = (value) => Array.isArray(value) && Array.from(value).every((item) => typeof item === "string");
|
|
186
226
|
const isCheckStatusShape = (value) => {
|
|
187
227
|
if (!isPlainRecord(value)) return false;
|
|
188
228
|
if (value.state === "ok" || value.state === "na") return true;
|
|
229
|
+
if (value.state === "violations") return Array.isArray(value.violations) && value.violations.length > 0 && Array.from(value.violations).every(isViolationStatusShape);
|
|
230
|
+
return isViolationStatusShape(value);
|
|
231
|
+
};
|
|
232
|
+
const isViolationStatusShape = (value) => {
|
|
233
|
+
if (!isPlainRecord(value)) return false;
|
|
189
234
|
return value.state === "violation" && typeof value.message === "string" && (value.file === void 0 || isRelPath(value.file)) && (value.expected === void 0 || isConfigValueShape(value.expected)) && (value.severity === void 0 || typeof value.severity === "string" && isSeverity(value.severity)) && !("manualSteps" in value) && !("fix" in value) && !("fixable" in value) && (value.remediation === void 0 || isRemediationShape(value.remediation));
|
|
190
235
|
};
|
|
191
236
|
const isSetKeyOperation = (value) => isPlainRecord(value) && value.op === "setKey" && isConfigFileRefShape(value.file) && isDenseStringArray(value.keyPath) && value.keyPath.length > 0 && isConfigValueShape(value.value);
|
|
@@ -207,12 +252,26 @@ const isRuleShape = (value) => {
|
|
|
207
252
|
return typeof value.id === "string" && typeof value.title === "string" && typeof value.description === "string" && typeof value.severity === "string" && isSeverity(value.severity) && isOptionalString(value.docs) && isProjectTypesShape(value.projectTypes) && isBindingsShape(value.bindings);
|
|
208
253
|
};
|
|
209
254
|
//#endregion
|
|
210
|
-
//#region src/
|
|
255
|
+
//#region src/domain/services/pm-versions.ts
|
|
256
|
+
/** Release history establishes stable versions, not ranges or prerelease builds. */
|
|
257
|
+
const isStableVersion = (value) => {
|
|
258
|
+
if (typeof value !== "string" || !/^\d/u.test(value) || value.trim() !== value) return false;
|
|
259
|
+
const version = parse(value);
|
|
260
|
+
return version !== null && version.prerelease.length === 0;
|
|
261
|
+
};
|
|
262
|
+
/** Keep name-only detection independent from whether the declaration pins a version. */
|
|
263
|
+
const declaredPMVersion = (declaration) => {
|
|
264
|
+
const [pm, version, ...extra] = declaration?.trim().split("@") ?? [];
|
|
265
|
+
return pm && isPM(pm) && isStableVersion(version) && extra.length === 0 ? { [pm]: version } : {};
|
|
266
|
+
};
|
|
267
|
+
//#endregion
|
|
268
|
+
//#region src/domain/services/parse-siro-config.ts
|
|
211
269
|
const RuleSettingSchema = vb.union([vb.picklist(SEVERITIES), vb.literal("off")]);
|
|
212
270
|
const ConfigSchema = vb.strictObject({
|
|
213
271
|
customRules: vb.optional(vb.array(vb.custom(isRuleShape, "must be a structurally valid rule"))),
|
|
214
272
|
pms: vb.optional(vb.pipe(vb.array(vb.picklist(PMS)), vb.minLength(1, "must not be empty (omit the key to auto-detect, or list at least one PM)"))),
|
|
215
273
|
projectType: vb.optional(vb.picklist(PROJECT_TYPES)),
|
|
274
|
+
pmVersions: vb.optional(vb.pipe(vb.custom(isPlainRecord, "must be an object of PM versions"), vb.check((value) => Object.keys(value).every(isPM), "unknown package manager in pmVersions"), vb.record(vb.picklist(PMS), vb.custom(isStableVersion, "must be an exact stable version such as 10.16.0")))),
|
|
216
275
|
reporters: vb.optional(vb.array(vb.custom(isReporterShape, "must be a { name, format } reporter"))),
|
|
217
276
|
rules: vb.optional(vb.pipe(vb.custom(isPlainRecord, "must be an object of rule settings"), vb.rawTransform(({ dataset, addIssue }) => {
|
|
218
277
|
const entries = [];
|
|
@@ -269,276 +328,210 @@ const loadConfig = async (cwd, nodeVersion = process.versions.node) => {
|
|
|
269
328
|
return parseConfig(mod !== null && typeof mod === "object" && "default" in mod ? mod.default : mod, name);
|
|
270
329
|
};
|
|
271
330
|
//#endregion
|
|
272
|
-
//#region src/domain/
|
|
273
|
-
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
331
|
+
//#region src/domain/entities/config-files.ts
|
|
332
|
+
/** Known configuration locations and their parsers. */
|
|
333
|
+
const CONFIG_FILES = {
|
|
334
|
+
aubeWorkspace: {
|
|
335
|
+
kind: "yaml",
|
|
336
|
+
path: asRelPath("aube-workspace.yaml")
|
|
337
|
+
},
|
|
338
|
+
bunfig: {
|
|
339
|
+
kind: "toml",
|
|
340
|
+
path: asRelPath("bunfig.toml")
|
|
341
|
+
},
|
|
342
|
+
denoJson: {
|
|
343
|
+
kind: "json",
|
|
344
|
+
path: asRelPath("deno.json")
|
|
345
|
+
},
|
|
346
|
+
npmrc: {
|
|
347
|
+
kind: "npmrc",
|
|
348
|
+
path: asRelPath(".npmrc")
|
|
349
|
+
},
|
|
350
|
+
packageJson: {
|
|
351
|
+
kind: "json",
|
|
352
|
+
path: asRelPath("package.json")
|
|
353
|
+
},
|
|
354
|
+
pnpmWorkspace: {
|
|
355
|
+
kind: "yaml",
|
|
356
|
+
path: asRelPath("pnpm-workspace.yaml")
|
|
357
|
+
},
|
|
358
|
+
yarnrc: {
|
|
359
|
+
kind: "yaml",
|
|
360
|
+
path: asRelPath(".yarnrc.yml")
|
|
287
361
|
}
|
|
288
|
-
return {
|
|
289
|
-
findings,
|
|
290
|
-
summary
|
|
291
|
-
};
|
|
292
|
-
};
|
|
293
|
-
/**
|
|
294
|
-
* Exit code for a lint run. Non-zero when any finding meets `threshold`
|
|
295
|
-
* (default: only `error` fails the run).
|
|
296
|
-
*/
|
|
297
|
-
const exitCodeForLint = (result, threshold = "error") => {
|
|
298
|
-
if (result.findings.some((finding) => meetsThreshold(finding.severity, threshold))) return EXIT_FAILURE;
|
|
299
|
-
return EXIT_SUCCESS;
|
|
300
362
|
};
|
|
301
363
|
//#endregion
|
|
302
|
-
//#region src/
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
const githubReporter = {
|
|
312
|
-
format(result, io) {
|
|
313
|
-
for (const finding of result.findings) {
|
|
314
|
-
let file = "";
|
|
315
|
-
if (finding.file) file = `file=${escapeProp(finding.file)},`;
|
|
316
|
-
let body = `[${finding.pm}] ${finding.message}`;
|
|
317
|
-
if (finding.docs) body += ` (${finding.docs})`;
|
|
318
|
-
io.stdout(`::${COMMAND[finding.severity]} ${file}title=${escapeProp(finding.ruleId)}::${escapeData(body)}`);
|
|
319
|
-
}
|
|
364
|
+
//#region src/domain/setting-availability.ts
|
|
365
|
+
const { npmrc: npmrc$7, packageJson: packageJson$2, pnpmWorkspace: pnpmWorkspace$11, yarnrc: yarnrc$10, bunfig: bunfig$6 } = CONFIG_FILES;
|
|
366
|
+
const settingAvailability = [
|
|
367
|
+
{
|
|
368
|
+
pm: "npm",
|
|
369
|
+
file: npmrc$7,
|
|
370
|
+
keyPath: ["provenance"],
|
|
371
|
+
since: "9.5.0",
|
|
372
|
+
source: "https://github.com/npm/cli/releases/tag/v9.5.0"
|
|
320
373
|
},
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
//#region src/adapters/reporters/json.ts
|
|
328
|
-
/**
|
|
329
|
-
* Versioned machine-readable output — the public contract consumed by
|
|
330
|
-
* external fixers (see docs/json-output.md). Bump `schemaVersion` on any
|
|
331
|
-
* breaking shape change and update that doc in the same commit.
|
|
332
|
-
*/
|
|
333
|
-
const SCHEMA_VERSION = 2;
|
|
334
|
-
const jsonReporter = {
|
|
335
|
-
format(result, io) {
|
|
336
|
-
io.stdout(JSON.stringify({
|
|
337
|
-
schemaVersion: SCHEMA_VERSION,
|
|
338
|
-
siroVersion: version,
|
|
339
|
-
findings: result.findings,
|
|
340
|
-
summary: result.summary
|
|
341
|
-
}, void 0, 2));
|
|
374
|
+
{
|
|
375
|
+
pm: "npm",
|
|
376
|
+
file: packageJson$2,
|
|
377
|
+
keyPath: ["publishConfig", "provenance"],
|
|
378
|
+
since: "9.5.0",
|
|
379
|
+
source: "https://github.com/npm/cli/releases/tag/v9.5.0"
|
|
342
380
|
},
|
|
343
|
-
|
|
344
|
-
|
|
381
|
+
{
|
|
382
|
+
pm: "npm",
|
|
383
|
+
file: npmrc$7,
|
|
384
|
+
keyPath: ["min-release-age"],
|
|
385
|
+
since: "11.10.0",
|
|
386
|
+
source: "https://github.com/npm/cli/releases/tag/v11.10.0"
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
pm: "pnpm",
|
|
390
|
+
file: pnpmWorkspace$11,
|
|
391
|
+
keyPath: ["strictDepBuilds"],
|
|
392
|
+
since: "10.6.0",
|
|
393
|
+
source: "https://github.com/pnpm/pnpm/releases/tag/v10.6.0"
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
pm: "pnpm",
|
|
397
|
+
file: pnpmWorkspace$11,
|
|
398
|
+
keyPath: ["dangerouslyAllowAllBuilds"],
|
|
399
|
+
since: "10.9.0",
|
|
400
|
+
source: "https://github.com/pnpm/pnpm/releases/tag/v10.9.0"
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
pm: "pnpm",
|
|
404
|
+
file: pnpmWorkspace$11,
|
|
405
|
+
keyPath: ["minimumReleaseAge"],
|
|
406
|
+
since: "10.16.0",
|
|
407
|
+
source: "https://github.com/pnpm/pnpm/releases/tag/v10.16.0"
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
pm: "pnpm",
|
|
411
|
+
file: pnpmWorkspace$11,
|
|
412
|
+
keyPath: ["minimumReleaseAgeExclude"],
|
|
413
|
+
since: "10.16.0",
|
|
414
|
+
source: "https://github.com/pnpm/pnpm/releases/tag/v10.16.0"
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
pm: "pnpm",
|
|
418
|
+
file: pnpmWorkspace$11,
|
|
419
|
+
keyPath: ["trustPolicy"],
|
|
420
|
+
since: "10.21.0",
|
|
421
|
+
source: "https://github.com/pnpm/pnpm/releases/tag/v10.21.0"
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
pm: "pnpm",
|
|
425
|
+
file: pnpmWorkspace$11,
|
|
426
|
+
keyPath: ["blockExoticSubdeps"],
|
|
427
|
+
since: "10.26.0",
|
|
428
|
+
source: "https://github.com/pnpm/pnpm/releases/tag/v10.26.0"
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
pm: "pnpm",
|
|
432
|
+
file: pnpmWorkspace$11,
|
|
433
|
+
keyPath: ["frozenStore"],
|
|
434
|
+
since: "11.7.0",
|
|
435
|
+
source: "https://github.com/pnpm/pnpm/releases/tag/v11.7.0"
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
pm: "yarn",
|
|
439
|
+
file: yarnrc$10,
|
|
440
|
+
keyPath: ["enableHardenedMode"],
|
|
441
|
+
since: "4.0.0",
|
|
442
|
+
source: "https://yarnpkg.com/blog/release/4.0"
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
pm: "yarn",
|
|
446
|
+
file: yarnrc$10,
|
|
447
|
+
keyPath: ["npmMinimalAgeGate"],
|
|
448
|
+
since: "4.10.0",
|
|
449
|
+
source: "https://github.com/yarnpkg/berry/releases/tag/@yarnpkg/cli/4.10.0"
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
pm: "yarn",
|
|
453
|
+
file: yarnrc$10,
|
|
454
|
+
keyPath: ["npmPreapprovedPackages"],
|
|
455
|
+
since: "4.10.0",
|
|
456
|
+
source: "https://github.com/yarnpkg/berry/releases/tag/@yarnpkg/cli/4.10.0"
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
pm: "bun",
|
|
460
|
+
file: bunfig$6,
|
|
461
|
+
keyPath: ["install", "minimumReleaseAge"],
|
|
462
|
+
since: "1.3.0",
|
|
463
|
+
source: "https://bun.com/blog/bun-v1.3#minimum-release-age"
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
pm: "bun",
|
|
467
|
+
file: bunfig$6,
|
|
468
|
+
keyPath: [
|
|
469
|
+
"install",
|
|
470
|
+
"security",
|
|
471
|
+
"scanner"
|
|
472
|
+
],
|
|
473
|
+
since: "1.2.21",
|
|
474
|
+
source: "https://bun.com/blog/bun-v1.2.21#security-scanner-api-for-bun-install"
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
pm: "deno",
|
|
478
|
+
file: npmrc$7,
|
|
479
|
+
keyPath: ["min-release-age"],
|
|
480
|
+
since: "2.8.1",
|
|
481
|
+
source: "https://github.com/denoland/deno/releases/tag/v2.8.1"
|
|
482
|
+
}
|
|
483
|
+
];
|
|
345
484
|
//#endregion
|
|
346
|
-
//#region src/
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
* only way NO_COLOR can truly win, as https://no-color.org/ requires.
|
|
360
|
-
*/
|
|
361
|
-
const colorSupportedNow = () => {
|
|
362
|
-
const { env } = process;
|
|
363
|
-
if (typeof env.NO_COLOR !== "undefined" && env.NO_COLOR !== "") return false;
|
|
364
|
-
if ("FORCE_COLOR" in env) return env.FORCE_COLOR !== "0" && env.FORCE_COLOR !== "";
|
|
365
|
-
return pc.isColorSupported;
|
|
366
|
-
};
|
|
367
|
-
const renderFinding = (finding, ctx) => {
|
|
368
|
-
let where = "";
|
|
369
|
-
if (finding.file) where = ctx.colors.dim(` (${finding.file})`);
|
|
370
|
-
ctx.io.stdout(`${ctx.tag[finding.severity](GLYPH[finding.severity])} [${finding.pm}] ${ctx.colors.bold(finding.ruleId)}${where}`);
|
|
371
|
-
ctx.io.stdout(` ${finding.message}`);
|
|
372
|
-
for (const step of finding.remediation?.kind === "manual" ? finding.remediation.steps : []) ctx.io.stdout(` ↳ ${step}`);
|
|
373
|
-
if (finding.docs) ctx.io.stdout(ctx.colors.dim(` → ${finding.docs}`));
|
|
374
|
-
};
|
|
375
|
-
const buildRenderCtx = (io) => {
|
|
376
|
-
const colors = pc.createColors(colorSupportedNow());
|
|
485
|
+
//#region src/domain/services/remediation-availability.ts
|
|
486
|
+
const sameSetting = (setting, target) => target.file.path === setting.file.path && target.file.kind === setting.file.kind && target.keyPath.length === setting.keyPath.length && target.keyPath.every((key, index) => key === setting.keyPath[index]);
|
|
487
|
+
/** Undefined means the target version or the setting's introduction is unknown. */
|
|
488
|
+
const settingSupportedByTarget = (pm, version, target) => {
|
|
489
|
+
if (version === void 0) return void 0;
|
|
490
|
+
const availability = settingAvailability.find((setting) => setting.pm === pm && sameSetting(setting, target));
|
|
491
|
+
return availability === void 0 ? void 0 : !lt(version, availability.since);
|
|
492
|
+
};
|
|
493
|
+
/** Keep a proposal atomic and make known version prerequisites explicit. */
|
|
494
|
+
const guardRemediationAvailability = (pm, version, remediation, targets = remediation?.kind === "automatic" ? remediation.operations : []) => {
|
|
495
|
+
if (version === void 0 || remediation === void 0) return remediation;
|
|
496
|
+
const unsupported = settingAvailability.filter((setting) => setting.pm === pm && lt(version, setting.since) && targets.some((target) => sameSetting(setting, target)));
|
|
497
|
+
if (unsupported.length === 0) return remediation;
|
|
377
498
|
return {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
tag: {
|
|
381
|
-
error: colors.red,
|
|
382
|
-
info: colors.cyan,
|
|
383
|
-
warn: colors.yellow
|
|
384
|
-
}
|
|
499
|
+
kind: "manual",
|
|
500
|
+
steps: [`Target ${pm} ${version} does not support this proposal: ${unsupported.map((setting) => `${setting.file.path}#${setting.keyPath.join(".")} requires ${pm} >=${setting.since}`).join("; ")}. Verify and upgrade the target before applying the steps below, or choose a supported security control. Removing a setting alone does not provide its protection.`, ...remediation.kind === "manual" ? remediation.steps : remediation.operations.map((operation) => `After upgrading, set ${operation.file.path}#${operation.keyPath.join(".")} to ${JSON.stringify(operation.value)}.`)]
|
|
385
501
|
};
|
|
386
502
|
};
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
503
|
+
//#endregion
|
|
504
|
+
//#region src/domain/rules/remediation.ts
|
|
505
|
+
/** Scalar writes must not discard an existing container or replace a non-object parent. */
|
|
506
|
+
const proposeChanges = (config, operations) => {
|
|
507
|
+
for (const operation of operations) {
|
|
508
|
+
let current = config;
|
|
509
|
+
for (const key of operation.keyPath) {
|
|
510
|
+
if (current === void 0) break;
|
|
511
|
+
if (!isPlainRecord(current)) return {
|
|
512
|
+
kind: "manual",
|
|
513
|
+
steps: [`Review ${operation.file.path}: ${operation.keyPath.join(".")} has a non-object parent. Restructure it before setting the value to ${JSON.stringify(operation.value)}.`]
|
|
514
|
+
};
|
|
515
|
+
current = Object.hasOwn(current, key) ? current[key] : void 0;
|
|
393
516
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
}
|
|
399
|
-
|
|
517
|
+
if (current !== null && typeof current === "object") return {
|
|
518
|
+
kind: "manual",
|
|
519
|
+
steps: [`Review ${operation.file.path}: ${operation.keyPath.join(".")} contains nested settings. Preserve the required entries when correcting this value.`]
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
return {
|
|
523
|
+
kind: "automatic",
|
|
524
|
+
operations
|
|
525
|
+
};
|
|
400
526
|
};
|
|
401
527
|
//#endregion
|
|
402
|
-
//#region src/
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
const JSON_REPORTER_NAME = jsonReporter.name;
|
|
410
|
-
/** Names of every built-in reporter, in stable display order. */
|
|
411
|
-
const BUILTIN_REPORTER_NAMES = BUILTINS.map((rep) => rep.name);
|
|
412
|
-
/** Later registrations replace earlier reporters with the same name. */
|
|
413
|
-
const createRegistry = (extras = []) => new Map([...BUILTINS, ...extras].map((reporter) => [reporter.name, reporter]));
|
|
414
|
-
//#endregion
|
|
415
|
-
//#region src/domain/schemas/package-json.ts
|
|
416
|
-
const PackageJsonSchema = vb.looseObject({
|
|
417
|
-
files: vb.optional(vb.array(vb.string())),
|
|
418
|
-
name: vb.optional(vb.string()),
|
|
419
|
-
packageManager: vb.optional(vb.string()),
|
|
420
|
-
private: vb.optional(vb.boolean()),
|
|
421
|
-
publishConfig: vb.optional(vb.looseObject({ access: vb.optional(vb.picklist([
|
|
422
|
-
"public",
|
|
423
|
-
"restricted",
|
|
424
|
-
"private"
|
|
425
|
-
])) })),
|
|
426
|
-
trustedDependencies: vb.optional(vb.array(vb.string()))
|
|
427
|
-
});
|
|
428
|
-
const parsePackageJson = (value) => {
|
|
429
|
-
if (!isPlainRecord(value)) throw new ConfigError("package.json: expected an object at the root.");
|
|
430
|
-
const result = vb.safeParse(PackageJsonSchema, value);
|
|
431
|
-
if (result.success) return result.output;
|
|
432
|
-
const issue = result.issues[0];
|
|
433
|
-
const field = vb.getDotPath(issue);
|
|
434
|
-
throw new ConfigError(`package.json: ${field ? `${field}: ` : ""}${issue.message}`);
|
|
435
|
-
};
|
|
436
|
-
//#endregion
|
|
437
|
-
//#region src/adapters/repo-context.ts
|
|
438
|
-
const tryParseJson = (text) => {
|
|
439
|
-
try {
|
|
440
|
-
return JSON.parse(text);
|
|
441
|
-
} catch (error) {
|
|
442
|
-
let errMsg = String(error);
|
|
443
|
-
if (error instanceof Error) errMsg = error.message;
|
|
444
|
-
throw new ConfigError(`package.json: invalid JSON — ${errMsg}`);
|
|
445
|
-
}
|
|
446
|
-
};
|
|
447
|
-
const readPackageJson = (raw) => {
|
|
448
|
-
return parsePackageJson(tryParseJson(raw.trim()));
|
|
449
|
-
};
|
|
450
|
-
const createRepoContext = (root, fs = nodeFileSystem, projectType) => {
|
|
451
|
-
if (fs === nodeFileSystem) assertDirectory(root);
|
|
452
|
-
const readText = (relPath) => fs.readText(resolveIn(root, relPath));
|
|
453
|
-
const exists = (relPath) => fs.exists(resolveIn(root, relPath));
|
|
454
|
-
const raw = readText(asRelPath("package.json"));
|
|
455
|
-
let packageJson = void 0;
|
|
456
|
-
if (typeof raw !== "undefined") packageJson = readPackageJson(raw);
|
|
457
|
-
return {
|
|
458
|
-
exists,
|
|
459
|
-
packageJson,
|
|
460
|
-
projectType,
|
|
461
|
-
readText,
|
|
462
|
-
root
|
|
463
|
-
};
|
|
464
|
-
};
|
|
465
|
-
//#endregion
|
|
466
|
-
//#region src/adapters/codecs/ini.ts
|
|
467
|
-
const INTEGER_PATTERN = /^-?\d+$/u;
|
|
468
|
-
const coerceBoolean = (str) => {
|
|
469
|
-
if (str === "true") return true;
|
|
470
|
-
if (str === "false") return false;
|
|
471
|
-
};
|
|
472
|
-
const coerceInteger = (str) => {
|
|
473
|
-
if (INTEGER_PATTERN.test(str)) return Number(str);
|
|
474
|
-
};
|
|
475
|
-
const coerce = (raw) => {
|
|
476
|
-
if (typeof raw !== "string") return raw;
|
|
477
|
-
return coerceBoolean(raw) ?? coerceInteger(raw) ?? raw;
|
|
478
|
-
};
|
|
479
|
-
//#endregion
|
|
480
|
-
//#region src/adapters/codecs/store.ts
|
|
481
|
-
const CODECS = {
|
|
482
|
-
json: { parse(text) {
|
|
483
|
-
const trimmed = text.trim();
|
|
484
|
-
return toParsedConfig(JSON.parse(trimmed));
|
|
485
|
-
} },
|
|
486
|
-
npmrc: { parse(text) {
|
|
487
|
-
const raw = toParsedConfig(ini.parse(text));
|
|
488
|
-
const config = {};
|
|
489
|
-
for (const [key, value] of Object.entries(raw)) if (Array.isArray(value)) config[key] = value.map((elem) => coerce(elem));
|
|
490
|
-
else if (typeof value === "object" && value !== null) config[key] = toParsedConfig(value);
|
|
491
|
-
else config[key] = coerce(value);
|
|
492
|
-
return config;
|
|
493
|
-
} },
|
|
494
|
-
toml: { parse(text) {
|
|
495
|
-
if (text.trim() === "") return {};
|
|
496
|
-
return toParsedConfig(parse(text.replace(/^\uFEFF/u, "")));
|
|
497
|
-
} },
|
|
498
|
-
yaml: { parse(text) {
|
|
499
|
-
if (text.trim() === "") return {};
|
|
500
|
-
const document = parseDocument(text);
|
|
501
|
-
if (document.errors.length > 0) throw document.errors[0];
|
|
502
|
-
if (document.contents === null) return {};
|
|
503
|
-
return toParsedConfig(document.toJS({ onAnchor: (value, count) => {
|
|
504
|
-
if (count > 1) JSON.stringify(value);
|
|
505
|
-
} }));
|
|
506
|
-
} }
|
|
507
|
-
};
|
|
508
|
-
/** Look up the codec for a parseable kind. Total — every CodecKind has one. */
|
|
509
|
-
const codecFor = (kind) => CODECS[kind];
|
|
510
|
-
//#endregion
|
|
511
|
-
//#region src/domain/rules/remediation.ts
|
|
512
|
-
/** Scalar writes must not discard an existing container or replace a non-object parent. */
|
|
513
|
-
const proposeChanges = (config, operations) => {
|
|
514
|
-
for (const operation of operations) {
|
|
515
|
-
let current = config;
|
|
516
|
-
for (const key of operation.keyPath) {
|
|
517
|
-
if (current === void 0) break;
|
|
518
|
-
if (!isPlainRecord(current)) return {
|
|
519
|
-
kind: "manual",
|
|
520
|
-
steps: [`Review ${operation.file.path}: ${operation.keyPath.join(".")} has a non-object parent. Restructure it before setting the value to ${JSON.stringify(operation.value)}.`]
|
|
521
|
-
};
|
|
522
|
-
current = Object.hasOwn(current, key) ? current[key] : void 0;
|
|
523
|
-
}
|
|
524
|
-
if (current !== null && typeof current === "object") return {
|
|
525
|
-
kind: "manual",
|
|
526
|
-
steps: [`Review ${operation.file.path}: ${operation.keyPath.join(".")} contains nested settings. Preserve the required entries when correcting this value.`]
|
|
527
|
-
};
|
|
528
|
-
}
|
|
529
|
-
return {
|
|
530
|
-
kind: "automatic",
|
|
531
|
-
operations
|
|
532
|
-
};
|
|
533
|
-
};
|
|
534
|
-
//#endregion
|
|
535
|
-
//#region src/domain/rules/builders/require-config-key.ts
|
|
536
|
-
const overrideBindings = (rule, overrides) => ({
|
|
537
|
-
...rule,
|
|
538
|
-
bindings: {
|
|
539
|
-
...rule.bindings,
|
|
540
|
-
...overrides
|
|
541
|
-
}
|
|
528
|
+
//#region src/domain/rules/builders/require-config-key.ts
|
|
529
|
+
const overrideBindings = (rule, overrides) => ({
|
|
530
|
+
...rule,
|
|
531
|
+
bindings: {
|
|
532
|
+
...rule.bindings,
|
|
533
|
+
...overrides
|
|
534
|
+
}
|
|
542
535
|
});
|
|
543
536
|
const accepts = (spec, actual) => spec.accept ? spec.accept(actual) : actual === spec.value;
|
|
544
537
|
const checkKeyValue = (spec, config) => {
|
|
@@ -561,10 +554,14 @@ const checkKeyValue = (spec, config) => {
|
|
|
561
554
|
}])
|
|
562
555
|
};
|
|
563
556
|
};
|
|
564
|
-
const buildBinding = (spec, applies) => ({
|
|
557
|
+
const buildBinding = (spec, pm, applies) => ({
|
|
565
558
|
check(ctx, config) {
|
|
566
559
|
if (typeof applies !== "undefined" && !applies(ctx)) return { state: "na" };
|
|
567
|
-
|
|
560
|
+
const status = checkKeyValue(spec, config);
|
|
561
|
+
return status.state === "violation" ? {
|
|
562
|
+
...status,
|
|
563
|
+
remediation: guardRemediationAvailability(pm, ctx.pmVersion, status.remediation, [spec])
|
|
564
|
+
} : status;
|
|
568
565
|
},
|
|
569
566
|
docs: spec.docs,
|
|
570
567
|
file: spec.file,
|
|
@@ -578,7 +575,7 @@ const requireConfigKey = (options) => {
|
|
|
578
575
|
const spec = options.bindings[pm];
|
|
579
576
|
if (typeof spec !== "undefined") {
|
|
580
577
|
if ("extraFix" in spec) throw new TypeError("extraFix is no longer supported; use a custom binding.");
|
|
581
|
-
bindings[pm] = buildBinding(spec, options.applies);
|
|
578
|
+
bindings[pm] = buildBinding(spec, pm, options.applies);
|
|
582
579
|
}
|
|
583
580
|
}
|
|
584
581
|
return {
|
|
@@ -605,39 +602,6 @@ const withAubeParanoid = (rule) => {
|
|
|
605
602
|
} });
|
|
606
603
|
};
|
|
607
604
|
//#endregion
|
|
608
|
-
//#region src/domain/entities/config-files.ts
|
|
609
|
-
/** Known configuration locations and their parsers. */
|
|
610
|
-
const CONFIG_FILES = {
|
|
611
|
-
aubeWorkspace: {
|
|
612
|
-
kind: "yaml",
|
|
613
|
-
path: asRelPath("aube-workspace.yaml")
|
|
614
|
-
},
|
|
615
|
-
bunfig: {
|
|
616
|
-
kind: "toml",
|
|
617
|
-
path: asRelPath("bunfig.toml")
|
|
618
|
-
},
|
|
619
|
-
denoJson: {
|
|
620
|
-
kind: "json",
|
|
621
|
-
path: asRelPath("deno.json")
|
|
622
|
-
},
|
|
623
|
-
npmrc: {
|
|
624
|
-
kind: "npmrc",
|
|
625
|
-
path: asRelPath(".npmrc")
|
|
626
|
-
},
|
|
627
|
-
packageJson: {
|
|
628
|
-
kind: "json",
|
|
629
|
-
path: asRelPath("package.json")
|
|
630
|
-
},
|
|
631
|
-
pnpmWorkspace: {
|
|
632
|
-
kind: "yaml",
|
|
633
|
-
path: asRelPath("pnpm-workspace.yaml")
|
|
634
|
-
},
|
|
635
|
-
yarnrc: {
|
|
636
|
-
kind: "yaml",
|
|
637
|
-
path: asRelPath(".yarnrc.yml")
|
|
638
|
-
}
|
|
639
|
-
};
|
|
640
|
-
//#endregion
|
|
641
605
|
//#region src/domain/rules/advisory-check.ts
|
|
642
606
|
const { aubeWorkspace: aubeWorkspace$8 } = CONFIG_FILES;
|
|
643
607
|
const advisoryCheck = withAubeParanoid(requireConfigKey({
|
|
@@ -746,7 +710,7 @@ const blockAutoInstall = requireConfigKey({
|
|
|
746
710
|
const { aubeWorkspace: aubeWorkspace$7, npmrc: npmrc$6, pnpmWorkspace: pnpmWorkspace$10 } = CONFIG_FILES;
|
|
747
711
|
const isRootOrNone = (value) => value === "root" || value === "none";
|
|
748
712
|
const npmMessage = "Set `allow-git=none` and `allow-remote=none` in .npmrc to block git/tarball URLs; use `root` only when direct URL dependencies are required.";
|
|
749
|
-
const npmBinding$
|
|
713
|
+
const npmBinding$1 = {
|
|
750
714
|
check(_ctx, config) {
|
|
751
715
|
const git = getByPath(config, ["allow-git"]);
|
|
752
716
|
const remote = getByPath(config, ["allow-remote"]);
|
|
@@ -802,13 +766,13 @@ const blockExoticSubdeps = overrideBindings(requireConfigKey({
|
|
|
802
766
|
id: "block-exotic-subdeps",
|
|
803
767
|
severity: "warn",
|
|
804
768
|
title: "Block exotic subdependencies"
|
|
805
|
-
}), { npm: npmBinding$
|
|
769
|
+
}), { npm: npmBinding$1 });
|
|
806
770
|
//#endregion
|
|
807
771
|
//#region src/domain/rules/bun-security-scanner.ts
|
|
808
772
|
const { bunfig: bunfig$4 } = CONFIG_FILES;
|
|
809
773
|
const bunSecurityScanner = defineRule({
|
|
810
774
|
bindings: { bun: {
|
|
811
|
-
check(
|
|
775
|
+
check(ctx, config) {
|
|
812
776
|
const scanner = getByPath(config, [
|
|
813
777
|
"install",
|
|
814
778
|
"security",
|
|
@@ -816,10 +780,17 @@ const bunSecurityScanner = defineRule({
|
|
|
816
780
|
]);
|
|
817
781
|
if (isNonBlankString(scanner)) return { state: "ok" };
|
|
818
782
|
return {
|
|
819
|
-
remediation: {
|
|
783
|
+
remediation: guardRemediationAvailability("bun", ctx.pmVersion, {
|
|
820
784
|
kind: "manual",
|
|
821
785
|
steps: ["Add `[install.security] scanner = \"@socketsecurity/bun-security-scanner\"` (or another bun-compatible scanner) to bunfig.toml."]
|
|
822
|
-
},
|
|
786
|
+
}, [{
|
|
787
|
+
file: bunfig$4,
|
|
788
|
+
keyPath: [
|
|
789
|
+
"install",
|
|
790
|
+
"security",
|
|
791
|
+
"scanner"
|
|
792
|
+
]
|
|
793
|
+
}]),
|
|
823
794
|
actual: scanner,
|
|
824
795
|
message: "Configure `[install.security] scanner = \"...\"` in bunfig.toml (e.g. `@socketsecurity/bun-security-scanner`) to scan new packages on install.",
|
|
825
796
|
state: "violation"
|
|
@@ -827,7 +798,7 @@ const bunSecurityScanner = defineRule({
|
|
|
827
798
|
},
|
|
828
799
|
docs: "https://bun.com/docs/pm/security-scanner-api",
|
|
829
800
|
file: bunfig$4,
|
|
830
|
-
versionNote: { configAvailableSince: "bun 1.
|
|
801
|
+
versionNote: { configAvailableSince: "bun 1.2.21" }
|
|
831
802
|
} },
|
|
832
803
|
description: "Bun supports a Security Scanner API that intercepts new packages at install time (e.g. Socket Firewall).",
|
|
833
804
|
docs: "https://github.com/bodadotsh/npm-security-best-practices#preinstall-scanners",
|
|
@@ -927,7 +898,7 @@ const lockfileBinding = (pm) => {
|
|
|
927
898
|
versionNote: LOCKFILE_VERSION_NOTES[pm]
|
|
928
899
|
};
|
|
929
900
|
};
|
|
930
|
-
const denoBinding$
|
|
901
|
+
const denoBinding$1 = {
|
|
931
902
|
file: CONFIG_FILES.denoJson,
|
|
932
903
|
docs: LOCKFILE_DOCS.deno,
|
|
933
904
|
versionNote: LOCKFILE_VERSION_NOTES.deno,
|
|
@@ -958,7 +929,7 @@ const commitLockfile = defineRule({
|
|
|
958
929
|
bindings: {
|
|
959
930
|
aube: lockfileBinding("aube"),
|
|
960
931
|
bun: lockfileBinding("bun"),
|
|
961
|
-
deno: denoBinding$
|
|
932
|
+
deno: denoBinding$1,
|
|
962
933
|
npm: lockfileBinding("npm"),
|
|
963
934
|
pnpm: lockfileBinding("pnpm"),
|
|
964
935
|
yarn: lockfileBinding("yarn")
|
|
@@ -1008,31 +979,74 @@ const dependencyOverrides = defineRule({
|
|
|
1008
979
|
//#region src/domain/rules/disable-lifecycle-scripts.ts
|
|
1009
980
|
const { npmrc: npmrc$5, pnpmWorkspace: pnpmWorkspace$8, yarnrc: yarnrc$6, aubeWorkspace: aubeWorkspace$5, bunfig: bunfig$3 } = CONFIG_FILES;
|
|
1010
981
|
const pnpmBinding$4 = {
|
|
1011
|
-
check(
|
|
982
|
+
check(ctx, config) {
|
|
1012
983
|
if (getByPath(config, ["ignoreScripts"]) === true) return { state: "ok" };
|
|
1013
984
|
const bypass = getByPath(config, ["dangerouslyAllowAllBuilds"]);
|
|
1014
|
-
if (bypass === true) return {
|
|
1015
|
-
actual: bypass,
|
|
1016
|
-
expected: false,
|
|
1017
|
-
remediation: {
|
|
1018
|
-
kind: "manual",
|
|
1019
|
-
steps: ["Remove `dangerouslyAllowAllBuilds: true` from pnpm-workspace.yaml (or set it to `false`). Setting `strictDepBuilds: true` alone has no effect while the bypass remains."]
|
|
1020
|
-
},
|
|
1021
|
-
message: "`dangerouslyAllowAllBuilds: true` in pnpm-workspace.yaml bypasses strictDepBuilds — remove it (or set it to false) to restore lifecycle-script gating.",
|
|
1022
|
-
state: "violation"
|
|
1023
|
-
};
|
|
1024
985
|
const strict = getByPath(config, ["strictDepBuilds"]);
|
|
986
|
+
if (bypass === true) {
|
|
987
|
+
const bypassTarget = {
|
|
988
|
+
file: pnpmWorkspace$8,
|
|
989
|
+
keyPath: ["dangerouslyAllowAllBuilds"]
|
|
990
|
+
};
|
|
991
|
+
if (settingSupportedByTarget("pnpm", ctx.pmVersion, bypassTarget) === false) {
|
|
992
|
+
const proposal = {
|
|
993
|
+
kind: "manual",
|
|
994
|
+
steps: ["Remove `dangerouslyAllowAllBuilds: true` before upgrading to pnpm 10.9.0 or newer; the declared target ignores it, but a later version would activate the bypass.", ...strict === true ? [] : ["Set `strictDepBuilds: true` in pnpm-workspace.yaml to enable lifecycle-script gating for the declared target."]]
|
|
995
|
+
};
|
|
996
|
+
return {
|
|
997
|
+
state: "violation",
|
|
998
|
+
actual: bypass,
|
|
999
|
+
expected: false,
|
|
1000
|
+
message: "The declared target ignores this future-version bypass. Removing the ignored bypass alone does not provide protection against lifecycle scripts, and leaving it configured would disable gating after an upgrade.",
|
|
1001
|
+
remediation: strict === true ? proposal : guardRemediationAvailability("pnpm", ctx.pmVersion, proposal, [{
|
|
1002
|
+
file: pnpmWorkspace$8,
|
|
1003
|
+
keyPath: ["strictDepBuilds"]
|
|
1004
|
+
}])
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
const proposal = {
|
|
1008
|
+
kind: "manual",
|
|
1009
|
+
steps: ["After upgrading, remove `dangerouslyAllowAllBuilds: true` (or set it to false) and configure lifecycle-script gating."]
|
|
1010
|
+
};
|
|
1011
|
+
const guarded = guardRemediationAvailability("pnpm", ctx.pmVersion, proposal, [{
|
|
1012
|
+
file: pnpmWorkspace$8,
|
|
1013
|
+
keyPath: ["strictDepBuilds"]
|
|
1014
|
+
}, {
|
|
1015
|
+
file: pnpmWorkspace$8,
|
|
1016
|
+
keyPath: ["dangerouslyAllowAllBuilds"]
|
|
1017
|
+
}]);
|
|
1018
|
+
if (guarded !== proposal) return {
|
|
1019
|
+
state: "violation",
|
|
1020
|
+
actual: bypass,
|
|
1021
|
+
expected: false,
|
|
1022
|
+
message: "The target cannot use the proposed YAML lifecycle-script controls. Removing an unsupported bypass alone does not provide protection.",
|
|
1023
|
+
remediation: guarded
|
|
1024
|
+
};
|
|
1025
|
+
return {
|
|
1026
|
+
actual: bypass,
|
|
1027
|
+
expected: false,
|
|
1028
|
+
remediation: {
|
|
1029
|
+
kind: "manual",
|
|
1030
|
+
steps: ["Remove `dangerouslyAllowAllBuilds: true` from pnpm-workspace.yaml (or set it to `false`). Setting `strictDepBuilds: true` alone has no effect while the bypass remains."]
|
|
1031
|
+
},
|
|
1032
|
+
message: "`dangerouslyAllowAllBuilds: true` in pnpm-workspace.yaml bypasses strictDepBuilds — remove it (or set it to false) to restore lifecycle-script gating.",
|
|
1033
|
+
state: "violation"
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1025
1036
|
if (strict === true) return { state: "ok" };
|
|
1026
1037
|
return {
|
|
1027
1038
|
state: "violation",
|
|
1028
1039
|
actual: strict,
|
|
1029
1040
|
expected: true,
|
|
1030
1041
|
message: strict === void 0 ? "Set `strictDepBuilds: true` in pnpm-workspace.yaml to pin lifecycle-script gating across versions." : "Set `strictDepBuilds: true` in pnpm-workspace.yaml to block silent skips of un-approved dep builds.",
|
|
1031
|
-
remediation: proposeChanges(config, [{
|
|
1042
|
+
remediation: guardRemediationAvailability("pnpm", ctx.pmVersion, proposeChanges(config, [{
|
|
1032
1043
|
file: pnpmWorkspace$8,
|
|
1033
1044
|
keyPath: ["strictDepBuilds"],
|
|
1034
1045
|
op: "setKey",
|
|
1035
1046
|
value: true
|
|
1047
|
+
}]), [{
|
|
1048
|
+
file: pnpmWorkspace$8,
|
|
1049
|
+
keyPath: ["strictDepBuilds"]
|
|
1036
1050
|
}])
|
|
1037
1051
|
};
|
|
1038
1052
|
},
|
|
@@ -1040,6 +1054,7 @@ const pnpmBinding$4 = {
|
|
|
1040
1054
|
file: pnpmWorkspace$8,
|
|
1041
1055
|
versionNote: {
|
|
1042
1056
|
configAvailableSince: "pnpm 10.3.0",
|
|
1057
|
+
note: "pnpm-workspace.yaml settings require pnpm 10.6.0",
|
|
1043
1058
|
defaultSafeSince: "pnpm 11.0.0"
|
|
1044
1059
|
}
|
|
1045
1060
|
};
|
|
@@ -1049,31 +1064,37 @@ const aubeBinding$2 = {
|
|
|
1049
1064
|
const jail = getByPath(config, ["jailBuilds"]);
|
|
1050
1065
|
const strict = getByPath(npmConfig, ["strictDepBuilds"]);
|
|
1051
1066
|
if (jail === true && strict === true) return { state: "ok" };
|
|
1052
|
-
const
|
|
1053
|
-
|
|
1054
|
-
keyPath: ["jailBuilds"],
|
|
1055
|
-
op: "setKey",
|
|
1056
|
-
value: true
|
|
1057
|
-
}]);
|
|
1058
|
-
const strictRemedy = proposeChanges(npmConfig, [{
|
|
1059
|
-
file: npmrc$5,
|
|
1060
|
-
keyPath: ["strictDepBuilds"],
|
|
1061
|
-
op: "setKey",
|
|
1062
|
-
value: true
|
|
1063
|
-
}]);
|
|
1064
|
-
return {
|
|
1067
|
+
const violations = [];
|
|
1068
|
+
if (jail !== true) violations.push({
|
|
1065
1069
|
state: "violation",
|
|
1066
|
-
file:
|
|
1067
|
-
actual: jail
|
|
1070
|
+
file: aubeWorkspace$5.path,
|
|
1071
|
+
actual: jail,
|
|
1068
1072
|
expected: true,
|
|
1069
|
-
message: "Set `jailBuilds: true` in aube-workspace.yaml
|
|
1070
|
-
remediation:
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1073
|
+
message: "Set `jailBuilds: true` in aube-workspace.yaml to sandbox approved builds.",
|
|
1074
|
+
remediation: proposeChanges(config, [{
|
|
1075
|
+
file: aubeWorkspace$5,
|
|
1076
|
+
keyPath: ["jailBuilds"],
|
|
1077
|
+
op: "setKey",
|
|
1078
|
+
value: true
|
|
1079
|
+
}])
|
|
1080
|
+
});
|
|
1081
|
+
if (strict !== true) violations.push({
|
|
1082
|
+
state: "violation",
|
|
1083
|
+
file: npmrc$5.path,
|
|
1084
|
+
actual: strict,
|
|
1085
|
+
expected: true,
|
|
1086
|
+
message: "Set `strictDepBuilds=true` in .npmrc to reject unreviewed lifecycle scripts.",
|
|
1087
|
+
remediation: proposeChanges(npmConfig, [{
|
|
1088
|
+
file: npmrc$5,
|
|
1089
|
+
keyPath: ["strictDepBuilds"],
|
|
1090
|
+
op: "setKey",
|
|
1091
|
+
value: true
|
|
1092
|
+
}])
|
|
1093
|
+
});
|
|
1094
|
+
const [first, ...rest] = violations;
|
|
1095
|
+
return !first ? { state: "ok" } : rest.length === 0 ? first : {
|
|
1096
|
+
state: "violations",
|
|
1097
|
+
violations: [first, ...rest]
|
|
1077
1098
|
};
|
|
1078
1099
|
},
|
|
1079
1100
|
docs: "https://aube.jdx.dev/security.html",
|
|
@@ -1196,7 +1217,7 @@ const resolvePackageJsonProjectType = (ctx) => {
|
|
|
1196
1217
|
const inferredPackage = pkg?.private !== true && isPublishableName(pkg?.name);
|
|
1197
1218
|
return resolveProjectType(ctx.projectType, inferredPackage);
|
|
1198
1219
|
};
|
|
1199
|
-
const resolveDenoProjectType = (ctx, denoConfig) => resolveProjectType(ctx.projectType, isPublishableName(getByPath(denoConfig, ["name"])));
|
|
1220
|
+
const resolveDenoProjectType = (ctx, denoConfig) => resolveProjectType(ctx.projectType, getByPath(denoConfig, ["publish"]) !== false && isPublishableName(getByPath(denoConfig, ["name"])));
|
|
1200
1221
|
//#endregion
|
|
1201
1222
|
//#region src/domain/rules/publishable.ts
|
|
1202
1223
|
const isPublishable = (ctx) => resolvePackageJsonProjectType(ctx) === "package";
|
|
@@ -1319,14 +1340,17 @@ const frozenLockfile = overrideBindings(requireConfigKey({
|
|
|
1319
1340
|
const { pnpmWorkspace: pnpmWorkspace$6 } = CONFIG_FILES;
|
|
1320
1341
|
const frozenStore = defineRule({
|
|
1321
1342
|
bindings: { pnpm: {
|
|
1322
|
-
check(
|
|
1343
|
+
check(ctx, config) {
|
|
1323
1344
|
const value = getByPath(config, ["frozenStore"]);
|
|
1324
1345
|
if (value === true) return { state: "ok" };
|
|
1325
1346
|
return {
|
|
1326
|
-
remediation: {
|
|
1347
|
+
remediation: guardRemediationAvailability("pnpm", ctx.pmVersion, {
|
|
1327
1348
|
kind: "manual",
|
|
1328
1349
|
steps: ["Populate the store before enabling `frozenStore: true`. Use it for read-only deployments; it is incompatible with --force and a configured pnpr server."]
|
|
1329
|
-
},
|
|
1350
|
+
}, [{
|
|
1351
|
+
file: pnpmWorkspace$6,
|
|
1352
|
+
keyPath: ["frozenStore"]
|
|
1353
|
+
}]),
|
|
1330
1354
|
actual: value,
|
|
1331
1355
|
message: "For a pre-populated read-only store, consider `frozenStore` in pnpm-workspace.yaml. Normal installs need a writable store.",
|
|
1332
1356
|
state: "violation"
|
|
@@ -1365,19 +1389,9 @@ const hardenedMode = requireConfigKey({
|
|
|
1365
1389
|
title: "Enable hardened mode (yarn)"
|
|
1366
1390
|
});
|
|
1367
1391
|
//#endregion
|
|
1368
|
-
//#region src/domain/
|
|
1369
|
-
const HOURS_PER_DAY = 24;
|
|
1370
|
-
const MINUTES_PER_HOUR = 60;
|
|
1371
|
-
const SECONDS_PER_MINUTE = 60;
|
|
1372
|
-
const MINUTES_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR;
|
|
1373
|
-
const SECONDS_PER_DAY = MINUTES_PER_DAY * SECONDS_PER_MINUTE;
|
|
1374
|
-
const RECOMMENDED_RELEASE_AGE_DAYS = 3;
|
|
1375
|
-
const RECOMMENDED_RELEASE_AGE_MINUTES = RECOMMENDED_RELEASE_AGE_DAYS * MINUTES_PER_DAY;
|
|
1376
|
-
const RECOMMENDED_RELEASE_AGE_SECONDS = RECOMMENDED_RELEASE_AGE_DAYS * SECONDS_PER_DAY;
|
|
1377
|
-
const DOCUMENTED_DEFAULT_MINUTES = MINUTES_PER_DAY;
|
|
1378
|
-
const { npmrc: npmrc$3, pnpmWorkspace: pnpmWorkspace$5, yarnrc: yarnrc$2, bunfig: bunfig$1, denoJson: denoJson$1, aubeWorkspace: aubeWorkspace$4 } = CONFIG_FILES;
|
|
1392
|
+
//#region src/domain/services/deno-release-age.ts
|
|
1379
1393
|
const DENO_DURATION = /^\+?P(?:\+?\d+[Ww]|(?:\d+[Dd])*(?:T(?:\d+[HhMm]|\d+(?:\.\d+)?[Ss])+)?)$/u;
|
|
1380
|
-
const DENO_MIN_TIMESTAMP = Date.
|
|
1394
|
+
const DENO_MIN_TIMESTAMP = Date.UTC(-262143, 0, 1);
|
|
1381
1395
|
const DENO_UNIT_SECONDS = {
|
|
1382
1396
|
w: 604800,
|
|
1383
1397
|
d: 86400,
|
|
@@ -1388,35 +1402,59 @@ const DENO_UNIT_SECONDS = {
|
|
|
1388
1402
|
const DENO_DATE = /^\d{4}-\d{2}-\d{2}$/u;
|
|
1389
1403
|
const DENO_TIMESTAMP = /^\d{4}-\d{2}-\d{2}[Tt ](?:[01]\d|2[0-3]):[0-5]\d:(?:[0-5]\d|60)(?:\.\d+)?(?:[Zz]|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/u;
|
|
1390
1404
|
const DENO_OFFSET_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T(?:[01]\d|2[0-3]):[0-5]\d(?::(?:[0-5]\d|60))?[+-](?:[01]\d|2[0-3]):?[0-5]\d$/u;
|
|
1391
|
-
const
|
|
1392
|
-
const
|
|
1393
|
-
const isPositiveDenoSeconds = (seconds) => seconds > 0 && Number.isFinite(seconds) &&
|
|
1394
|
-
const
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
let seconds = 0;
|
|
1400
|
-
for (const [, integer, fraction, unit] of value.matchAll(/(\d+)(?:\.(\d+))?([WDHMS])/giu)) {
|
|
1401
|
-
const amount = Number(integer) + Number(`0.${(fraction ?? "").slice(0, 9) || "0"}`);
|
|
1402
|
-
seconds += amount * (DENO_UNIT_SECONDS[unit?.toLowerCase() ?? ""] ?? 0);
|
|
1403
|
-
}
|
|
1404
|
-
return isPositiveDenoSeconds(seconds);
|
|
1405
|
+
const DENO_DURATION_TOKEN = /(\d+)(?:\.(\d+))?([WDHMS])/giu;
|
|
1406
|
+
const LEAP_SECOND = /:60(?=\.|Z|z|[+-])/u;
|
|
1407
|
+
const isPositiveDenoSeconds = (seconds, now) => seconds > 0 && Number.isFinite(seconds) && now - seconds * 1e3 >= DENO_MIN_TIMESTAMP;
|
|
1408
|
+
const durationSeconds = (value) => {
|
|
1409
|
+
let seconds = 0;
|
|
1410
|
+
for (const [, integer, fraction, unit] of value.matchAll(DENO_DURATION_TOKEN)) {
|
|
1411
|
+
const amount = Number(integer) + Number(`0.${(fraction ?? "").slice(0, 9) || "0"}`);
|
|
1412
|
+
seconds += amount * (DENO_UNIT_SECONDS[unit?.toLowerCase() ?? ""] ?? 0);
|
|
1405
1413
|
}
|
|
1414
|
+
return seconds;
|
|
1415
|
+
};
|
|
1416
|
+
/** Match Deno's minimumDependencyAge value grammar and active-cutoff semantics. */
|
|
1417
|
+
const isActiveDenoReleaseAge = (value, now, parse) => {
|
|
1418
|
+
if (typeof value === "number") return Number.isSafeInteger(value) && isPositiveDenoSeconds(value * 60, now);
|
|
1419
|
+
if (typeof value !== "string") return false;
|
|
1420
|
+
if (/^\d+$/u.test(value)) return Number.isSafeInteger(Number(value)) && isPositiveDenoSeconds(Number(value) * 60, now);
|
|
1421
|
+
if (DENO_DURATION.test(value)) return isPositiveDenoSeconds(durationSeconds(value), now);
|
|
1406
1422
|
if (!DENO_DATE.test(value) && !DENO_TIMESTAMP.test(value) && !DENO_OFFSET_TIMESTAMP.test(value)) return false;
|
|
1407
1423
|
const date = value.slice(0, 10);
|
|
1408
1424
|
const midnight = /* @__PURE__ */ new Date(`${date}T00:00:00Z`);
|
|
1409
|
-
const leapSecond =
|
|
1410
|
-
const timestamp =
|
|
1411
|
-
return !Number.isNaN(midnight.valueOf()) && midnight.toISOString().slice(0, 10) === date && timestamp <
|
|
1425
|
+
const leapSecond = LEAP_SECOND.test(value);
|
|
1426
|
+
const timestamp = parse(value.replace(LEAP_SECOND, ":59")) + (leapSecond ? 1e3 : 0);
|
|
1427
|
+
return !Number.isNaN(midnight.valueOf()) && midnight.toISOString().slice(0, 10) === date && timestamp < now;
|
|
1412
1428
|
};
|
|
1413
|
-
|
|
1414
|
-
|
|
1429
|
+
//#endregion
|
|
1430
|
+
//#region src/domain/rules/minimum-release-age.ts
|
|
1431
|
+
const HOURS_PER_DAY = 24;
|
|
1432
|
+
const MINUTES_PER_HOUR = 60;
|
|
1433
|
+
const SECONDS_PER_MINUTE = 60;
|
|
1434
|
+
const MINUTES_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR;
|
|
1435
|
+
const SECONDS_PER_DAY = MINUTES_PER_DAY * SECONDS_PER_MINUTE;
|
|
1436
|
+
const RECOMMENDED_RELEASE_AGE_DAYS = 3;
|
|
1437
|
+
const RECOMMENDED_RELEASE_AGE_MINUTES = RECOMMENDED_RELEASE_AGE_DAYS * MINUTES_PER_DAY;
|
|
1438
|
+
const RECOMMENDED_RELEASE_AGE_SECONDS = RECOMMENDED_RELEASE_AGE_DAYS * SECONDS_PER_DAY;
|
|
1439
|
+
const DOCUMENTED_DEFAULT_MINUTES = MINUTES_PER_DAY;
|
|
1440
|
+
const { npmrc: npmrc$3, pnpmWorkspace: pnpmWorkspace$5, yarnrc: yarnrc$2, bunfig: bunfig$1, denoJson: denoJson$1, aubeWorkspace: aubeWorkspace$4 } = CONFIG_FILES;
|
|
1441
|
+
const isPositiveNumber = (value) => typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
1442
|
+
const isPositiveYarnDuration = (value) => isPositiveNumber(value) || typeof value === "string" && /^\d*\.?\d+(?:ms|s|m|h|d|w)?$/u.test(value) && Number.isFinite(Number.parseFloat(value)) && Number.parseFloat(value) > 0;
|
|
1443
|
+
const isNonDisabledDenoReleaseAge = (value, now, parse) => {
|
|
1444
|
+
if (!isPlainRecord(value)) return isActiveDenoReleaseAge(value, now, parse);
|
|
1445
|
+
if (Object.keys(value).some((key) => key !== "age" && key !== "exclude")) return false;
|
|
1446
|
+
if (value.exclude !== void 0 && !isStringList(value.exclude)) return false;
|
|
1447
|
+
return value.age == null || isActiveDenoReleaseAge(value.age, now, parse);
|
|
1448
|
+
};
|
|
1449
|
+
const denoAgeUsesFallback = (value) => {
|
|
1450
|
+
if (value === void 0 || value === null) return true;
|
|
1451
|
+
if (!isPlainRecord(value)) return false;
|
|
1415
1452
|
if (Object.keys(value).some((key) => key !== "age" && key !== "exclude")) return false;
|
|
1416
1453
|
if (value.exclude !== void 0 && !isStringList(value.exclude)) return false;
|
|
1417
|
-
return value.age == null
|
|
1454
|
+
return value.age == null;
|
|
1418
1455
|
};
|
|
1419
|
-
const
|
|
1456
|
+
const isPositiveDenoNpmrcDays = (value) => typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
1457
|
+
const baseRule$1 = requireConfigKey({
|
|
1420
1458
|
bindings: {
|
|
1421
1459
|
aube: {
|
|
1422
1460
|
accept: isPositiveNumber,
|
|
@@ -1468,75 +1506,105 @@ const minimumReleaseAge = overrideBindings(requireConfigKey({
|
|
|
1468
1506
|
id: "minimum-release-age",
|
|
1469
1507
|
severity: "warn",
|
|
1470
1508
|
title: "Set a minimum release age"
|
|
1471
|
-
})
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
return {
|
|
1482
|
-
state: "violation",
|
|
1483
|
-
actual,
|
|
1484
|
-
expected: "a date in the past",
|
|
1485
|
-
message: "Use a valid past before cutoff, or remove before and set min-release-age.",
|
|
1486
|
-
remediation: {
|
|
1487
|
-
kind: "manual",
|
|
1488
|
-
steps: [`In .npmrc, set before to a valid past date, or remove before and set min-release-age to ~${RECOMMENDED_RELEASE_AGE_DAYS} days. A future or disabled before overrides min-release-age in this file.`]
|
|
1489
|
-
}
|
|
1490
|
-
};
|
|
1491
|
-
}
|
|
1492
|
-
const actual = getByPath(config, ["min-release-age"]);
|
|
1493
|
-
const age = typeof actual === "number" || typeof actual === "string" ? Number(actual) : NaN;
|
|
1494
|
-
const cutoff = (/* @__PURE__ */ new Date(now - SECONDS_PER_DAY * 1e3 * age)).valueOf();
|
|
1495
|
-
if (age > 0 && Number.isFinite(cutoff) && cutoff < now) return { state: "ok" };
|
|
1509
|
+
});
|
|
1510
|
+
const createNpmBinding = (time) => ({
|
|
1511
|
+
file: npmrc$3,
|
|
1512
|
+
docs: "https://docs.npmjs.com/cli/v12/using-npm/config#min-release-age",
|
|
1513
|
+
versionNote: { note: "min-release-age available since npm 11.10.0" },
|
|
1514
|
+
check(ctx, config) {
|
|
1515
|
+
const now = time.now();
|
|
1516
|
+
if (Object.hasOwn(config, "before")) {
|
|
1517
|
+
const actual = config.before;
|
|
1518
|
+
if ((typeof actual === "string" || typeof actual === "number") && time.parse(String(actual)) < now) return { state: "ok" };
|
|
1496
1519
|
return {
|
|
1497
1520
|
state: "violation",
|
|
1498
1521
|
actual,
|
|
1499
|
-
expected:
|
|
1500
|
-
message:
|
|
1501
|
-
remediation:
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1522
|
+
expected: "a date in the past",
|
|
1523
|
+
message: "Use a valid past before cutoff, or remove before and set min-release-age.",
|
|
1524
|
+
remediation: {
|
|
1525
|
+
kind: "manual",
|
|
1526
|
+
steps: ["In .npmrc, set before to a valid past date. A future or disabled before overrides min-release-age in this file.", ...guardRemediationAvailability("npm", ctx.pmVersion, {
|
|
1527
|
+
kind: "manual",
|
|
1528
|
+
steps: [`Alternatively, remove before and set min-release-age to ~${RECOMMENDED_RELEASE_AGE_DAYS} days.`]
|
|
1529
|
+
}, [{
|
|
1530
|
+
file: npmrc$3,
|
|
1531
|
+
keyPath: ["min-release-age"]
|
|
1532
|
+
}])?.steps ?? []]
|
|
1533
|
+
}
|
|
1507
1534
|
};
|
|
1508
1535
|
}
|
|
1536
|
+
const actual = getByPath(config, ["min-release-age"]);
|
|
1537
|
+
const age = typeof actual === "number" || typeof actual === "string" ? Number(actual) : NaN;
|
|
1538
|
+
const cutoff = (/* @__PURE__ */ new Date(now - SECONDS_PER_DAY * 1e3 * age)).valueOf();
|
|
1539
|
+
if (age > 0 && Number.isFinite(cutoff) && cutoff < now) return { state: "ok" };
|
|
1540
|
+
return {
|
|
1541
|
+
state: "violation",
|
|
1542
|
+
actual,
|
|
1543
|
+
expected: RECOMMENDED_RELEASE_AGE_DAYS,
|
|
1544
|
+
message: `Set min-release-age to ~${RECOMMENDED_RELEASE_AGE_DAYS} days to quarantine brand-new releases.`,
|
|
1545
|
+
remediation: guardRemediationAvailability("npm", ctx.pmVersion, proposeChanges(config, [{
|
|
1546
|
+
file: npmrc$3,
|
|
1547
|
+
op: "setKey",
|
|
1548
|
+
keyPath: ["min-release-age"],
|
|
1549
|
+
value: RECOMMENDED_RELEASE_AGE_DAYS
|
|
1550
|
+
}]), [{
|
|
1551
|
+
file: npmrc$3,
|
|
1552
|
+
keyPath: ["min-release-age"]
|
|
1553
|
+
}])
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
1556
|
+
});
|
|
1557
|
+
const createDenoBinding = (time) => ({
|
|
1558
|
+
file: denoJson$1,
|
|
1559
|
+
docs: "https://docs.deno.com/runtime/reference/deno_json/",
|
|
1560
|
+
versionNote: {
|
|
1561
|
+
defaultSafeSince: "deno 2.9.0 (1440 minutes)",
|
|
1562
|
+
note: "object age may be omitted; project .npmrc fallback available since deno 2.8.1"
|
|
1509
1563
|
},
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
check(_ctx, config) {
|
|
1518
|
-
const actual = getByPath(config, ["minimumDependencyAge"]);
|
|
1519
|
-
if (isNonDisabledDenoDuration(actual)) return { state: "ok" };
|
|
1520
|
-
const objectAge = isPlainRecord(actual);
|
|
1521
|
-
const invalidObject = objectAge && (Object.keys(actual).some((key) => key !== "age" && key !== "exclude") || actual.exclude !== void 0 && !isStringList(actual.exclude));
|
|
1522
|
-
return {
|
|
1564
|
+
check(ctx, config) {
|
|
1565
|
+
const actual = getByPath(config, ["minimumDependencyAge"]);
|
|
1566
|
+
if (denoAgeUsesFallback(actual)) {
|
|
1567
|
+
const npmrcConfig = ctx.readConfig(npmrc$3);
|
|
1568
|
+
const npmrcAge = getByPath(npmrcConfig, ["min-release-age"]);
|
|
1569
|
+
if (isPositiveDenoNpmrcDays(npmrcAge)) return { state: "ok" };
|
|
1570
|
+
if (npmrcAge === 0) return {
|
|
1523
1571
|
state: "violation",
|
|
1524
|
-
actual,
|
|
1525
|
-
expected:
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
} : proposeChanges(config, [{
|
|
1531
|
-
file: denoJson$1,
|
|
1572
|
+
actual: npmrcAge,
|
|
1573
|
+
expected: RECOMMENDED_RELEASE_AGE_DAYS,
|
|
1574
|
+
file: npmrc$3.path,
|
|
1575
|
+
message: `Set min-release-age to ~${RECOMMENDED_RELEASE_AGE_DAYS} days in .npmrc, or set minimumDependencyAge in deno.json.`,
|
|
1576
|
+
remediation: proposeChanges(npmrcConfig, [{
|
|
1577
|
+
file: npmrc$3,
|
|
1532
1578
|
op: "setKey",
|
|
1533
|
-
keyPath:
|
|
1534
|
-
value:
|
|
1579
|
+
keyPath: ["min-release-age"],
|
|
1580
|
+
value: RECOMMENDED_RELEASE_AGE_DAYS
|
|
1535
1581
|
}])
|
|
1536
1582
|
};
|
|
1537
1583
|
}
|
|
1584
|
+
if (isNonDisabledDenoReleaseAge(actual, time.now(), time.parse)) return { state: "ok" };
|
|
1585
|
+
const objectAge = isPlainRecord(actual);
|
|
1586
|
+
const invalidObject = objectAge && (Object.keys(actual).some((key) => key !== "age" && key !== "exclude") || actual.exclude !== void 0 && !isStringList(actual.exclude));
|
|
1587
|
+
return {
|
|
1588
|
+
state: "violation",
|
|
1589
|
+
actual,
|
|
1590
|
+
expected: "P3D",
|
|
1591
|
+
message: `Set minimumDependencyAge (e.g. "P3D" for a ${RECOMMENDED_RELEASE_AGE_DAYS}-day cooldown) in deno.json.`,
|
|
1592
|
+
remediation: invalidObject ? {
|
|
1593
|
+
kind: "manual",
|
|
1594
|
+
steps: ["Use only age and exclude in minimumDependencyAge. Make exclude a list of package names and set age to a positive supported duration."]
|
|
1595
|
+
} : proposeChanges(config, [{
|
|
1596
|
+
file: denoJson$1,
|
|
1597
|
+
op: "setKey",
|
|
1598
|
+
keyPath: objectAge ? ["minimumDependencyAge", "age"] : ["minimumDependencyAge"],
|
|
1599
|
+
value: "P3D"
|
|
1600
|
+
}])
|
|
1601
|
+
};
|
|
1538
1602
|
}
|
|
1539
1603
|
});
|
|
1604
|
+
const createMinimumReleaseAge = (time) => overrideBindings(baseRule$1, {
|
|
1605
|
+
npm: createNpmBinding(time),
|
|
1606
|
+
deno: createDenoBinding(time)
|
|
1607
|
+
});
|
|
1540
1608
|
//#endregion
|
|
1541
1609
|
//#region src/domain/rules/named-registries.ts
|
|
1542
1610
|
const { pnpmWorkspace: pnpmWorkspace$4 } = CONFIG_FILES;
|
|
@@ -1617,7 +1685,8 @@ const REGISTRY_VERSION = /^(?:npm|jsr):(?:@[^/@]+\/)?[^/@]+@(?<version>[^/]*)/u;
|
|
|
1617
1685
|
const isUnpinnedRegistryImport = (specifier) => {
|
|
1618
1686
|
if (!specifier.startsWith("npm:") && !specifier.startsWith("jsr:")) return false;
|
|
1619
1687
|
const version = REGISTRY_VERSION.exec(specifier)?.groups?.version;
|
|
1620
|
-
|
|
1688
|
+
const normalized = version?.startsWith("=") ? version.slice(1) : version;
|
|
1689
|
+
return normalized === void 0 || valid(normalized) === null;
|
|
1621
1690
|
};
|
|
1622
1691
|
const collectUnpinnedImports = (imports) => {
|
|
1623
1692
|
const offenders = [];
|
|
@@ -1943,9 +2012,74 @@ const strictStoreIntegrity = defineRule({
|
|
|
1943
2012
|
//#endregion
|
|
1944
2013
|
//#region src/domain/rules/trust-policy.ts
|
|
1945
2014
|
const { aubeWorkspace, pnpmWorkspace } = CONFIG_FILES;
|
|
2015
|
+
const trustPolicy = withAubeParanoid(requireConfigKey({
|
|
2016
|
+
bindings: {
|
|
2017
|
+
aube: {
|
|
2018
|
+
docs: "https://aube.jdx.dev/security.html",
|
|
2019
|
+
documentedDefault: "no-downgrade",
|
|
2020
|
+
file: aubeWorkspace,
|
|
2021
|
+
keyPath: ["trustPolicy"],
|
|
2022
|
+
message: "Set `trustPolicy: no-downgrade` in aube-workspace.yaml to block packages whose trust level has decreased.",
|
|
2023
|
+
value: "no-downgrade"
|
|
2024
|
+
},
|
|
2025
|
+
pnpm: {
|
|
2026
|
+
docs: "https://pnpm.io/settings#trustpolicy",
|
|
2027
|
+
file: pnpmWorkspace,
|
|
2028
|
+
keyPath: ["trustPolicy"],
|
|
2029
|
+
message: "Set `trustPolicy: no-downgrade` in pnpm-workspace.yaml to block packages whose trust level has decreased.",
|
|
2030
|
+
value: "no-downgrade",
|
|
2031
|
+
versionNote: { configAvailableSince: "pnpm 10.21.0" }
|
|
2032
|
+
}
|
|
2033
|
+
},
|
|
2034
|
+
description: "Fail installation when a package trust level has decreased compared to previous releases, catching publisher credential downgrades.",
|
|
2035
|
+
docs: "https://pnpm.io/settings#trustpolicy",
|
|
2036
|
+
id: "trust-policy",
|
|
2037
|
+
severity: "warn",
|
|
2038
|
+
title: "Enforce trust policy on dependency updates"
|
|
2039
|
+
}));
|
|
2040
|
+
//#endregion
|
|
2041
|
+
//#region src/domain/rules/unsupported-settings.ts
|
|
2042
|
+
const bindingFor = (pm) => {
|
|
2043
|
+
const settings = settingAvailability.filter((setting) => setting.pm === pm);
|
|
2044
|
+
return { check(ctx) {
|
|
2045
|
+
const version = ctx.pmVersion;
|
|
2046
|
+
if (version === void 0) return { state: "na" };
|
|
2047
|
+
const unsupported = settings.filter((setting) => lt(version, setting.since) && getByPath(ctx.readConfig(setting.file), setting.keyPath) !== void 0);
|
|
2048
|
+
const [first, ...rest] = [...new Set(unsupported.map((setting) => setting.file.path))].map((file) => {
|
|
2049
|
+
const fileSettings = unsupported.filter((setting) => setting.file.path === file);
|
|
2050
|
+
return {
|
|
2051
|
+
state: "violation",
|
|
2052
|
+
file,
|
|
2053
|
+
message: `Target ${pm} ${version} predates support for ${file}: ${fileSettings.map((setting) => `${setting.keyPath.join(".")} (requires ${pm} >=${setting.since})`).join("; ")}.`,
|
|
2054
|
+
remediation: {
|
|
2055
|
+
kind: "manual",
|
|
2056
|
+
steps: ["Verify the PM version used by your install or publish command. If the declared target is accurate, upgrade the PM or use a security control supported by that target; removing a setting alone does not provide its protection.", ...fileSettings.map((setting) => `${setting.file.path}#${setting.keyPath.join(".")}: ${setting.source}`)]
|
|
2057
|
+
}
|
|
2058
|
+
};
|
|
2059
|
+
});
|
|
2060
|
+
return first ? {
|
|
2061
|
+
state: "violations",
|
|
2062
|
+
violations: [first, ...rest]
|
|
2063
|
+
} : { state: "ok" };
|
|
2064
|
+
} };
|
|
2065
|
+
};
|
|
2066
|
+
const unsupportedSettings = defineRule({
|
|
2067
|
+
id: "unsupported-settings",
|
|
2068
|
+
title: "Use settings available in the target PM version",
|
|
2069
|
+
description: "Report configured settings whose recorded introduction version is newer than the declared or explicit stable PM target. Reports each affected file separately, grouping its unsupported keys. Workspace findings retain their member directory. Only the coverage table below is checked. Unknown targets and unlisted settings are not evaluated for availability.",
|
|
2070
|
+
severity: "error",
|
|
2071
|
+
docs: "https://github.com/pHo9UBenaA/siro/blob/main/docs/rules.md#unsupported-settings--error",
|
|
2072
|
+
bindings: {
|
|
2073
|
+
npm: bindingFor("npm"),
|
|
2074
|
+
pnpm: bindingFor("pnpm"),
|
|
2075
|
+
yarn: bindingFor("yarn"),
|
|
2076
|
+
bun: bindingFor("bun"),
|
|
2077
|
+
deno: bindingFor("deno")
|
|
2078
|
+
}
|
|
2079
|
+
});
|
|
1946
2080
|
//#endregion
|
|
1947
2081
|
//#region src/domain/builtin-rules.ts
|
|
1948
|
-
const
|
|
2082
|
+
const createBuiltinRules = (time) => [
|
|
1949
2083
|
advisoryCheck,
|
|
1950
2084
|
approvedGitRepos,
|
|
1951
2085
|
auditSuppression,
|
|
@@ -1961,7 +2095,7 @@ const rules = [
|
|
|
1961
2095
|
frozenLockfile,
|
|
1962
2096
|
frozenStore,
|
|
1963
2097
|
hardenedMode,
|
|
1964
|
-
|
|
2098
|
+
createMinimumReleaseAge(time),
|
|
1965
2099
|
namedRegistries,
|
|
1966
2100
|
paranoidMode,
|
|
1967
2101
|
patchedDependencies,
|
|
@@ -1972,32 +2106,449 @@ const rules = [
|
|
|
1972
2106
|
strictAllowScripts,
|
|
1973
2107
|
strictReleaseAge,
|
|
1974
2108
|
strictStoreIntegrity,
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
2109
|
+
trustPolicy,
|
|
2110
|
+
unsupportedSettings
|
|
2111
|
+
];
|
|
2112
|
+
//#endregion
|
|
2113
|
+
//#region src/composition/rules.ts
|
|
2114
|
+
/** Read time at evaluation, never at module initialization. */
|
|
2115
|
+
const rules = createBuiltinRules({
|
|
2116
|
+
now: () => Date.now(),
|
|
2117
|
+
parse: (value) => Date.parse(value)
|
|
2118
|
+
});
|
|
2119
|
+
//#endregion
|
|
2120
|
+
//#region src/adapters/workspace-globs.ts
|
|
2121
|
+
const MAX_WORKSPACE_GLOB_ALTERNATIVES = 8192;
|
|
2122
|
+
const BRACE_EXPANSION_PROBE_LIMIT = 8193;
|
|
2123
|
+
const boundedBraceExpand = (pattern) => {
|
|
2124
|
+
const alternatives = braceExpand(pattern, { braceExpandMax: BRACE_EXPANSION_PROBE_LIMIT });
|
|
2125
|
+
if (alternatives.length > MAX_WORKSPACE_GLOB_ALTERNATIVES) throw new ConfigError(`Workspace pattern expands beyond ${MAX_WORKSPACE_GLOB_ALTERNATIVES} alternatives.`);
|
|
2126
|
+
return alternatives;
|
|
2127
|
+
};
|
|
2128
|
+
/** Expand with minimatch's own bounded brace semantics. */
|
|
2129
|
+
const expandWorkspaceGlob = (pattern) => {
|
|
2130
|
+
try {
|
|
2131
|
+
return boundedBraceExpand(pattern);
|
|
2132
|
+
} catch (error) {
|
|
2133
|
+
if (error instanceof TypeError) throw new ConfigError(`Invalid workspace pattern: ${error.message}.`);
|
|
2134
|
+
throw error;
|
|
2135
|
+
}
|
|
2136
|
+
};
|
|
2137
|
+
/** Engine choices live here; the application supplies only PM matching policy. */
|
|
2138
|
+
const engineOptions = (options) => {
|
|
2139
|
+
if (options.kind === "declaration") return {};
|
|
2140
|
+
return {
|
|
2141
|
+
platform: "linux",
|
|
2142
|
+
windowsPathsNoEscape: true,
|
|
2143
|
+
nonegate: true,
|
|
2144
|
+
nocomment: !options.hashComments,
|
|
2145
|
+
nocase: options.caseInsensitive,
|
|
2146
|
+
dot: options.includeDotDirectories ?? false,
|
|
2147
|
+
nobrace: options.syntax === "wildcards",
|
|
2148
|
+
noext: options.syntax === "wildcards" || options.extendedPatterns === false,
|
|
2149
|
+
optimizationLevel: options.syntax === "shell" ? 2 : 1
|
|
2150
|
+
};
|
|
2151
|
+
};
|
|
2152
|
+
/** Compile once so membership, exclusions, and traversal share glob semantics. */
|
|
2153
|
+
const compileWorkspaceGlob = (pattern, options) => {
|
|
2154
|
+
let matcher;
|
|
2155
|
+
try {
|
|
2156
|
+
const settings = engineOptions(options);
|
|
2157
|
+
if (!settings.nobrace) boundedBraceExpand(pattern);
|
|
2158
|
+
matcher = new Minimatch(options.kind === "directory" && options.syntax === "wildcards" ? pattern.replace(/[[\]]/gu, (character) => character === "[" ? "[[]" : "[]]") : pattern, {
|
|
2159
|
+
...settings,
|
|
2160
|
+
braceExpandMax: BRACE_EXPANSION_PROBE_LIMIT
|
|
2161
|
+
});
|
|
2162
|
+
} catch (error) {
|
|
2163
|
+
if (error instanceof TypeError) throw new ConfigError(`Invalid workspace pattern: ${error.message}.`);
|
|
2164
|
+
throw error;
|
|
2165
|
+
}
|
|
2166
|
+
return {
|
|
2167
|
+
matches: (directory) => matcher.match(directory),
|
|
2168
|
+
canDescend(directory) {
|
|
2169
|
+
const parts = directory.split("/");
|
|
2170
|
+
return matcher.set.some((alternative) => (alternative.includes(GLOBSTAR) || parts.length < alternative.length) && matcher.matchOne(parts, alternative, true));
|
|
2171
|
+
}
|
|
2172
|
+
};
|
|
2173
|
+
};
|
|
2174
|
+
const minimatchGlobs = {
|
|
2175
|
+
expand: expandWorkspaceGlob,
|
|
2176
|
+
compile: compileWorkspaceGlob
|
|
2177
|
+
};
|
|
2178
|
+
//#endregion
|
|
2179
|
+
//#region src/domain/services/parse-config-file.ts
|
|
2180
|
+
const EMPTY_FILE = Object.freeze({});
|
|
2181
|
+
/** Create a fresh parser and cache for one lint command. */
|
|
2182
|
+
const createConfigParser = (codecFor, ctx) => {
|
|
2183
|
+
const cache = /* @__PURE__ */ new Map();
|
|
2184
|
+
return (file) => {
|
|
2185
|
+
if (file === void 0) return EMPTY_FILE;
|
|
2186
|
+
const cacheKey = `${file.kind}:${file.path}`;
|
|
2187
|
+
const cached = cache.get(cacheKey);
|
|
2188
|
+
if (cached !== void 0) return cached;
|
|
2189
|
+
const text = ctx.readText(file.path);
|
|
2190
|
+
if (text === void 0) {
|
|
2191
|
+
cache.set(cacheKey, EMPTY_FILE);
|
|
2192
|
+
return EMPTY_FILE;
|
|
2193
|
+
}
|
|
2194
|
+
const parsed = wrapCodecError(file.path, () => codecFor(file.kind).parse(text));
|
|
2195
|
+
cache.set(cacheKey, parsed);
|
|
2196
|
+
return parsed;
|
|
2197
|
+
};
|
|
2198
|
+
};
|
|
2199
|
+
//#endregion
|
|
2200
|
+
//#region src/application/workspace-pattern.ts
|
|
2201
|
+
/** Detect the basic * and ? markers shared across workspace dialects. */
|
|
2202
|
+
const hasBasicWorkspaceWildcard = (pattern) => pattern.includes("*") || pattern.includes("?");
|
|
2203
|
+
/** Workspace declarations use POSIX separators regardless of the host platform. */
|
|
2204
|
+
const stripTrailingWorkspaceSlashes = (pattern) => {
|
|
2205
|
+
let end = pattern.length;
|
|
2206
|
+
while (end > 0 && pattern[end - 1] === "/") end -= 1;
|
|
2207
|
+
return pattern.slice(0, end);
|
|
2208
|
+
};
|
|
2209
|
+
//#endregion
|
|
2210
|
+
//#region src/application/workspace-prefix.ts
|
|
2211
|
+
/** Anchor Deno's native literal prefix before applying its glob suffix. */
|
|
2212
|
+
const anchorWorkspacePrefix = (pattern, glob, resolveChild) => {
|
|
2213
|
+
const parts = pattern.split("/");
|
|
2214
|
+
const firstWildcard = parts.findIndex(hasBasicWorkspaceWildcard);
|
|
2215
|
+
const prefix = parts.slice(0, firstWildcard < 0 ? parts.length : firstWildcard);
|
|
2216
|
+
if (prefix.length === 0 || pattern === ".") return glob;
|
|
2217
|
+
const declared = prefix.join("/");
|
|
2218
|
+
let resolved;
|
|
2219
|
+
let attempted = false;
|
|
2220
|
+
const resolve = () => {
|
|
2221
|
+
if (!attempted) {
|
|
2222
|
+
attempted = true;
|
|
2223
|
+
let current = ".";
|
|
2224
|
+
for (const part of prefix) {
|
|
2225
|
+
const child = resolveChild(current, part);
|
|
2226
|
+
if (child === void 0) return void 0;
|
|
2227
|
+
current = current === "." ? child : `${current}/${child}`;
|
|
1992
2228
|
}
|
|
2229
|
+
resolved = current;
|
|
2230
|
+
}
|
|
2231
|
+
return resolved;
|
|
2232
|
+
};
|
|
2233
|
+
const rewrite = (directory, actual) => directory === actual ? declared : directory.startsWith(`${actual}/`) ? declared + directory.slice(actual.length) : void 0;
|
|
2234
|
+
return {
|
|
2235
|
+
matches(directory) {
|
|
2236
|
+
if (directory === ".") return false;
|
|
2237
|
+
const actual = resolve();
|
|
2238
|
+
const rewritten = actual === void 0 ? void 0 : rewrite(directory, actual);
|
|
2239
|
+
return rewritten !== void 0 && glob.matches(rewritten);
|
|
1993
2240
|
},
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2241
|
+
canDescend(directory) {
|
|
2242
|
+
const actual = resolve();
|
|
2243
|
+
if (actual === void 0) return false;
|
|
2244
|
+
if (directory === "." || actual.startsWith(`${directory}/`)) return true;
|
|
2245
|
+
const rewritten = rewrite(directory, actual);
|
|
2246
|
+
return rewritten !== void 0 && glob.canDescend(rewritten);
|
|
2247
|
+
}
|
|
2248
|
+
};
|
|
2249
|
+
};
|
|
2250
|
+
//#endregion
|
|
2251
|
+
//#region src/application/workspace-glob-policy.ts
|
|
2252
|
+
const workspaceGlobOptions = (caseInsensitive) => ({
|
|
2253
|
+
kind: "directory",
|
|
2254
|
+
syntax: "shell",
|
|
2255
|
+
caseInsensitive
|
|
2256
|
+
});
|
|
2257
|
+
//#endregion
|
|
2258
|
+
//#region src/application/workspace-dialects.ts
|
|
2259
|
+
const containsUnsupportedAubeGlobSyntax = (pattern) => /[[\]{}()]/u.test(pattern);
|
|
2260
|
+
const matchesCrossPathGlob = (pattern, value) => {
|
|
2261
|
+
const patternCharacters = [...pattern];
|
|
2262
|
+
const valueCharacters = [...value];
|
|
2263
|
+
let patternIndex = 0;
|
|
2264
|
+
let valueIndex = 0;
|
|
2265
|
+
let starIndex = -1;
|
|
2266
|
+
let retryIndex = 0;
|
|
2267
|
+
while (valueIndex < valueCharacters.length) {
|
|
2268
|
+
const character = patternCharacters[patternIndex];
|
|
2269
|
+
if (character === "?" || character === valueCharacters[valueIndex]) {
|
|
2270
|
+
patternIndex += 1;
|
|
2271
|
+
valueIndex += 1;
|
|
2272
|
+
} else if (character === "*") {
|
|
2273
|
+
starIndex = patternIndex;
|
|
2274
|
+
retryIndex = valueIndex;
|
|
2275
|
+
patternIndex += 1;
|
|
2276
|
+
} else if (starIndex >= 0) {
|
|
2277
|
+
patternIndex = starIndex + 1;
|
|
2278
|
+
retryIndex += 1;
|
|
2279
|
+
valueIndex = retryIndex;
|
|
2280
|
+
} else return false;
|
|
2281
|
+
}
|
|
2282
|
+
while (patternCharacters[patternIndex] === "*") patternIndex += 1;
|
|
2283
|
+
return patternIndex === patternCharacters.length;
|
|
2284
|
+
};
|
|
2285
|
+
/** The bounded glob subset verified against the recorded Deno/Aube sources. */
|
|
2286
|
+
const compileAdditionalWorkspaceGlob = (pattern, pm, excluded, caseInsensitive, globs) => {
|
|
2287
|
+
const parts = pattern.split("/");
|
|
2288
|
+
if (parts.some((part) => part.includes("**") && part !== "**")) throw new ConfigError(`${pm}: ** must occupy an entire workspace path component.`);
|
|
2289
|
+
if (pm === "aube" && (containsUnsupportedAubeGlobSyntax(pattern) || pattern.includes("**") && (parts.at(-1) !== "**" || hasBasicWorkspaceWildcard(parts.slice(0, -1).join("/"))))) throw new ConfigError("Aube workspace inspection supports literals, * and ?, and a trailing ** after a literal prefix; other glob forms are not yet supported.");
|
|
2290
|
+
const matcher = globs.compile(pattern, {
|
|
2291
|
+
kind: "directory",
|
|
2292
|
+
syntax: "wildcards",
|
|
2293
|
+
includeDotDirectories: pm === "aube",
|
|
2294
|
+
caseInsensitive: pm === "deno" && (hasBasicWorkspaceWildcard(pattern) || caseInsensitive)
|
|
2295
|
+
});
|
|
2296
|
+
if (pm === "deno" && hasBasicWorkspaceWildcard(pattern)) {
|
|
2297
|
+
const files = globs.compile(`${pattern}/package.json`, {
|
|
2298
|
+
kind: "directory",
|
|
2299
|
+
syntax: "wildcards",
|
|
2300
|
+
caseInsensitive: true
|
|
2301
|
+
});
|
|
2302
|
+
return {
|
|
2303
|
+
...matcher,
|
|
2304
|
+
matches: (directory) => files.matches(directory === "." ? "package.json" : `${directory}/package.json`)
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
2307
|
+
if (pm !== "aube" || !excluded) return matcher;
|
|
2308
|
+
const self = pattern.endsWith("/**") ? pattern.slice(0, -3) : void 0;
|
|
2309
|
+
return {
|
|
2310
|
+
...matcher,
|
|
2311
|
+
matches: (directory) => matchesCrossPathGlob(pattern, directory) || directory === self
|
|
2312
|
+
};
|
|
2313
|
+
};
|
|
2314
|
+
//#endregion
|
|
2315
|
+
//#region src/application/workspaces.ts
|
|
2316
|
+
/** npm cancels an earlier exclusion when a later positive pattern matches it. */
|
|
2317
|
+
const splitNpmPattern = (raw) => {
|
|
2318
|
+
const prefix = /^!+/u.exec(raw)?.[0] ?? "";
|
|
2319
|
+
return {
|
|
2320
|
+
excluded: prefix.length % 2 === 1,
|
|
2321
|
+
pattern: raw.slice(prefix.length)
|
|
2322
|
+
};
|
|
2323
|
+
};
|
|
2324
|
+
const npmPatterns = (patterns, globs) => {
|
|
2325
|
+
const positive = [];
|
|
2326
|
+
let negative = [];
|
|
2327
|
+
for (const raw of patterns) {
|
|
2328
|
+
const parsed = splitNpmPattern(raw);
|
|
2329
|
+
const pattern = parsed.pattern.replace(/^\.?\/+/u, "");
|
|
2330
|
+
if (parsed.excluded) negative.push({
|
|
2331
|
+
pattern,
|
|
2332
|
+
glob: globs.compile(pattern, { kind: "declaration" })
|
|
2333
|
+
});
|
|
2334
|
+
else {
|
|
2335
|
+
for (let index = 0; index < negative.length; index += 1) if (negative[index]?.glob.matches(pattern)) negative.splice(index, 1);
|
|
2336
|
+
positive.push(pattern);
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
return [...positive, ...negative.map(({ pattern }) => `!${pattern}`)];
|
|
2340
|
+
};
|
|
2341
|
+
/** Read declaration sources separately so Deno's two manifest sets stay distinct. */
|
|
2342
|
+
const workspaceDefinitions = (ctx, pm, dependencies) => {
|
|
2343
|
+
const { codecFor, paths, globs } = dependencies;
|
|
2344
|
+
const parse = createConfigParser(codecFor, ctx);
|
|
2345
|
+
const validate = (value, source, denoManifests = false) => {
|
|
2346
|
+
if (value === void 0) return {
|
|
2347
|
+
patterns: [],
|
|
2348
|
+
denoManifests
|
|
2349
|
+
};
|
|
2350
|
+
if (!Array.isArray(value) || !Array.from(value).every((item) => typeof item === "string")) throw new ConfigError(`${source}: expected an array of directory patterns.`);
|
|
2351
|
+
for (const pattern of value) {
|
|
2352
|
+
const positive = pm === "npm" ? splitNpmPattern(pattern).pattern : pattern.startsWith("!") ? pattern.slice(1) : pattern;
|
|
2353
|
+
if ((pm === "deno" || pm === "aube" ? [positive] : globs.expand(positive)).some((alternative) => !isRelPath(alternative) || alternative.includes("\\") || alternative.startsWith("!"))) throw new ConfigError(`${source}: use relative directory patterns without parent traversal: ${JSON.stringify(pattern)}.`);
|
|
2354
|
+
if (pm === "deno" && denoManifests && !pattern.startsWith("!") && paths.normalizePattern(positive) === ".") throw new ConfigError(`${source}: a Deno workspace cannot contain itself.`);
|
|
2355
|
+
}
|
|
2356
|
+
return {
|
|
2357
|
+
patterns: pm === "npm" ? npmPatterns(value, globs) : value,
|
|
2358
|
+
denoManifests
|
|
2359
|
+
};
|
|
2360
|
+
};
|
|
2361
|
+
const packageDefinition = () => {
|
|
2362
|
+
let value = ctx.packageJson?.workspaces;
|
|
2363
|
+
if (isPlainRecord(value)) {
|
|
2364
|
+
value = value.packages;
|
|
2365
|
+
if (value === void 0) throw new ConfigError("package.json#workspaces: expected a packages array.");
|
|
2366
|
+
}
|
|
2367
|
+
return validate(value, "package.json#workspaces");
|
|
2368
|
+
};
|
|
2369
|
+
if (pm === "pnpm") return [validate(ctx.exists(CONFIG_FILES.pnpmWorkspace.path) ? parse(CONFIG_FILES.pnpmWorkspace).packages : void 0, "pnpm-workspace.yaml#packages")];
|
|
2370
|
+
if (pm === "aube") {
|
|
2371
|
+
const file = [CONFIG_FILES.aubeWorkspace, CONFIG_FILES.pnpmWorkspace].find((candidate) => ctx.exists(candidate.path));
|
|
2372
|
+
return [file ? validate(parse(file).packages, `${file.path}#packages`) : packageDefinition()];
|
|
2373
|
+
}
|
|
2374
|
+
if (pm === "deno") return [validate(parse(CONFIG_FILES.denoJson).workspace, "deno.json#workspace", true), packageDefinition()];
|
|
2375
|
+
return [packageDefinition()];
|
|
2376
|
+
};
|
|
2377
|
+
/** Traverse only ordinary directories that can fall under a positive pattern. */
|
|
2378
|
+
const workspaceDirectories = (ctx, fs, definition, pm, dependencies) => {
|
|
2379
|
+
const { codecFor, paths, globs, caseInsensitiveGlobs } = dependencies;
|
|
2380
|
+
const defaultWorkspaceGlobOptions = workspaceGlobOptions(caseInsensitiveGlobs);
|
|
2381
|
+
const { patterns } = definition;
|
|
2382
|
+
const skipVendor = pm === "deno" && createConfigParser(codecFor, ctx)(CONFIG_FILES.denoJson).vendor === true;
|
|
2383
|
+
const positive = patterns.filter((pattern) => !pattern.startsWith("!")).map((pattern) => stripTrailingWorkspaceSlashes(paths.normalizePattern(pattern)));
|
|
2384
|
+
const negative = patterns.filter((pattern) => pattern.startsWith("!")).map((pattern) => stripTrailingWorkspaceSlashes(paths.normalizePattern(pattern.slice(1))));
|
|
2385
|
+
const directoryCache = /* @__PURE__ */ new Map();
|
|
2386
|
+
const readDirectories = (directory) => {
|
|
2387
|
+
const cached = directoryCache.get(directory);
|
|
2388
|
+
if (cached) return cached;
|
|
2389
|
+
if (!fs.readDirectories) throw new UsageError("Workspace discovery requires FileSystem.readDirectories; no host filesystem fallback is used.");
|
|
2390
|
+
const names = fs.readDirectories(paths.resolve(ctx.root, asRelPath(directory)));
|
|
2391
|
+
if (!Array.isArray(names)) throw new ConfigError("FileSystem.readDirectories must return an array of directory names.");
|
|
2392
|
+
for (const name of Array.from(names)) if (!isRelPath(name) || name === "." || name === ".." || /[\\/]/u.test(name)) throw new ConfigError("FileSystem.readDirectories returned an invalid child directory name.");
|
|
2393
|
+
const result = [...new Set(names)].sort();
|
|
2394
|
+
directoryCache.set(directory, result);
|
|
2395
|
+
return result;
|
|
2396
|
+
};
|
|
2397
|
+
const resolveChild = (parent, name) => {
|
|
2398
|
+
if (name === ".git" || name === "node_modules") return void 0;
|
|
2399
|
+
const names = readDirectories(parent);
|
|
2400
|
+
const resolved = names.includes(name) ? name : fs.resolveDirectory?.(paths.resolve(ctx.root, asRelPath(parent)), name);
|
|
2401
|
+
if (resolved !== void 0 && !names.includes(resolved)) throw new ConfigError("FileSystem.resolveDirectory must return an enumerated ordinary child directory name.");
|
|
2402
|
+
return resolved === ".git" || resolved === "node_modules" ? void 0 : resolved;
|
|
2403
|
+
};
|
|
2404
|
+
const compile = (pattern, excluded = false) => {
|
|
2405
|
+
if (pm !== "deno" && pm !== "aube") return globs.compile(pattern, pm === "npm" ? {
|
|
2406
|
+
...defaultWorkspaceGlobOptions,
|
|
2407
|
+
hashComments: true
|
|
2408
|
+
} : defaultWorkspaceGlobOptions);
|
|
2409
|
+
const glob = compileAdditionalWorkspaceGlob(pattern, pm, excluded, caseInsensitiveGlobs, globs);
|
|
2410
|
+
return pm === "deno" && !excluded ? anchorWorkspacePrefix(pattern, glob, resolveChild) : glob;
|
|
2411
|
+
};
|
|
2412
|
+
const included = positive.map((pattern) => compile(pattern));
|
|
2413
|
+
const excluded = negative.map((pattern) => {
|
|
2414
|
+
return {
|
|
2415
|
+
glob: compile(pattern, true),
|
|
2416
|
+
subtree: pm === "deno" || pm === "aube" || pm === "bun" ? void 0 : pattern === "**" ? { matches: () => true } : pattern.endsWith("/**") ? compile(pattern.slice(0, -3), true) : void 0
|
|
2417
|
+
};
|
|
2418
|
+
});
|
|
2419
|
+
const declaredBase = (pattern) => {
|
|
2420
|
+
const parts = pattern.split("/");
|
|
2421
|
+
const wildcard = parts.findIndex(hasBasicWorkspaceWildcard);
|
|
2422
|
+
return parts.slice(0, wildcard < 0 ? parts.length : wildcard).join("/") || ".";
|
|
2423
|
+
};
|
|
2424
|
+
const relatedBases = (left, right) => left === "." || right === "." || left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
|
|
2425
|
+
const denoPositive = pm === "deno" ? positive.filter(hasBasicWorkspaceWildcard).map((pattern) => ({
|
|
2426
|
+
base: declaredBase(pattern),
|
|
2427
|
+
scope: anchorWorkspacePrefix(declaredBase(pattern), {
|
|
2428
|
+
matches: () => true,
|
|
2429
|
+
canDescend: () => true
|
|
2430
|
+
}, resolveChild)
|
|
2431
|
+
})) : [];
|
|
2432
|
+
const denoOrdered = pm === "deno" ? patterns.filter((pattern) => hasBasicWorkspaceWildcard(pattern) || pattern.startsWith("!")).map((raw) => {
|
|
2433
|
+
const isExcluded = raw.startsWith("!");
|
|
2434
|
+
const pattern = stripTrailingWorkspaceSlashes(paths.normalizePattern(isExcluded ? raw.slice(1) : raw));
|
|
2435
|
+
const glob = compile(pattern, isExcluded);
|
|
2436
|
+
if (!isExcluded) return {
|
|
2437
|
+
excluded: false,
|
|
2438
|
+
glob
|
|
2439
|
+
};
|
|
2440
|
+
return {
|
|
2441
|
+
excluded: true,
|
|
2442
|
+
glob: {
|
|
2443
|
+
...glob,
|
|
2444
|
+
matches(directory) {
|
|
2445
|
+
if (!hasBasicWorkspaceWildcard(pattern)) return directory === pattern;
|
|
2446
|
+
if (!glob.matches(directory)) return false;
|
|
2447
|
+
const applicable = denoPositive.filter((item) => item.scope.matches(directory)).map((item) => relatedBases(item.base, declaredBase(pattern)));
|
|
2448
|
+
if (applicable.some(Boolean) && applicable.some((value) => !value)) throw new ConfigError("Deno workspace exclusion has ambiguous overlapping bases; use consistent prefix spelling or separate non-overlapping patterns.");
|
|
2449
|
+
return applicable.some(Boolean);
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
};
|
|
2453
|
+
}) : [];
|
|
2454
|
+
const bunPatterns = pm === "bun" ? patterns.map((raw) => {
|
|
2455
|
+
const isExcluded = raw.startsWith("!");
|
|
2456
|
+
const pattern = stripTrailingWorkspaceSlashes(paths.normalizePattern(isExcluded ? raw.slice(1) : raw));
|
|
2457
|
+
const usesGlob = isExcluded || hasBasicWorkspaceWildcard(pattern) || pattern.includes("{") || pattern.includes("[");
|
|
2458
|
+
const glob = globs.compile(pattern, {
|
|
2459
|
+
...defaultWorkspaceGlobOptions,
|
|
2460
|
+
extendedPatterns: false
|
|
2461
|
+
});
|
|
2462
|
+
const subtree = isExcluded && pattern.endsWith("/**") ? globs.compile(pattern.slice(0, -3), {
|
|
2463
|
+
...defaultWorkspaceGlobOptions,
|
|
2464
|
+
extendedPatterns: false
|
|
2465
|
+
}) : void 0;
|
|
2466
|
+
return {
|
|
2467
|
+
isExcluded,
|
|
2468
|
+
usesGlob,
|
|
2469
|
+
glob,
|
|
2470
|
+
matches: (directory) => glob.matches(directory) || glob.matches(`${directory}/`) || subtree?.matches(directory) === true
|
|
2471
|
+
};
|
|
2472
|
+
}) : [];
|
|
2473
|
+
const bunOrdered = bunPatterns.filter(({ usesGlob }) => usesGlob);
|
|
2474
|
+
const bunLiterals = bunPatterns.filter(({ isExcluded, usesGlob }) => !isExcluded && !usesGlob);
|
|
2475
|
+
const bunTraversal = bunPatterns.filter(({ isExcluded }) => !isExcluded).map(({ glob }) => glob);
|
|
2476
|
+
const denoLiteral = pm === "deno" ? positive.filter((pattern) => !hasBasicWorkspaceWildcard(pattern)).map((pattern) => compile(pattern)) : [];
|
|
2477
|
+
if (definition.denoManifests && denoOrdered.findLast(({ glob }) => glob.matches("."))?.excluded === false) throw new ConfigError("deno.json#workspace: a Deno workspace cannot contain itself.");
|
|
2478
|
+
if (positive.length === 0 || positive.every((pattern) => pattern === ".")) return [];
|
|
2479
|
+
if (!fs.readDirectories) throw new UsageError("Workspace discovery requires FileSystem.readDirectories; no host filesystem fallback is used.");
|
|
2480
|
+
const result = [];
|
|
2481
|
+
const pending = ["."];
|
|
2482
|
+
while (pending.length > 0) {
|
|
2483
|
+
const current = pending.pop();
|
|
2484
|
+
if (current === void 0) break;
|
|
2485
|
+
for (const name of readDirectories(current)) {
|
|
2486
|
+
if (name === ".git" || name === "node_modules") continue;
|
|
2487
|
+
const directory = asRelPath(current === "." ? name : `${current}/${name}`);
|
|
2488
|
+
if (pm === "bun" && name === "CMakeFiles" && !bunLiterals.some(({ glob }) => glob.matches(directory) || glob.canDescend(directory))) continue;
|
|
2489
|
+
const inVendor = skipVendor && (directory === "vendor" || directory.startsWith("vendor/"));
|
|
2490
|
+
if (inVendor && !denoLiteral.some((pattern) => pattern.matches(directory) || pattern.canDescend(directory))) continue;
|
|
2491
|
+
const isExcluded = pm !== "deno" && excluded.some(({ glob }) => glob.matches(directory) || glob.matches(`${directory}/`));
|
|
2492
|
+
if (excluded.some(({ subtree }) => subtree?.matches(directory))) continue;
|
|
2493
|
+
const lastDenoMatch = denoOrdered.findLast(({ glob }) => glob.matches(directory));
|
|
2494
|
+
const lastBunMatch = bunOrdered.findLast(({ matches }) => matches(directory));
|
|
2495
|
+
if (pm === "deno" ? denoLiteral.some((pattern) => pattern.matches(directory)) || !inVendor && lastDenoMatch?.excluded === false : pm === "bun" ? bunLiterals.some(({ glob }) => glob.matches(directory)) || lastBunMatch?.isExcluded === false : !isExcluded && included.some((pattern) => pattern.matches(directory))) result.push(directory);
|
|
2496
|
+
if ((pm === "bun" ? bunTraversal : inVendor ? denoLiteral : included).some((pattern) => pattern.canDescend(directory))) pending.push(directory);
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
return result.sort();
|
|
2500
|
+
};
|
|
2501
|
+
//#endregion
|
|
2502
|
+
//#region src/application/workspace-contexts.ts
|
|
2503
|
+
/** Expand each manager's declarations into isolated publication contexts. */
|
|
2504
|
+
const collectWorkspaceMembers = (ctx, fs, pms, projectType, dependencies) => {
|
|
2505
|
+
const { codecFor, createRepoContext, paths } = dependencies;
|
|
2506
|
+
return pms.flatMap((pm) => {
|
|
2507
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2508
|
+
return workspaceDefinitions(ctx, pm, dependencies).flatMap((definition) => workspaceDirectories(ctx, fs, definition, pm, dependencies).flatMap((directory) => {
|
|
2509
|
+
const root = paths.resolve(ctx.root, directory);
|
|
2510
|
+
const manifest = paths.resolve(root, asRelPath("package.json"));
|
|
2511
|
+
const denoManifest = paths.resolve(root, asRelPath("deno.json"));
|
|
2512
|
+
const denoJsonc = paths.resolve(root, asRelPath("deno.jsonc"));
|
|
2513
|
+
const allowed = new Set(pm === "deno" ? [
|
|
2514
|
+
manifest,
|
|
2515
|
+
denoManifest,
|
|
2516
|
+
denoJsonc
|
|
2517
|
+
] : [manifest]);
|
|
2518
|
+
const memberFs = {
|
|
2519
|
+
exists: (file) => allowed.has(file) && fs.exists(file),
|
|
2520
|
+
readText: (file) => allowed.has(file) ? fs.readText(file) : void 0
|
|
2521
|
+
};
|
|
2522
|
+
try {
|
|
2523
|
+
const explicitMember = definition.patterns.some((pattern) => !pattern.startsWith("!") && !hasBasicWorkspaceWildcard(pattern) && stripTrailingWorkspaceSlashes(paths.normalizePattern(pattern)) === directory);
|
|
2524
|
+
if (!definition.denoManifests && !fs.exists(manifest)) {
|
|
2525
|
+
if (pm === "deno" && explicitMember) throw new ConfigError("Declared npm workspace member has no package.json.");
|
|
2526
|
+
return [];
|
|
2527
|
+
}
|
|
2528
|
+
if (pm === "deno" && !fs.exists(denoManifest) && fs.exists(denoJsonc)) throw new ConfigError("deno.jsonc is not supported; use strict deno.json.");
|
|
2529
|
+
const memberCtx = createRepoContext(root, memberFs, projectType);
|
|
2530
|
+
if (!definition.denoManifests && !memberCtx.packageJson) return [];
|
|
2531
|
+
if (!memberCtx.packageJson && !memberCtx.exists(asRelPath("deno.json"))) {
|
|
2532
|
+
if (definition.denoManifests && explicitMember) throw new ConfigError("Declared Deno member has no deno.json or package.json.");
|
|
2533
|
+
return [];
|
|
2534
|
+
}
|
|
2535
|
+
if (pm === "deno") {
|
|
2536
|
+
if (createConfigParser(codecFor, memberCtx)(CONFIG_FILES.denoJson).workspace !== void 0 || memberCtx.packageJson?.workspaces !== void 0) throw new ConfigError("Nested workspace declarations are not supported by Deno.");
|
|
2537
|
+
}
|
|
2538
|
+
if (seen.has(directory)) return [];
|
|
2539
|
+
seen.add(directory);
|
|
2540
|
+
return [{
|
|
2541
|
+
directory,
|
|
2542
|
+
pm,
|
|
2543
|
+
ctx: memberCtx
|
|
2544
|
+
}];
|
|
2545
|
+
} catch (error) {
|
|
2546
|
+
if (error instanceof ConfigError) throw new ConfigError(`${directory}/${error.message}`);
|
|
2547
|
+
throw error;
|
|
2548
|
+
}
|
|
2549
|
+
}));
|
|
2550
|
+
});
|
|
2551
|
+
};
|
|
2001
2552
|
//#endregion
|
|
2002
2553
|
//#region src/domain/services/apply-config.ts
|
|
2003
2554
|
/** Select active rules and keep user severity choices as explicit runtime data. */
|
|
@@ -2076,27 +2627,6 @@ const resolvePMs = (ctx, opts) => {
|
|
|
2076
2627
|
/** Resolve the most specific severity signal for one finding. */
|
|
2077
2628
|
const decideSeverity = (status, binding, rule, userOverride) => userOverride ?? status.severity ?? binding.severity ?? rule.severity;
|
|
2078
2629
|
//#endregion
|
|
2079
|
-
//#region src/domain/services/parse-config-file.ts
|
|
2080
|
-
const EMPTY_FILE = Object.freeze({});
|
|
2081
|
-
/** Create a fresh parser and cache for one lint command. */
|
|
2082
|
-
const createConfigParser = (codecFor, ctx) => {
|
|
2083
|
-
const cache = /* @__PURE__ */ new Map();
|
|
2084
|
-
return (file) => {
|
|
2085
|
-
if (file === void 0) return EMPTY_FILE;
|
|
2086
|
-
const cacheKey = `${file.kind}:${file.path}`;
|
|
2087
|
-
const cached = cache.get(cacheKey);
|
|
2088
|
-
if (cached !== void 0) return cached;
|
|
2089
|
-
const text = ctx.readText(file.path);
|
|
2090
|
-
if (text === void 0) {
|
|
2091
|
-
cache.set(cacheKey, EMPTY_FILE);
|
|
2092
|
-
return EMPTY_FILE;
|
|
2093
|
-
}
|
|
2094
|
-
const parsed = wrapCodecError(file.path, () => codecFor(file.kind).parse(text));
|
|
2095
|
-
cache.set(cacheKey, parsed);
|
|
2096
|
-
return parsed;
|
|
2097
|
-
};
|
|
2098
|
-
};
|
|
2099
|
-
//#endregion
|
|
2100
2630
|
//#region src/domain/services/render-version-note.ts
|
|
2101
2631
|
const buildVersionNoteParts = (versionNote) => {
|
|
2102
2632
|
const parts = [];
|
|
@@ -2128,29 +2658,33 @@ const runLint = (opts) => {
|
|
|
2128
2658
|
warn: 0
|
|
2129
2659
|
};
|
|
2130
2660
|
const parseConfig = createConfigParser(codecFor, ctx);
|
|
2131
|
-
const ruleContext = {
|
|
2132
|
-
...ctx,
|
|
2133
|
-
readConfig: parseConfig
|
|
2134
|
-
};
|
|
2135
2661
|
for (const rule of ruleSet) for (const pm of pms) {
|
|
2136
2662
|
const binding = rule.bindings[pm];
|
|
2137
2663
|
if (!binding || rule.projectTypes && !rule.projectTypes.includes(resolveBindingProjectType(ctx, pm, parseConfig))) continue;
|
|
2138
|
-
const
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
ruleId: rule.id,
|
|
2143
|
-
pm,
|
|
2144
|
-
severity: decideSeverity(status, binding, rule, severityOverrides?.get(rule.id)),
|
|
2145
|
-
message: renderVersionNoteMessage(status.message, binding.versionNote),
|
|
2146
|
-
file: status.file ?? binding.file?.path,
|
|
2147
|
-
docs: binding.docs ?? rule.docs,
|
|
2148
|
-
actual: status.actual,
|
|
2149
|
-
expected: status.expected,
|
|
2150
|
-
remediation: status.remediation
|
|
2664
|
+
const ruleContext = {
|
|
2665
|
+
...ctx,
|
|
2666
|
+
readConfig: parseConfig,
|
|
2667
|
+
pmVersion: opts.pmVersions?.[pm]
|
|
2151
2668
|
};
|
|
2152
|
-
|
|
2153
|
-
|
|
2669
|
+
const response = binding.check(ruleContext, parseConfig(binding.file));
|
|
2670
|
+
if (!isCheckStatusShape(response)) throw new ConfigError(`Rule '${rule.id}' returned an invalid check result.`);
|
|
2671
|
+
const statuses = response.state === "violations" ? response.violations : [response];
|
|
2672
|
+
for (const status of statuses) {
|
|
2673
|
+
if (status.state !== "violation") continue;
|
|
2674
|
+
const finding = {
|
|
2675
|
+
ruleId: rule.id,
|
|
2676
|
+
pm,
|
|
2677
|
+
severity: decideSeverity(status, binding, rule, severityOverrides?.get(rule.id)),
|
|
2678
|
+
message: renderVersionNoteMessage(status.message, binding.versionNote),
|
|
2679
|
+
file: status.file ?? binding.file?.path,
|
|
2680
|
+
docs: binding.docs ?? rule.docs,
|
|
2681
|
+
actual: status.actual,
|
|
2682
|
+
expected: status.expected,
|
|
2683
|
+
remediation: guardRemediationAvailability(pm, ruleContext.pmVersion, status.remediation)
|
|
2684
|
+
};
|
|
2685
|
+
findings.push(finding);
|
|
2686
|
+
summary[finding.severity] += 1;
|
|
2687
|
+
}
|
|
2154
2688
|
}
|
|
2155
2689
|
return {
|
|
2156
2690
|
findings,
|
|
@@ -2160,52 +2694,352 @@ const runLint = (opts) => {
|
|
|
2160
2694
|
//#endregion
|
|
2161
2695
|
//#region src/application/lint.ts
|
|
2162
2696
|
/** Evaluate a repository without reporting or executing configuration files. */
|
|
2163
|
-
const prepareLint = (options) => {
|
|
2164
|
-
|
|
2697
|
+
const prepareLint = (options, dependencies) => {
|
|
2698
|
+
const { paths, codecFor, createRepoContext } = dependencies;
|
|
2699
|
+
if (!options || !paths.isAbsolute(options.cwd)) throw new UsageError("cwd must be an absolute filesystem path.");
|
|
2165
2700
|
if ("customRules" in options || "reporters" in options) throw new UsageError("Pass customRules and reporters inside the config option.");
|
|
2166
2701
|
if (options.pm !== void 0 && !isPM(options.pm)) throw new UsageError(`Unknown package manager: ${String(options.pm)}`);
|
|
2167
2702
|
if (options.projectType !== void 0 && !isProjectType(options.projectType)) throw new UsageError(`Unknown project type: ${String(options.projectType)}`);
|
|
2703
|
+
if (options.pmVersion !== void 0 && (!options.pm || !isStableVersion(options.pmVersion))) throw new UsageError("pmVersion / --pm-version requires pm / --pm and an exact stable version such as 10.16.0.");
|
|
2704
|
+
if (options.workspaces !== void 0 && typeof options.workspaces !== "boolean") throw new UsageError("workspaces must be a boolean.");
|
|
2168
2705
|
const config = options.config === void 0 ? void 0 : parseConfig(options.config);
|
|
2169
|
-
const
|
|
2706
|
+
const fs = options.fs === void 0 ? dependencies.fileSystem : options.fs;
|
|
2707
|
+
const ctx = createRepoContext(options.cwd, fs, options.projectType ?? config?.projectType);
|
|
2170
2708
|
const pms = resolvePMs(ctx, {
|
|
2171
2709
|
allowed: config?.pms,
|
|
2172
2710
|
pmOverride: options.pm
|
|
2173
2711
|
});
|
|
2174
2712
|
if (pms.includes("deno") && !ctx.exists(asRelPath("deno.json")) && ctx.exists(asRelPath("deno.jsonc"))) throw new ConfigError("deno.jsonc is not supported. siro currently reads strict JSON from deno.json only.");
|
|
2175
|
-
const configured = applyConfig(rules, config);
|
|
2713
|
+
const configured = applyConfig(dependencies.rules, config);
|
|
2176
2714
|
return {
|
|
2177
2715
|
ctx,
|
|
2178
2716
|
pms,
|
|
2717
|
+
pmVersions: {
|
|
2718
|
+
...declaredPMVersion(ctx.packageJson?.packageManager),
|
|
2719
|
+
...config?.pmVersions,
|
|
2720
|
+
...options.pm && options.pmVersion ? { [options.pm]: options.pmVersion } : {}
|
|
2721
|
+
},
|
|
2722
|
+
members: options.workspaces ? collectWorkspaceMembers(ctx, fs, pms, options.projectType ?? config?.projectType, dependencies) : [],
|
|
2179
2723
|
ruleSet: configured.rules,
|
|
2180
2724
|
severityOverrides: configured.severityOverrides,
|
|
2181
2725
|
codecFor,
|
|
2182
2726
|
reporters: config?.reporters ?? []
|
|
2183
2727
|
};
|
|
2184
2728
|
};
|
|
2185
|
-
|
|
2729
|
+
/** Reuse the existing manifest checks; installation policy remains rooted at cwd. */
|
|
2730
|
+
const runPreparedLint = (prepared) => {
|
|
2731
|
+
const result = runLint(prepared);
|
|
2732
|
+
const findings = [...result.findings];
|
|
2733
|
+
const summary = { ...result.summary };
|
|
2734
|
+
const memberRules = prepared.ruleSet.filter((rule) => [
|
|
2735
|
+
"files-field",
|
|
2736
|
+
"publish-access",
|
|
2737
|
+
"unsupported-settings"
|
|
2738
|
+
].includes(rule.id));
|
|
2739
|
+
for (const member of prepared.members) {
|
|
2740
|
+
const child = runLint({
|
|
2741
|
+
...prepared,
|
|
2742
|
+
ctx: member.ctx,
|
|
2743
|
+
pms: [member.pm],
|
|
2744
|
+
ruleSet: memberRules
|
|
2745
|
+
});
|
|
2746
|
+
for (const finding of child.findings) findings.push({
|
|
2747
|
+
...finding,
|
|
2748
|
+
file: `${member.directory}/${finding.file ?? "package.json"}`,
|
|
2749
|
+
message: `${member.directory}: ${finding.message}`,
|
|
2750
|
+
remediation: finding.remediation?.kind === "manual" ? {
|
|
2751
|
+
kind: "manual",
|
|
2752
|
+
steps: [`Work in ${member.directory} for this finding.`, ...finding.remediation.steps]
|
|
2753
|
+
} : finding.remediation
|
|
2754
|
+
});
|
|
2755
|
+
for (const level of [
|
|
2756
|
+
"error",
|
|
2757
|
+
"warn",
|
|
2758
|
+
"info"
|
|
2759
|
+
]) summary[level] += child.summary[level];
|
|
2760
|
+
}
|
|
2761
|
+
return {
|
|
2762
|
+
findings,
|
|
2763
|
+
summary
|
|
2764
|
+
};
|
|
2765
|
+
};
|
|
2766
|
+
const lint$1 = (options, dependencies) => runPreparedLint(prepareLint(options, dependencies));
|
|
2767
|
+
//#endregion
|
|
2768
|
+
//#region src/domain/services/filter.ts
|
|
2769
|
+
const EXIT_SUCCESS = 0;
|
|
2770
|
+
const EXIT_FAILURE = 1;
|
|
2771
|
+
const meetsThreshold = (severity, threshold) => SEVERITY_RANK[severity] >= SEVERITY_RANK[threshold];
|
|
2772
|
+
/** Keep only findings at or above `threshold`, recomputing the summary. */
|
|
2773
|
+
const filterBySeverity = (result, threshold) => {
|
|
2774
|
+
const findings = [];
|
|
2775
|
+
const summary = {
|
|
2776
|
+
error: 0,
|
|
2777
|
+
info: 0,
|
|
2778
|
+
warn: 0
|
|
2779
|
+
};
|
|
2780
|
+
for (const finding of result.findings) if (meetsThreshold(finding.severity, threshold)) {
|
|
2781
|
+
findings.push(finding);
|
|
2782
|
+
summary[finding.severity] += 1;
|
|
2783
|
+
}
|
|
2784
|
+
return {
|
|
2785
|
+
findings,
|
|
2786
|
+
summary
|
|
2787
|
+
};
|
|
2788
|
+
};
|
|
2789
|
+
/**
|
|
2790
|
+
* Exit code for a lint run. Non-zero when any finding meets `threshold`
|
|
2791
|
+
* (default: only `error` fails the run).
|
|
2792
|
+
*/
|
|
2793
|
+
const exitCodeForLint = (result, threshold = "error") => {
|
|
2794
|
+
if (result.findings.some((finding) => meetsThreshold(finding.severity, threshold))) return EXIT_FAILURE;
|
|
2795
|
+
return EXIT_SUCCESS;
|
|
2796
|
+
};
|
|
2186
2797
|
//#endregion
|
|
2187
2798
|
//#region src/application/commands/lint.ts
|
|
2188
2799
|
/** Evaluate and report. Executable config loading belongs to the CLI adapter. */
|
|
2189
|
-
const lintCommand = async (options, io) => {
|
|
2800
|
+
const lintCommand$1 = async (options, io, dependencies, reporters) => {
|
|
2190
2801
|
if (!options) throw new UsageError("Lint options are required.");
|
|
2191
2802
|
if (options.severity !== void 0 && !isSeverity(options.severity)) throw new UsageError(`Invalid severity: ${String(options.severity)}`);
|
|
2192
|
-
const prepared = prepareLint(options);
|
|
2193
|
-
const registry = createRegistry(prepared.reporters);
|
|
2194
|
-
const selection = options.reporter ??
|
|
2803
|
+
const prepared = prepareLint(options, dependencies);
|
|
2804
|
+
const registry = reporters.createRegistry(prepared.reporters);
|
|
2805
|
+
const selection = options.reporter ?? reporters.defaultName;
|
|
2195
2806
|
const reporter = typeof selection === "string" ? registry.get(selection) : selection;
|
|
2196
2807
|
if (!isReporterShape(reporter)) throw new UsageError(`${typeof selection === "string" ? "Unknown" : "Invalid"} reporter: ${String(selection)} (available: ${[...registry.keys()].join(", ")})`);
|
|
2197
|
-
const result =
|
|
2808
|
+
const result = runPreparedLint(prepared);
|
|
2198
2809
|
const exitCode = exitCodeForLint(result, options.severity ?? "error");
|
|
2199
|
-
reporter.format(filterBySeverity(result, options.severity ?? "info"), io);
|
|
2810
|
+
await reporter.format(filterBySeverity(result, options.severity ?? "info"), io);
|
|
2200
2811
|
return exitCode;
|
|
2201
2812
|
};
|
|
2202
2813
|
//#endregion
|
|
2814
|
+
//#region src/domain/schemas/package-json.ts
|
|
2815
|
+
const PackageJsonSchema = vb.looseObject({
|
|
2816
|
+
files: vb.optional(vb.array(vb.string())),
|
|
2817
|
+
name: vb.optional(vb.string()),
|
|
2818
|
+
packageManager: vb.optional(vb.string()),
|
|
2819
|
+
private: vb.optional(vb.boolean()),
|
|
2820
|
+
publishConfig: vb.optional(vb.looseObject({ access: vb.optional(vb.picklist([
|
|
2821
|
+
"public",
|
|
2822
|
+
"restricted",
|
|
2823
|
+
"private"
|
|
2824
|
+
])) })),
|
|
2825
|
+
trustedDependencies: vb.optional(vb.array(vb.string()))
|
|
2826
|
+
});
|
|
2827
|
+
const parsePackageJson = (value) => {
|
|
2828
|
+
if (!isPlainRecord(value)) throw new ConfigError("package.json: expected an object at the root.");
|
|
2829
|
+
const result = vb.safeParse(PackageJsonSchema, value);
|
|
2830
|
+
if (result.success) return result.output;
|
|
2831
|
+
const issue = result.issues[0];
|
|
2832
|
+
const field = vb.getDotPath(issue);
|
|
2833
|
+
throw new ConfigError(`package.json: ${field ? `${field}: ` : ""}${issue.message}`);
|
|
2834
|
+
};
|
|
2835
|
+
//#endregion
|
|
2836
|
+
//#region src/adapters/repo-context.ts
|
|
2837
|
+
const tryParseJson = (text) => {
|
|
2838
|
+
try {
|
|
2839
|
+
return JSON.parse(text);
|
|
2840
|
+
} catch (error) {
|
|
2841
|
+
let errMsg = String(error);
|
|
2842
|
+
if (error instanceof Error) errMsg = error.message;
|
|
2843
|
+
throw new ConfigError(`package.json: invalid JSON — ${errMsg}`);
|
|
2844
|
+
}
|
|
2845
|
+
};
|
|
2846
|
+
const readPackageJson = (raw) => {
|
|
2847
|
+
return parsePackageJson(tryParseJson(raw.trim()));
|
|
2848
|
+
};
|
|
2849
|
+
const createRepoContext = (root, fs = nodeFileSystem, projectType) => {
|
|
2850
|
+
if (fs === nodeFileSystem) assertDirectory(root);
|
|
2851
|
+
const readText = (relPath) => fs.readText(resolveIn(root, relPath));
|
|
2852
|
+
const exists = (relPath) => fs.exists(resolveIn(root, relPath));
|
|
2853
|
+
const raw = readText(asRelPath("package.json"));
|
|
2854
|
+
let packageJson = void 0;
|
|
2855
|
+
if (typeof raw !== "undefined") packageJson = readPackageJson(raw);
|
|
2856
|
+
return {
|
|
2857
|
+
exists,
|
|
2858
|
+
packageJson,
|
|
2859
|
+
projectType,
|
|
2860
|
+
readText,
|
|
2861
|
+
root
|
|
2862
|
+
};
|
|
2863
|
+
};
|
|
2864
|
+
//#endregion
|
|
2865
|
+
//#region src/adapters/codecs/ini.ts
|
|
2866
|
+
const INTEGER_PATTERN = /^-?\d+$/u;
|
|
2867
|
+
const coerceBoolean = (str) => {
|
|
2868
|
+
if (str === "true") return true;
|
|
2869
|
+
if (str === "false") return false;
|
|
2870
|
+
};
|
|
2871
|
+
const coerceInteger = (str) => {
|
|
2872
|
+
if (INTEGER_PATTERN.test(str)) return Number(str);
|
|
2873
|
+
};
|
|
2874
|
+
const coerce = (raw) => {
|
|
2875
|
+
if (typeof raw !== "string") return raw;
|
|
2876
|
+
return coerceBoolean(raw) ?? coerceInteger(raw) ?? raw;
|
|
2877
|
+
};
|
|
2878
|
+
//#endregion
|
|
2879
|
+
//#region src/adapters/codecs/store.ts
|
|
2880
|
+
const CODECS = {
|
|
2881
|
+
json: { parse(text) {
|
|
2882
|
+
const trimmed = text.trim();
|
|
2883
|
+
return toParsedConfig(JSON.parse(trimmed));
|
|
2884
|
+
} },
|
|
2885
|
+
npmrc: { parse(text) {
|
|
2886
|
+
const raw = toParsedConfig(ini.parse(text));
|
|
2887
|
+
const config = {};
|
|
2888
|
+
for (const [key, value] of Object.entries(raw)) if (Array.isArray(value)) config[key] = value.map((elem) => coerce(elem));
|
|
2889
|
+
else if (typeof value === "object" && value !== null) config[key] = toParsedConfig(value);
|
|
2890
|
+
else config[key] = coerce(value);
|
|
2891
|
+
return config;
|
|
2892
|
+
} },
|
|
2893
|
+
toml: { parse(text) {
|
|
2894
|
+
if (text.trim() === "") return {};
|
|
2895
|
+
return toParsedConfig(parse$1(text.startsWith("") ? text.slice(1) : text));
|
|
2896
|
+
} },
|
|
2897
|
+
yaml: { parse(text) {
|
|
2898
|
+
if (text.trim() === "") return {};
|
|
2899
|
+
const document = parseDocument(text);
|
|
2900
|
+
if (document.errors.length > 0) throw document.errors[0];
|
|
2901
|
+
if (document.contents === null) return {};
|
|
2902
|
+
return toParsedConfig(document.toJS({ onAnchor: (value, count) => {
|
|
2903
|
+
if (count > 1) JSON.stringify(value);
|
|
2904
|
+
} }));
|
|
2905
|
+
} }
|
|
2906
|
+
};
|
|
2907
|
+
/** Look up the codec for a parseable kind. Total — every CodecKind has one. */
|
|
2908
|
+
const codecFor = (kind) => CODECS[kind];
|
|
2909
|
+
//#endregion
|
|
2910
|
+
//#region src/adapters/reporters/github.ts
|
|
2911
|
+
const COMMAND = {
|
|
2912
|
+
error: "error",
|
|
2913
|
+
info: "notice",
|
|
2914
|
+
warn: "warning"
|
|
2915
|
+
};
|
|
2916
|
+
const escapeData = (raw) => raw.replaceAll("%", "%25").replaceAll("\r", "%0D").replaceAll("\n", "%0A");
|
|
2917
|
+
const escapeProp = (raw) => escapeData(raw).replaceAll(":", "%3A").replaceAll(",", "%2C");
|
|
2918
|
+
/** Emit GitHub Actions workflow commands (annotations on PRs). */
|
|
2919
|
+
const githubReporter = {
|
|
2920
|
+
format(result, io) {
|
|
2921
|
+
for (const finding of result.findings) {
|
|
2922
|
+
let file = "";
|
|
2923
|
+
if (finding.file) file = `file=${escapeProp(finding.file)},`;
|
|
2924
|
+
let body = `[${finding.pm}] ${finding.message}`;
|
|
2925
|
+
if (finding.docs) body += ` (${finding.docs})`;
|
|
2926
|
+
io.stdout(`::${COMMAND[finding.severity]} ${file}title=${escapeProp(finding.ruleId)}::${escapeData(body)}`);
|
|
2927
|
+
}
|
|
2928
|
+
},
|
|
2929
|
+
name: "github"
|
|
2930
|
+
};
|
|
2931
|
+
//#endregion
|
|
2932
|
+
//#region src/adapters/reporters/json.ts
|
|
2933
|
+
/**
|
|
2934
|
+
* Versioned machine-readable output — the public contract consumed by
|
|
2935
|
+
* external fixers (see docs/json-output.md). Bump `schemaVersion` on any
|
|
2936
|
+
* breaking shape change and update that doc in the same commit.
|
|
2937
|
+
*/
|
|
2938
|
+
const SCHEMA_VERSION = 2;
|
|
2939
|
+
const jsonReporter = {
|
|
2940
|
+
format(result, io) {
|
|
2941
|
+
io.stdout(JSON.stringify({
|
|
2942
|
+
schemaVersion: SCHEMA_VERSION,
|
|
2943
|
+
siroVersion: version,
|
|
2944
|
+
findings: result.findings,
|
|
2945
|
+
summary: result.summary
|
|
2946
|
+
}, void 0, 2));
|
|
2947
|
+
},
|
|
2948
|
+
name: "json"
|
|
2949
|
+
};
|
|
2950
|
+
//#endregion
|
|
2951
|
+
//#region src/adapters/reporters/pretty.ts
|
|
2952
|
+
const GLYPH = {
|
|
2953
|
+
error: "✖ error",
|
|
2954
|
+
info: "ℹ info",
|
|
2955
|
+
warn: "⚠ warn"
|
|
2956
|
+
};
|
|
2957
|
+
/**
|
|
2958
|
+
* Decide colour at call time, not at import time.
|
|
2959
|
+
*
|
|
2960
|
+
* picocolors snapshots `isColorSupported` when imported, so a later
|
|
2961
|
+
* `process.env.NO_COLOR = '1'` (or a different stdout) is ignored. The
|
|
2962
|
+
* reporter is invoked per `siro lint` run from many contexts (TTY, CI
|
|
2963
|
+
* with FORCE_COLOR, redirected output, …) — checking each call is the
|
|
2964
|
+
* only way NO_COLOR can truly win, as https://no-color.org/ requires.
|
|
2965
|
+
*/
|
|
2966
|
+
const colorSupportedNow = () => {
|
|
2967
|
+
const { env } = process;
|
|
2968
|
+
if (typeof env.NO_COLOR !== "undefined" && env.NO_COLOR !== "") return false;
|
|
2969
|
+
if ("FORCE_COLOR" in env) return env.FORCE_COLOR !== "0" && env.FORCE_COLOR !== "";
|
|
2970
|
+
return pc.isColorSupported;
|
|
2971
|
+
};
|
|
2972
|
+
const renderFinding = (finding, ctx) => {
|
|
2973
|
+
let where = "";
|
|
2974
|
+
if (finding.file) where = ctx.colors.dim(` (${finding.file})`);
|
|
2975
|
+
ctx.io.stdout(`${ctx.tag[finding.severity](GLYPH[finding.severity])} [${finding.pm}] ${ctx.colors.bold(finding.ruleId)}${where}`);
|
|
2976
|
+
ctx.io.stdout(` ${finding.message}`);
|
|
2977
|
+
for (const step of finding.remediation?.kind === "manual" ? finding.remediation.steps : []) ctx.io.stdout(` ↳ ${step}`);
|
|
2978
|
+
if (finding.docs) ctx.io.stdout(ctx.colors.dim(` → ${finding.docs}`));
|
|
2979
|
+
};
|
|
2980
|
+
const buildRenderCtx = (io) => {
|
|
2981
|
+
const colors = pc.createColors(colorSupportedNow());
|
|
2982
|
+
return {
|
|
2983
|
+
colors,
|
|
2984
|
+
io,
|
|
2985
|
+
tag: {
|
|
2986
|
+
error: colors.red,
|
|
2987
|
+
info: colors.cyan,
|
|
2988
|
+
warn: colors.yellow
|
|
2989
|
+
}
|
|
2990
|
+
};
|
|
2991
|
+
};
|
|
2992
|
+
const prettyReporter = {
|
|
2993
|
+
format(result, io) {
|
|
2994
|
+
const ctx = buildRenderCtx(io);
|
|
2995
|
+
if (result.findings.length === 0) {
|
|
2996
|
+
io.stdout(ctx.colors.green("✔ No security best-practice issues found."));
|
|
2997
|
+
return;
|
|
2998
|
+
}
|
|
2999
|
+
for (const finding of result.findings) renderFinding(finding, ctx);
|
|
3000
|
+
const { error, warn, info } = result.summary;
|
|
3001
|
+
io.stdout("");
|
|
3002
|
+
io.stdout(`Summary: ${error} error, ${warn} warn, ${info} info`);
|
|
3003
|
+
},
|
|
3004
|
+
name: "pretty"
|
|
3005
|
+
};
|
|
3006
|
+
//#endregion
|
|
3007
|
+
//#region src/adapters/reporters/registry.ts
|
|
3008
|
+
const BUILTINS = [
|
|
3009
|
+
prettyReporter,
|
|
3010
|
+
jsonReporter,
|
|
3011
|
+
githubReporter
|
|
3012
|
+
];
|
|
3013
|
+
const DEFAULT_REPORTER_NAME = prettyReporter.name;
|
|
3014
|
+
const JSON_REPORTER_NAME = jsonReporter.name;
|
|
3015
|
+
/** Names of every built-in reporter, in stable display order. */
|
|
3016
|
+
const BUILTIN_REPORTER_NAMES = BUILTINS.map((rep) => rep.name);
|
|
3017
|
+
/** Later registrations replace earlier reporters with the same name. */
|
|
3018
|
+
const createRegistry = (extras = []) => new Map([...BUILTINS, ...extras].map((reporter) => [reporter.name, reporter]));
|
|
3019
|
+
//#endregion
|
|
3020
|
+
//#region src/composition/lint.ts
|
|
3021
|
+
const dependencies = {
|
|
3022
|
+
rules,
|
|
3023
|
+
fileSystem: nodeFileSystem,
|
|
3024
|
+
paths: nodePaths,
|
|
3025
|
+
createRepoContext,
|
|
3026
|
+
codecFor,
|
|
3027
|
+
globs: minimatchGlobs,
|
|
3028
|
+
caseInsensitiveGlobs: process.platform === "darwin" || process.platform === "win32"
|
|
3029
|
+
};
|
|
3030
|
+
/** Public Node API: callers can replace the filesystem without assembling the application. */
|
|
3031
|
+
const lint = (options) => lint$1(options, dependencies);
|
|
3032
|
+
const lintCommand = (options, io) => lintCommand$1(options, io, dependencies, {
|
|
3033
|
+
defaultName: DEFAULT_REPORTER_NAME,
|
|
3034
|
+
createRegistry
|
|
3035
|
+
});
|
|
3036
|
+
//#endregion
|
|
2203
3037
|
//#region src/adapters/node-io.ts
|
|
2204
3038
|
const nodeIO = {
|
|
2205
3039
|
stderr: (line) => process.stderr.write(`${line}\n`),
|
|
2206
3040
|
stdout: (line) => process.stdout.write(`${line}\n`)
|
|
2207
3041
|
};
|
|
2208
3042
|
//#endregion
|
|
2209
|
-
export { ConfigError as A, SUPPORTED_NODE_RANGE as C, isNodeError as D, nodeFileSystem as E, UsageError as M, asAbsPath as O,
|
|
3043
|
+
export { ConfigError as A, SUPPORTED_NODE_RANGE as C, isNodeError as D, nodeFileSystem as E, UsageError as M, asAbsPath as O, isSupportedNodeVersion as S, assertDirectory as T, isProjectType as _, DEFAULT_REPORTER_NAME as a, isPM as b, jsonReporter as c, requireConfigKey as d, CONFIG_FILES as f, PROJECT_TYPES as g, getByPath as h, BUILTIN_REPORTER_NAMES as i, SiroError as j, asRelPath as k, githubReporter as l, defineRule as m, lint as n, JSON_REPORTER_NAME as o, loadConfig as p, lintCommand as r, prettyReporter as s, nodeIO as t, overrideBindings as u, PMS as v, version as w, isSeverity as x, SEVERITIES as y };
|
|
2210
3044
|
|
|
2211
|
-
//# sourceMappingURL=node-io-
|
|
3045
|
+
//# sourceMappingURL=node-io-BDHg4J2w.mjs.map
|