@pho9ubenaa/siro 0.4.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/README.md +13 -3
- package/dist/cli.mjs +13 -2
- package/dist/index.d.mts +42 -22
- package/dist/index.mjs +1 -1
- package/dist/{node-io-B3Hbcwyn.mjs → node-io-CaqobxYp.mjs} +958 -196
- package/package.json +5 -4
|
@@ -1,12 +1,14 @@
|
|
|
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 { lt, parse } from "semver";
|
|
6
7
|
import pc from "picocolors";
|
|
7
8
|
import ini from "ini";
|
|
8
|
-
import { parse } from "smol-toml";
|
|
9
|
+
import { parse as parse$1 } from "smol-toml";
|
|
9
10
|
import { parseDocument } from "yaml";
|
|
11
|
+
import { GLOBSTAR, Minimatch, braceExpand } from "minimatch";
|
|
10
12
|
import valid from "semver/functions/valid.js";
|
|
11
13
|
//#region src/shared/errors.ts
|
|
12
14
|
var SiroError = class extends Error {
|
|
@@ -62,10 +64,34 @@ const asRelPath = (value) => {
|
|
|
62
64
|
const isNodeError = (value) => value instanceof Error && "code" in value && typeof value.code === "string";
|
|
63
65
|
//#endregion
|
|
64
66
|
//#region src/adapters/node-file-system.ts
|
|
67
|
+
const assertRegularFile = (filePath) => {
|
|
68
|
+
if (!statSync(filePath).isFile()) throw new ConfigError(`${filePath}: expected a regular file.`);
|
|
69
|
+
};
|
|
65
70
|
const nodeFileSystem = {
|
|
71
|
+
readDirectories(directory) {
|
|
72
|
+
return readdirSync(directory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
73
|
+
},
|
|
74
|
+
resolveDirectory(parent, name) {
|
|
75
|
+
let target;
|
|
76
|
+
try {
|
|
77
|
+
target = lstatSync(path.join(parent, name), { bigint: true });
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (isNodeError(error) && error.code === "ENOENT") return void 0;
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
if (!target.isDirectory()) return void 0;
|
|
83
|
+
const names = readdirSync(parent, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
84
|
+
if (names.includes(name)) return name;
|
|
85
|
+
const matches = names.filter((candidate) => {
|
|
86
|
+
const entry = lstatSync(path.join(parent, candidate), { bigint: true });
|
|
87
|
+
return entry.isDirectory() && entry.dev === target.dev && entry.ino === target.ino;
|
|
88
|
+
});
|
|
89
|
+
if (target.ino === 0n || matches.length !== 1) throw new ConfigError(`${parent}: cannot identify the resolved workspace directory ${name}.`);
|
|
90
|
+
return matches[0];
|
|
91
|
+
},
|
|
66
92
|
exists(filePath) {
|
|
67
93
|
try {
|
|
68
|
-
|
|
94
|
+
assertRegularFile(filePath);
|
|
69
95
|
return true;
|
|
70
96
|
} catch (error) {
|
|
71
97
|
if (isNodeError(error) && error.code === "ENOENT") return false;
|
|
@@ -74,6 +100,7 @@ const nodeFileSystem = {
|
|
|
74
100
|
},
|
|
75
101
|
readText(filePath) {
|
|
76
102
|
try {
|
|
103
|
+
assertRegularFile(filePath);
|
|
77
104
|
return readFileSync(filePath, "utf8");
|
|
78
105
|
} catch (error) {
|
|
79
106
|
if (isNodeError(error) && error.code === "ENOENT") return;
|
|
@@ -87,7 +114,7 @@ const assertDirectory = (root) => {
|
|
|
87
114
|
};
|
|
88
115
|
//#endregion
|
|
89
116
|
//#region package.json
|
|
90
|
-
var version$1 = "0.
|
|
117
|
+
var version$1 = "0.5.0";
|
|
91
118
|
//#endregion
|
|
92
119
|
//#region src/shared/node-version.ts
|
|
93
120
|
const SUPPORTED_NODE_RANGE = { "node": "^22.18.0 || ^24.0.0" }.node;
|
|
@@ -186,6 +213,11 @@ const isDenseStringArray = (value) => Array.isArray(value) && Array.from(value).
|
|
|
186
213
|
const isCheckStatusShape = (value) => {
|
|
187
214
|
if (!isPlainRecord(value)) return false;
|
|
188
215
|
if (value.state === "ok" || value.state === "na") return true;
|
|
216
|
+
if (value.state === "violations") return Array.isArray(value.violations) && value.violations.length > 0 && Array.from(value.violations).every(isViolationStatusShape);
|
|
217
|
+
return isViolationStatusShape(value);
|
|
218
|
+
};
|
|
219
|
+
const isViolationStatusShape = (value) => {
|
|
220
|
+
if (!isPlainRecord(value)) return false;
|
|
189
221
|
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
222
|
};
|
|
191
223
|
const isSetKeyOperation = (value) => isPlainRecord(value) && value.op === "setKey" && isConfigFileRefShape(value.file) && isDenseStringArray(value.keyPath) && value.keyPath.length > 0 && isConfigValueShape(value.value);
|
|
@@ -207,12 +239,26 @@ const isRuleShape = (value) => {
|
|
|
207
239
|
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
240
|
};
|
|
209
241
|
//#endregion
|
|
242
|
+
//#region src/domain/services/pm-versions.ts
|
|
243
|
+
/** Release history establishes stable versions, not ranges or prerelease builds. */
|
|
244
|
+
const isStableVersion = (value) => {
|
|
245
|
+
if (typeof value !== "string" || !/^\d/u.test(value) || value.trim() !== value) return false;
|
|
246
|
+
const version = parse(value);
|
|
247
|
+
return version !== null && version.prerelease.length === 0;
|
|
248
|
+
};
|
|
249
|
+
/** Keep name-only detection independent from whether the declaration pins a version. */
|
|
250
|
+
const declaredPMVersion = (declaration) => {
|
|
251
|
+
const [pm, version, ...extra] = declaration?.trim().split("@") ?? [];
|
|
252
|
+
return pm && isPM(pm) && isStableVersion(version) && extra.length === 0 ? { [pm]: version } : {};
|
|
253
|
+
};
|
|
254
|
+
//#endregion
|
|
210
255
|
//#region src/application/config.ts
|
|
211
256
|
const RuleSettingSchema = vb.union([vb.picklist(SEVERITIES), vb.literal("off")]);
|
|
212
257
|
const ConfigSchema = vb.strictObject({
|
|
213
258
|
customRules: vb.optional(vb.array(vb.custom(isRuleShape, "must be a structurally valid rule"))),
|
|
214
259
|
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
260
|
projectType: vb.optional(vb.picklist(PROJECT_TYPES)),
|
|
261
|
+
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
262
|
reporters: vb.optional(vb.array(vb.custom(isReporterShape, "must be a { name, format } reporter"))),
|
|
217
263
|
rules: vb.optional(vb.pipe(vb.custom(isPlainRecord, "must be an object of rule settings"), vb.rawTransform(({ dataset, addIssue }) => {
|
|
218
264
|
const entries = [];
|
|
@@ -412,6 +458,105 @@ const BUILTIN_REPORTER_NAMES = BUILTINS.map((rep) => rep.name);
|
|
|
412
458
|
/** Later registrations replace earlier reporters with the same name. */
|
|
413
459
|
const createRegistry = (extras = []) => new Map([...BUILTINS, ...extras].map((reporter) => [reporter.name, reporter]));
|
|
414
460
|
//#endregion
|
|
461
|
+
//#region src/domain/entities/config-files.ts
|
|
462
|
+
/** Known configuration locations and their parsers. */
|
|
463
|
+
const CONFIG_FILES = {
|
|
464
|
+
aubeWorkspace: {
|
|
465
|
+
kind: "yaml",
|
|
466
|
+
path: asRelPath("aube-workspace.yaml")
|
|
467
|
+
},
|
|
468
|
+
bunfig: {
|
|
469
|
+
kind: "toml",
|
|
470
|
+
path: asRelPath("bunfig.toml")
|
|
471
|
+
},
|
|
472
|
+
denoJson: {
|
|
473
|
+
kind: "json",
|
|
474
|
+
path: asRelPath("deno.json")
|
|
475
|
+
},
|
|
476
|
+
npmrc: {
|
|
477
|
+
kind: "npmrc",
|
|
478
|
+
path: asRelPath(".npmrc")
|
|
479
|
+
},
|
|
480
|
+
packageJson: {
|
|
481
|
+
kind: "json",
|
|
482
|
+
path: asRelPath("package.json")
|
|
483
|
+
},
|
|
484
|
+
pnpmWorkspace: {
|
|
485
|
+
kind: "yaml",
|
|
486
|
+
path: asRelPath("pnpm-workspace.yaml")
|
|
487
|
+
},
|
|
488
|
+
yarnrc: {
|
|
489
|
+
kind: "yaml",
|
|
490
|
+
path: asRelPath(".yarnrc.yml")
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
//#endregion
|
|
494
|
+
//#region src/domain/services/parse-config-file.ts
|
|
495
|
+
const EMPTY_FILE = Object.freeze({});
|
|
496
|
+
/** Create a fresh parser and cache for one lint command. */
|
|
497
|
+
const createConfigParser = (codecFor, ctx) => {
|
|
498
|
+
const cache = /* @__PURE__ */ new Map();
|
|
499
|
+
return (file) => {
|
|
500
|
+
if (file === void 0) return EMPTY_FILE;
|
|
501
|
+
const cacheKey = `${file.kind}:${file.path}`;
|
|
502
|
+
const cached = cache.get(cacheKey);
|
|
503
|
+
if (cached !== void 0) return cached;
|
|
504
|
+
const text = ctx.readText(file.path);
|
|
505
|
+
if (text === void 0) {
|
|
506
|
+
cache.set(cacheKey, EMPTY_FILE);
|
|
507
|
+
return EMPTY_FILE;
|
|
508
|
+
}
|
|
509
|
+
const parsed = wrapCodecError(file.path, () => codecFor(file.kind).parse(text));
|
|
510
|
+
cache.set(cacheKey, parsed);
|
|
511
|
+
return parsed;
|
|
512
|
+
};
|
|
513
|
+
};
|
|
514
|
+
//#endregion
|
|
515
|
+
//#region src/adapters/codecs/ini.ts
|
|
516
|
+
const INTEGER_PATTERN = /^-?\d+$/u;
|
|
517
|
+
const coerceBoolean = (str) => {
|
|
518
|
+
if (str === "true") return true;
|
|
519
|
+
if (str === "false") return false;
|
|
520
|
+
};
|
|
521
|
+
const coerceInteger = (str) => {
|
|
522
|
+
if (INTEGER_PATTERN.test(str)) return Number(str);
|
|
523
|
+
};
|
|
524
|
+
const coerce = (raw) => {
|
|
525
|
+
if (typeof raw !== "string") return raw;
|
|
526
|
+
return coerceBoolean(raw) ?? coerceInteger(raw) ?? raw;
|
|
527
|
+
};
|
|
528
|
+
//#endregion
|
|
529
|
+
//#region src/adapters/codecs/store.ts
|
|
530
|
+
const CODECS = {
|
|
531
|
+
json: { parse(text) {
|
|
532
|
+
const trimmed = text.trim();
|
|
533
|
+
return toParsedConfig(JSON.parse(trimmed));
|
|
534
|
+
} },
|
|
535
|
+
npmrc: { parse(text) {
|
|
536
|
+
const raw = toParsedConfig(ini.parse(text));
|
|
537
|
+
const config = {};
|
|
538
|
+
for (const [key, value] of Object.entries(raw)) if (Array.isArray(value)) config[key] = value.map((elem) => coerce(elem));
|
|
539
|
+
else if (typeof value === "object" && value !== null) config[key] = toParsedConfig(value);
|
|
540
|
+
else config[key] = coerce(value);
|
|
541
|
+
return config;
|
|
542
|
+
} },
|
|
543
|
+
toml: { parse(text) {
|
|
544
|
+
if (text.trim() === "") return {};
|
|
545
|
+
return toParsedConfig(parse$1(text.replace(/^\uFEFF/u, "")));
|
|
546
|
+
} },
|
|
547
|
+
yaml: { parse(text) {
|
|
548
|
+
if (text.trim() === "") return {};
|
|
549
|
+
const document = parseDocument(text);
|
|
550
|
+
if (document.errors.length > 0) throw document.errors[0];
|
|
551
|
+
if (document.contents === null) return {};
|
|
552
|
+
return toParsedConfig(document.toJS({ onAnchor: (value, count) => {
|
|
553
|
+
if (count > 1) JSON.stringify(value);
|
|
554
|
+
} }));
|
|
555
|
+
} }
|
|
556
|
+
};
|
|
557
|
+
/** Look up the codec for a parseable kind. Total — every CodecKind has one. */
|
|
558
|
+
const codecFor = (kind) => CODECS[kind];
|
|
559
|
+
//#endregion
|
|
415
560
|
//#region src/domain/schemas/package-json.ts
|
|
416
561
|
const PackageJsonSchema = vb.looseObject({
|
|
417
562
|
files: vb.optional(vb.array(vb.string())),
|
|
@@ -463,50 +608,530 @@ const createRepoContext = (root, fs = nodeFileSystem, projectType) => {
|
|
|
463
608
|
};
|
|
464
609
|
};
|
|
465
610
|
//#endregion
|
|
466
|
-
//#region src/
|
|
467
|
-
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
|
|
611
|
+
//#region src/application/workspace-prefix.ts
|
|
612
|
+
/** Anchor Deno's native literal prefix before applying its glob suffix. */
|
|
613
|
+
const anchorWorkspacePrefix = (pattern, glob, resolveChild) => {
|
|
614
|
+
const parts = pattern.split("/");
|
|
615
|
+
const firstWildcard = parts.findIndex((part) => /[*?]/u.test(part));
|
|
616
|
+
const prefix = parts.slice(0, firstWildcard < 0 ? parts.length : firstWildcard);
|
|
617
|
+
if (prefix.length === 0 || pattern === ".") return glob;
|
|
618
|
+
const declared = prefix.join("/");
|
|
619
|
+
let resolved;
|
|
620
|
+
let attempted = false;
|
|
621
|
+
const resolve = () => {
|
|
622
|
+
if (!attempted) {
|
|
623
|
+
attempted = true;
|
|
624
|
+
let current = ".";
|
|
625
|
+
for (const part of prefix) {
|
|
626
|
+
const child = resolveChild(current, part);
|
|
627
|
+
if (child === void 0) return void 0;
|
|
628
|
+
current = current === "." ? child : `${current}/${child}`;
|
|
629
|
+
}
|
|
630
|
+
resolved = current;
|
|
631
|
+
}
|
|
632
|
+
return resolved;
|
|
633
|
+
};
|
|
634
|
+
const rewrite = (directory, actual) => directory === actual ? declared : directory.startsWith(`${actual}/`) ? declared + directory.slice(actual.length) : void 0;
|
|
635
|
+
return {
|
|
636
|
+
matches(directory) {
|
|
637
|
+
if (directory === ".") return false;
|
|
638
|
+
const actual = resolve();
|
|
639
|
+
const rewritten = actual === void 0 ? void 0 : rewrite(directory, actual);
|
|
640
|
+
return rewritten !== void 0 && glob.matches(rewritten);
|
|
641
|
+
},
|
|
642
|
+
canDescend(directory) {
|
|
643
|
+
const actual = resolve();
|
|
644
|
+
if (actual === void 0) return false;
|
|
645
|
+
if (directory === "." || actual.startsWith(`${directory}/`)) return true;
|
|
646
|
+
const rewritten = rewrite(directory, actual);
|
|
647
|
+
return rewritten !== void 0 && glob.canDescend(rewritten);
|
|
648
|
+
}
|
|
649
|
+
};
|
|
471
650
|
};
|
|
472
|
-
|
|
473
|
-
|
|
651
|
+
//#endregion
|
|
652
|
+
//#region src/application/workspace-globs.ts
|
|
653
|
+
const MAX_WORKSPACE_GLOB_ALTERNATIVES = 8192;
|
|
654
|
+
const BRACE_EXPANSION_PROBE_LIMIT = 8193;
|
|
655
|
+
const defaultWorkspaceGlobOptions = {
|
|
656
|
+
platform: "linux",
|
|
657
|
+
nocase: process.platform === "darwin" || process.platform === "win32",
|
|
658
|
+
windowsPathsNoEscape: true,
|
|
659
|
+
nonegate: true,
|
|
660
|
+
nocomment: true,
|
|
661
|
+
optimizationLevel: 2
|
|
662
|
+
};
|
|
663
|
+
const boundedBraceExpand = (pattern) => {
|
|
664
|
+
const alternatives = braceExpand(pattern, { braceExpandMax: BRACE_EXPANSION_PROBE_LIMIT });
|
|
665
|
+
if (alternatives.length > MAX_WORKSPACE_GLOB_ALTERNATIVES) throw new ConfigError(`Workspace pattern expands beyond ${MAX_WORKSPACE_GLOB_ALTERNATIVES} alternatives.`);
|
|
666
|
+
return alternatives;
|
|
667
|
+
};
|
|
668
|
+
/** Expand with minimatch's own bounded brace semantics. */
|
|
669
|
+
const expandWorkspaceGlob = (pattern) => {
|
|
670
|
+
try {
|
|
671
|
+
return boundedBraceExpand(pattern);
|
|
672
|
+
} catch (error) {
|
|
673
|
+
if (error instanceof TypeError) throw new ConfigError(`Invalid workspace pattern: ${error.message}.`);
|
|
674
|
+
throw error;
|
|
675
|
+
}
|
|
474
676
|
};
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
677
|
+
/** Compile once so membership, exclusions, and traversal share glob semantics. */
|
|
678
|
+
const compileWorkspaceGlob = (pattern, options = defaultWorkspaceGlobOptions) => {
|
|
679
|
+
let matcher;
|
|
680
|
+
try {
|
|
681
|
+
if (!options.nobrace) boundedBraceExpand(pattern);
|
|
682
|
+
matcher = new Minimatch(pattern, {
|
|
683
|
+
...options,
|
|
684
|
+
braceExpandMax: BRACE_EXPANSION_PROBE_LIMIT
|
|
685
|
+
});
|
|
686
|
+
} catch (error) {
|
|
687
|
+
if (error instanceof TypeError) throw new ConfigError(`Invalid workspace pattern: ${error.message}.`);
|
|
688
|
+
throw error;
|
|
689
|
+
}
|
|
690
|
+
return {
|
|
691
|
+
matches: (directory) => matcher.match(directory),
|
|
692
|
+
canDescend(directory) {
|
|
693
|
+
const parts = directory.split("/");
|
|
694
|
+
return matcher.set.some((alternative) => (alternative.includes(GLOBSTAR) || parts.length < alternative.length) && matcher.matchOne(parts, alternative, true));
|
|
695
|
+
}
|
|
696
|
+
};
|
|
478
697
|
};
|
|
479
698
|
//#endregion
|
|
480
|
-
//#region src/
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
699
|
+
//#region src/application/workspace-dialects.ts
|
|
700
|
+
const matchesCrossPathGlob = (pattern, value) => {
|
|
701
|
+
const patternCharacters = [...pattern];
|
|
702
|
+
const valueCharacters = [...value];
|
|
703
|
+
let patternIndex = 0;
|
|
704
|
+
let valueIndex = 0;
|
|
705
|
+
let starIndex = -1;
|
|
706
|
+
let retryIndex = 0;
|
|
707
|
+
while (valueIndex < valueCharacters.length) {
|
|
708
|
+
const character = patternCharacters[patternIndex];
|
|
709
|
+
if (character === "?" || character === valueCharacters[valueIndex]) {
|
|
710
|
+
patternIndex += 1;
|
|
711
|
+
valueIndex += 1;
|
|
712
|
+
} else if (character === "*") {
|
|
713
|
+
starIndex = patternIndex;
|
|
714
|
+
retryIndex = valueIndex;
|
|
715
|
+
patternIndex += 1;
|
|
716
|
+
} else if (starIndex >= 0) {
|
|
717
|
+
patternIndex = starIndex + 1;
|
|
718
|
+
retryIndex += 1;
|
|
719
|
+
valueIndex = retryIndex;
|
|
720
|
+
} else return false;
|
|
721
|
+
}
|
|
722
|
+
while (patternCharacters[patternIndex] === "*") patternIndex += 1;
|
|
723
|
+
return patternIndex === patternCharacters.length;
|
|
724
|
+
};
|
|
725
|
+
/** The bounded glob subset verified against the recorded Deno/Aube sources. */
|
|
726
|
+
const compileAdditionalWorkspaceGlob = (pattern, pm, excluded) => {
|
|
727
|
+
const parts = pattern.split("/");
|
|
728
|
+
if (parts.some((part) => part.includes("**") && part !== "**")) throw new ConfigError(`${pm}: ** must occupy an entire workspace path component.`);
|
|
729
|
+
if (pm === "aube" && (/[[\]{}()]/u.test(pattern) || pattern.includes("**") && (parts.at(-1) !== "**" || /[*?]/u.test(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.");
|
|
730
|
+
const escaped = pm === "deno" ? pattern.replace(/[[\]]/gu, (character) => character === "[" ? "[[]" : "[]]") : pattern;
|
|
731
|
+
const matcher = compileWorkspaceGlob(escaped, {
|
|
732
|
+
platform: "linux",
|
|
733
|
+
windowsPathsNoEscape: true,
|
|
734
|
+
nonegate: true,
|
|
735
|
+
nocomment: true,
|
|
736
|
+
nobrace: true,
|
|
737
|
+
noext: true,
|
|
738
|
+
dot: pm === "aube",
|
|
739
|
+
nocase: pm === "deno" && (/[*?]/u.test(pattern) || process.platform === "darwin" || process.platform === "win32")
|
|
740
|
+
});
|
|
741
|
+
if (pm === "deno" && /[*?]/u.test(pattern)) {
|
|
742
|
+
const files = compileWorkspaceGlob(`${escaped}/package.json`, {
|
|
743
|
+
platform: "linux",
|
|
744
|
+
windowsPathsNoEscape: true,
|
|
745
|
+
nonegate: true,
|
|
746
|
+
nocomment: true,
|
|
747
|
+
nobrace: true,
|
|
748
|
+
noext: true,
|
|
749
|
+
nocase: true
|
|
750
|
+
});
|
|
751
|
+
return {
|
|
752
|
+
...matcher,
|
|
753
|
+
matches: (directory) => files.matches(directory === "." ? "package.json" : `${directory}/package.json`)
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
if (pm !== "aube" || !excluded) return matcher;
|
|
757
|
+
const self = pattern.endsWith("/**") ? pattern.slice(0, -3) : void 0;
|
|
758
|
+
return {
|
|
759
|
+
...matcher,
|
|
760
|
+
matches: (directory) => matchesCrossPathGlob(pattern, directory) || directory === self
|
|
761
|
+
};
|
|
762
|
+
};
|
|
763
|
+
//#endregion
|
|
764
|
+
//#region src/application/workspaces.ts
|
|
765
|
+
/** npm cancels an earlier exclusion when a later positive pattern matches it. */
|
|
766
|
+
const splitNpmPattern = (raw) => {
|
|
767
|
+
const prefix = /^!+/u.exec(raw)?.[0] ?? "";
|
|
768
|
+
return {
|
|
769
|
+
excluded: prefix.length % 2 === 1,
|
|
770
|
+
pattern: raw.slice(prefix.length)
|
|
771
|
+
};
|
|
772
|
+
};
|
|
773
|
+
const npmPatterns = (patterns) => {
|
|
774
|
+
const positive = [];
|
|
775
|
+
let negative = [];
|
|
776
|
+
for (const raw of patterns) {
|
|
777
|
+
const parsed = splitNpmPattern(raw);
|
|
778
|
+
const pattern = parsed.pattern.replace(/^\.?\/+/u, "");
|
|
779
|
+
if (parsed.excluded) negative.push({
|
|
780
|
+
pattern,
|
|
781
|
+
glob: compileWorkspaceGlob(pattern, {})
|
|
782
|
+
});
|
|
783
|
+
else {
|
|
784
|
+
for (let index = 0; index < negative.length; index += 1) if (negative[index]?.glob.matches(pattern)) negative.splice(index, 1);
|
|
785
|
+
positive.push(pattern);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
return [...positive, ...negative.map(({ pattern }) => `!${pattern}`)];
|
|
789
|
+
};
|
|
790
|
+
/** Read declaration sources separately so Deno's two manifest sets stay distinct. */
|
|
791
|
+
const workspaceDefinitions = (ctx, pm) => {
|
|
792
|
+
const parse = createConfigParser(codecFor, ctx);
|
|
793
|
+
const validate = (value, source, denoManifests = false) => {
|
|
794
|
+
if (value === void 0) return {
|
|
795
|
+
patterns: [],
|
|
796
|
+
denoManifests
|
|
797
|
+
};
|
|
798
|
+
if (!Array.isArray(value) || !Array.from(value).every((item) => typeof item === "string")) throw new ConfigError(`${source}: expected an array of directory patterns.`);
|
|
799
|
+
for (const pattern of value) {
|
|
800
|
+
const positive = pm === "npm" ? splitNpmPattern(pattern).pattern : pattern.startsWith("!") ? pattern.slice(1) : pattern;
|
|
801
|
+
if ((pm === "deno" || pm === "aube" ? [positive] : expandWorkspaceGlob(positive)).some((alternative) => !isRelPath(alternative) || alternative.includes("\\") || alternative.startsWith("!"))) throw new ConfigError(`${source}: use relative directory patterns without parent traversal: ${JSON.stringify(pattern)}.`);
|
|
802
|
+
if (pm === "deno" && denoManifests && !pattern.startsWith("!") && path.posix.normalize(positive) === ".") throw new ConfigError(`${source}: a Deno workspace cannot contain itself.`);
|
|
803
|
+
}
|
|
804
|
+
return {
|
|
805
|
+
patterns: pm === "npm" ? npmPatterns(value) : value,
|
|
806
|
+
denoManifests
|
|
807
|
+
};
|
|
808
|
+
};
|
|
809
|
+
const packageDefinition = () => {
|
|
810
|
+
let value = ctx.packageJson?.workspaces;
|
|
811
|
+
if (isPlainRecord(value)) {
|
|
812
|
+
value = value.packages;
|
|
813
|
+
if (value === void 0) throw new ConfigError("package.json#workspaces: expected a packages array.");
|
|
814
|
+
}
|
|
815
|
+
return validate(value, "package.json#workspaces");
|
|
816
|
+
};
|
|
817
|
+
if (pm === "pnpm") return [validate(ctx.exists(CONFIG_FILES.pnpmWorkspace.path) ? parse(CONFIG_FILES.pnpmWorkspace).packages : void 0, "pnpm-workspace.yaml#packages")];
|
|
818
|
+
if (pm === "aube") {
|
|
819
|
+
const file = [CONFIG_FILES.aubeWorkspace, CONFIG_FILES.pnpmWorkspace].find((candidate) => ctx.exists(candidate.path));
|
|
820
|
+
return [file ? validate(parse(file).packages, `${file.path}#packages`) : packageDefinition()];
|
|
821
|
+
}
|
|
822
|
+
if (pm === "deno") return [validate(parse(CONFIG_FILES.denoJson).workspace, "deno.json#workspace", true), packageDefinition()];
|
|
823
|
+
return [packageDefinition()];
|
|
824
|
+
};
|
|
825
|
+
/** Traverse only ordinary directories that can fall under a positive pattern. */
|
|
826
|
+
const workspaceDirectories = (ctx, fs, definition, pm) => {
|
|
827
|
+
const { patterns } = definition;
|
|
828
|
+
const skipVendor = pm === "deno" && createConfigParser(codecFor, ctx)(CONFIG_FILES.denoJson).vendor === true;
|
|
829
|
+
const positive = patterns.filter((pattern) => !pattern.startsWith("!")).map((pattern) => path.posix.normalize(pattern).replace(/\/+$/u, ""));
|
|
830
|
+
const negative = patterns.filter((pattern) => pattern.startsWith("!")).map((pattern) => path.posix.normalize(pattern.slice(1)).replace(/\/+$/u, ""));
|
|
831
|
+
const directoryCache = /* @__PURE__ */ new Map();
|
|
832
|
+
const readDirectories = (directory) => {
|
|
833
|
+
const cached = directoryCache.get(directory);
|
|
834
|
+
if (cached) return cached;
|
|
835
|
+
if (!fs.readDirectories) throw new UsageError("Workspace discovery requires FileSystem.readDirectories; no host filesystem fallback is used.");
|
|
836
|
+
const names = fs.readDirectories(resolveIn(ctx.root, asRelPath(directory)));
|
|
837
|
+
if (!Array.isArray(names)) throw new ConfigError("FileSystem.readDirectories must return an array of directory names.");
|
|
838
|
+
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.");
|
|
839
|
+
const result = [...new Set(names)].sort();
|
|
840
|
+
directoryCache.set(directory, result);
|
|
841
|
+
return result;
|
|
842
|
+
};
|
|
843
|
+
const resolveChild = (parent, name) => {
|
|
844
|
+
if (name === ".git" || name === "node_modules") return void 0;
|
|
845
|
+
const names = readDirectories(parent);
|
|
846
|
+
const resolved = names.includes(name) ? name : fs.resolveDirectory?.(resolveIn(ctx.root, asRelPath(parent)), name);
|
|
847
|
+
if (resolved !== void 0 && !names.includes(resolved)) throw new ConfigError("FileSystem.resolveDirectory must return an enumerated ordinary child directory name.");
|
|
848
|
+
return resolved === ".git" || resolved === "node_modules" ? void 0 : resolved;
|
|
849
|
+
};
|
|
850
|
+
const compile = (pattern, excluded = false) => {
|
|
851
|
+
if (pm !== "deno" && pm !== "aube") return compileWorkspaceGlob(pattern, pm === "npm" ? {
|
|
852
|
+
...defaultWorkspaceGlobOptions,
|
|
853
|
+
nocomment: false
|
|
854
|
+
} : defaultWorkspaceGlobOptions);
|
|
855
|
+
const glob = compileAdditionalWorkspaceGlob(pattern, pm, excluded);
|
|
856
|
+
return pm === "deno" && !excluded ? anchorWorkspacePrefix(pattern, glob, resolveChild) : glob;
|
|
857
|
+
};
|
|
858
|
+
const included = positive.map((pattern) => compile(pattern));
|
|
859
|
+
const excluded = negative.map((pattern) => {
|
|
860
|
+
return {
|
|
861
|
+
glob: compile(pattern, true),
|
|
862
|
+
subtree: pm === "deno" || pm === "aube" || pm === "bun" ? void 0 : pattern === "**" ? { matches: () => true } : pattern.endsWith("/**") ? compile(pattern.slice(0, -3), true) : void 0
|
|
863
|
+
};
|
|
864
|
+
});
|
|
865
|
+
const declaredBase = (pattern) => {
|
|
866
|
+
const parts = pattern.split("/");
|
|
867
|
+
const wildcard = parts.findIndex((part) => /[*?]/u.test(part));
|
|
868
|
+
return parts.slice(0, wildcard < 0 ? parts.length : wildcard).join("/") || ".";
|
|
869
|
+
};
|
|
870
|
+
const relatedBases = (left, right) => left === "." || right === "." || left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
|
|
871
|
+
const denoPositive = pm === "deno" ? positive.filter((pattern) => /[*?]/u.test(pattern)).map((pattern) => ({
|
|
872
|
+
base: declaredBase(pattern),
|
|
873
|
+
scope: anchorWorkspacePrefix(declaredBase(pattern), {
|
|
874
|
+
matches: () => true,
|
|
875
|
+
canDescend: () => true
|
|
876
|
+
}, resolveChild)
|
|
877
|
+
})) : [];
|
|
878
|
+
const denoOrdered = pm === "deno" ? patterns.filter((pattern) => /[*?]/u.test(pattern) || pattern.startsWith("!")).map((raw) => {
|
|
879
|
+
const isExcluded = raw.startsWith("!");
|
|
880
|
+
const pattern = path.posix.normalize(raw.replace(/^!/u, "")).replace(/\/+$/u, "");
|
|
881
|
+
const glob = compile(pattern, isExcluded);
|
|
882
|
+
if (!isExcluded) return {
|
|
883
|
+
excluded: false,
|
|
884
|
+
glob
|
|
885
|
+
};
|
|
886
|
+
return {
|
|
887
|
+
excluded: true,
|
|
888
|
+
glob: {
|
|
889
|
+
...glob,
|
|
890
|
+
matches(directory) {
|
|
891
|
+
if (!/[*?]/u.test(pattern)) return directory === pattern;
|
|
892
|
+
if (!glob.matches(directory)) return false;
|
|
893
|
+
const applicable = denoPositive.filter((item) => item.scope.matches(directory)).map((item) => relatedBases(item.base, declaredBase(pattern)));
|
|
894
|
+
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.");
|
|
895
|
+
return applicable.some(Boolean);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
}) : [];
|
|
900
|
+
const bunPatterns = pm === "bun" ? patterns.map((raw) => {
|
|
901
|
+
const isExcluded = raw.startsWith("!");
|
|
902
|
+
const pattern = path.posix.normalize(raw.replace(/^!/u, "")).replace(/\/+$/u, "");
|
|
903
|
+
const usesGlob = isExcluded || pattern.includes("*") || pattern.includes("?") || pattern.includes("{") || pattern.includes("[");
|
|
904
|
+
const glob = compileWorkspaceGlob(pattern, {
|
|
905
|
+
...defaultWorkspaceGlobOptions,
|
|
906
|
+
noext: true
|
|
907
|
+
});
|
|
908
|
+
const subtree = isExcluded && pattern.endsWith("/**") ? compileWorkspaceGlob(pattern.slice(0, -3), {
|
|
909
|
+
...defaultWorkspaceGlobOptions,
|
|
910
|
+
noext: true
|
|
911
|
+
}) : void 0;
|
|
912
|
+
return {
|
|
913
|
+
isExcluded,
|
|
914
|
+
usesGlob,
|
|
915
|
+
glob,
|
|
916
|
+
matches: (directory) => glob.matches(directory) || glob.matches(`${directory}/`) || subtree?.matches(directory) === true
|
|
917
|
+
};
|
|
918
|
+
}) : [];
|
|
919
|
+
const bunOrdered = bunPatterns.filter(({ usesGlob }) => usesGlob);
|
|
920
|
+
const bunLiterals = bunPatterns.filter(({ isExcluded, usesGlob }) => !isExcluded && !usesGlob);
|
|
921
|
+
const bunTraversal = bunPatterns.filter(({ isExcluded }) => !isExcluded).map(({ glob }) => glob);
|
|
922
|
+
const denoLiteral = pm === "deno" ? positive.filter((pattern) => !/[*?]/u.test(pattern)).map((pattern) => compile(pattern)) : [];
|
|
923
|
+
if (definition.denoManifests && denoOrdered.findLast(({ glob }) => glob.matches("."))?.excluded === false) throw new ConfigError("deno.json#workspace: a Deno workspace cannot contain itself.");
|
|
924
|
+
if (positive.length === 0 || positive.every((pattern) => pattern === ".")) return [];
|
|
925
|
+
if (!fs.readDirectories) throw new UsageError("Workspace discovery requires FileSystem.readDirectories; no host filesystem fallback is used.");
|
|
926
|
+
const result = [];
|
|
927
|
+
const pending = ["."];
|
|
928
|
+
while (pending.length > 0) {
|
|
929
|
+
const current = pending.pop();
|
|
930
|
+
if (current === void 0) break;
|
|
931
|
+
for (const name of readDirectories(current)) {
|
|
932
|
+
if (name === ".git" || name === "node_modules") continue;
|
|
933
|
+
const directory = asRelPath(current === "." ? name : `${current}/${name}`);
|
|
934
|
+
if (pm === "bun" && name === "CMakeFiles" && !bunLiterals.some(({ glob }) => glob.matches(directory) || glob.canDescend(directory))) continue;
|
|
935
|
+
const inVendor = skipVendor && (directory === "vendor" || directory.startsWith("vendor/"));
|
|
936
|
+
if (inVendor && !denoLiteral.some((pattern) => pattern.matches(directory) || pattern.canDescend(directory))) continue;
|
|
937
|
+
const isExcluded = pm !== "deno" && excluded.some(({ glob }) => glob.matches(directory) || glob.matches(`${directory}/`));
|
|
938
|
+
if (excluded.some(({ subtree }) => subtree?.matches(directory))) continue;
|
|
939
|
+
const lastDenoMatch = denoOrdered.findLast(({ glob }) => glob.matches(directory));
|
|
940
|
+
const lastBunMatch = bunOrdered.findLast(({ matches }) => matches(directory));
|
|
941
|
+
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);
|
|
942
|
+
if ((pm === "bun" ? bunTraversal : inVendor ? denoLiteral : included).some((pattern) => pattern.canDescend(directory))) pending.push(directory);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
return result.sort();
|
|
946
|
+
};
|
|
947
|
+
//#endregion
|
|
948
|
+
//#region src/application/workspace-contexts.ts
|
|
949
|
+
/** Expand each manager's declarations into isolated publication contexts. */
|
|
950
|
+
const collectWorkspaceMembers = (ctx, fs, pms, projectType) => pms.flatMap((pm) => {
|
|
951
|
+
const seen = /* @__PURE__ */ new Set();
|
|
952
|
+
return workspaceDefinitions(ctx, pm).flatMap((definition) => workspaceDirectories(ctx, fs, definition, pm).flatMap((directory) => {
|
|
953
|
+
const root = resolveIn(ctx.root, directory);
|
|
954
|
+
const manifest = resolveIn(root, asRelPath("package.json"));
|
|
955
|
+
const denoManifest = resolveIn(root, asRelPath("deno.json"));
|
|
956
|
+
const denoJsonc = resolveIn(root, asRelPath("deno.jsonc"));
|
|
957
|
+
const allowed = new Set(pm === "deno" ? [
|
|
958
|
+
manifest,
|
|
959
|
+
denoManifest,
|
|
960
|
+
denoJsonc
|
|
961
|
+
] : [manifest]);
|
|
962
|
+
const memberFs = {
|
|
963
|
+
exists: (file) => allowed.has(file) && fs.exists(file),
|
|
964
|
+
readText: (file) => allowed.has(file) ? fs.readText(file) : void 0
|
|
965
|
+
};
|
|
966
|
+
try {
|
|
967
|
+
const explicitMember = definition.patterns.some((pattern) => !pattern.startsWith("!") && !/[*?]/u.test(pattern) && path.posix.normalize(pattern).replace(/\/+$/u, "") === directory);
|
|
968
|
+
if (!definition.denoManifests && !fs.exists(manifest)) {
|
|
969
|
+
if (pm === "deno" && explicitMember) throw new ConfigError("Declared npm workspace member has no package.json.");
|
|
970
|
+
return [];
|
|
971
|
+
}
|
|
972
|
+
if (pm === "deno" && !fs.exists(denoManifest) && fs.exists(denoJsonc)) throw new ConfigError("deno.jsonc is not supported; use strict deno.json.");
|
|
973
|
+
const memberCtx = createRepoContext(root, memberFs, projectType);
|
|
974
|
+
if (!definition.denoManifests && !memberCtx.packageJson) return [];
|
|
975
|
+
if (!memberCtx.packageJson && !memberCtx.exists(asRelPath("deno.json"))) {
|
|
976
|
+
if (definition.denoManifests && explicitMember) throw new ConfigError("Declared Deno member has no deno.json or package.json.");
|
|
977
|
+
return [];
|
|
978
|
+
}
|
|
979
|
+
if (pm === "deno") {
|
|
980
|
+
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.");
|
|
981
|
+
}
|
|
982
|
+
if (seen.has(directory)) return [];
|
|
983
|
+
seen.add(directory);
|
|
984
|
+
return [{
|
|
985
|
+
directory,
|
|
986
|
+
pm,
|
|
987
|
+
ctx: memberCtx
|
|
988
|
+
}];
|
|
989
|
+
} catch (error) {
|
|
990
|
+
if (error instanceof ConfigError) throw new ConfigError(`${directory}/${error.message}`);
|
|
991
|
+
throw error;
|
|
992
|
+
}
|
|
993
|
+
}));
|
|
994
|
+
});
|
|
995
|
+
//#endregion
|
|
996
|
+
//#region src/domain/setting-availability.ts
|
|
997
|
+
const { npmrc: npmrc$7, packageJson: packageJson$2, pnpmWorkspace: pnpmWorkspace$11, yarnrc: yarnrc$10, bunfig: bunfig$6 } = CONFIG_FILES;
|
|
998
|
+
const settingAvailability = [
|
|
999
|
+
{
|
|
1000
|
+
pm: "npm",
|
|
1001
|
+
file: npmrc$7,
|
|
1002
|
+
keyPath: ["provenance"],
|
|
1003
|
+
since: "9.5.0",
|
|
1004
|
+
source: "https://github.com/npm/cli/releases/tag/v9.5.0"
|
|
1005
|
+
},
|
|
1006
|
+
{
|
|
1007
|
+
pm: "npm",
|
|
1008
|
+
file: packageJson$2,
|
|
1009
|
+
keyPath: ["publishConfig", "provenance"],
|
|
1010
|
+
since: "9.5.0",
|
|
1011
|
+
source: "https://github.com/npm/cli/releases/tag/v9.5.0"
|
|
1012
|
+
},
|
|
1013
|
+
{
|
|
1014
|
+
pm: "npm",
|
|
1015
|
+
file: npmrc$7,
|
|
1016
|
+
keyPath: ["min-release-age"],
|
|
1017
|
+
since: "11.10.0",
|
|
1018
|
+
source: "https://github.com/npm/cli/releases/tag/v11.10.0"
|
|
1019
|
+
},
|
|
1020
|
+
{
|
|
1021
|
+
pm: "pnpm",
|
|
1022
|
+
file: pnpmWorkspace$11,
|
|
1023
|
+
keyPath: ["strictDepBuilds"],
|
|
1024
|
+
since: "10.6.0",
|
|
1025
|
+
source: "https://github.com/pnpm/pnpm/releases/tag/v10.6.0"
|
|
1026
|
+
},
|
|
1027
|
+
{
|
|
1028
|
+
pm: "pnpm",
|
|
1029
|
+
file: pnpmWorkspace$11,
|
|
1030
|
+
keyPath: ["dangerouslyAllowAllBuilds"],
|
|
1031
|
+
since: "10.9.0",
|
|
1032
|
+
source: "https://github.com/pnpm/pnpm/releases/tag/v10.9.0"
|
|
1033
|
+
},
|
|
1034
|
+
{
|
|
1035
|
+
pm: "pnpm",
|
|
1036
|
+
file: pnpmWorkspace$11,
|
|
1037
|
+
keyPath: ["minimumReleaseAge"],
|
|
1038
|
+
since: "10.16.0",
|
|
1039
|
+
source: "https://github.com/pnpm/pnpm/releases/tag/v10.16.0"
|
|
1040
|
+
},
|
|
1041
|
+
{
|
|
1042
|
+
pm: "pnpm",
|
|
1043
|
+
file: pnpmWorkspace$11,
|
|
1044
|
+
keyPath: ["minimumReleaseAgeExclude"],
|
|
1045
|
+
since: "10.16.0",
|
|
1046
|
+
source: "https://github.com/pnpm/pnpm/releases/tag/v10.16.0"
|
|
1047
|
+
},
|
|
1048
|
+
{
|
|
1049
|
+
pm: "pnpm",
|
|
1050
|
+
file: pnpmWorkspace$11,
|
|
1051
|
+
keyPath: ["trustPolicy"],
|
|
1052
|
+
since: "10.21.0",
|
|
1053
|
+
source: "https://github.com/pnpm/pnpm/releases/tag/v10.21.0"
|
|
1054
|
+
},
|
|
1055
|
+
{
|
|
1056
|
+
pm: "pnpm",
|
|
1057
|
+
file: pnpmWorkspace$11,
|
|
1058
|
+
keyPath: ["blockExoticSubdeps"],
|
|
1059
|
+
since: "10.26.0",
|
|
1060
|
+
source: "https://github.com/pnpm/pnpm/releases/tag/v10.26.0"
|
|
1061
|
+
},
|
|
1062
|
+
{
|
|
1063
|
+
pm: "pnpm",
|
|
1064
|
+
file: pnpmWorkspace$11,
|
|
1065
|
+
keyPath: ["frozenStore"],
|
|
1066
|
+
since: "11.7.0",
|
|
1067
|
+
source: "https://github.com/pnpm/pnpm/releases/tag/v11.7.0"
|
|
1068
|
+
},
|
|
1069
|
+
{
|
|
1070
|
+
pm: "yarn",
|
|
1071
|
+
file: yarnrc$10,
|
|
1072
|
+
keyPath: ["enableHardenedMode"],
|
|
1073
|
+
since: "4.0.0",
|
|
1074
|
+
source: "https://yarnpkg.com/blog/release/4.0"
|
|
1075
|
+
},
|
|
1076
|
+
{
|
|
1077
|
+
pm: "yarn",
|
|
1078
|
+
file: yarnrc$10,
|
|
1079
|
+
keyPath: ["npmMinimalAgeGate"],
|
|
1080
|
+
since: "4.10.0",
|
|
1081
|
+
source: "https://github.com/yarnpkg/berry/releases/tag/@yarnpkg/cli/4.10.0"
|
|
1082
|
+
},
|
|
1083
|
+
{
|
|
1084
|
+
pm: "yarn",
|
|
1085
|
+
file: yarnrc$10,
|
|
1086
|
+
keyPath: ["npmPreapprovedPackages"],
|
|
1087
|
+
since: "4.10.0",
|
|
1088
|
+
source: "https://github.com/yarnpkg/berry/releases/tag/@yarnpkg/cli/4.10.0"
|
|
1089
|
+
},
|
|
1090
|
+
{
|
|
1091
|
+
pm: "bun",
|
|
1092
|
+
file: bunfig$6,
|
|
1093
|
+
keyPath: ["install", "minimumReleaseAge"],
|
|
1094
|
+
since: "1.3.0",
|
|
1095
|
+
source: "https://bun.com/blog/bun-v1.3#minimum-release-age"
|
|
1096
|
+
},
|
|
1097
|
+
{
|
|
1098
|
+
pm: "bun",
|
|
1099
|
+
file: bunfig$6,
|
|
1100
|
+
keyPath: [
|
|
1101
|
+
"install",
|
|
1102
|
+
"security",
|
|
1103
|
+
"scanner"
|
|
1104
|
+
],
|
|
1105
|
+
since: "1.2.21",
|
|
1106
|
+
source: "https://bun.com/blog/bun-v1.2.21#security-scanner-api-for-bun-install"
|
|
1107
|
+
},
|
|
1108
|
+
{
|
|
1109
|
+
pm: "deno",
|
|
1110
|
+
file: npmrc$7,
|
|
1111
|
+
keyPath: ["min-release-age"],
|
|
1112
|
+
since: "2.8.1",
|
|
1113
|
+
source: "https://github.com/denoland/deno/releases/tag/v2.8.1"
|
|
1114
|
+
}
|
|
1115
|
+
];
|
|
1116
|
+
//#endregion
|
|
1117
|
+
//#region src/domain/services/remediation-availability.ts
|
|
1118
|
+
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]);
|
|
1119
|
+
/** Undefined means the target version or the setting's introduction is unknown. */
|
|
1120
|
+
const settingSupportedByTarget = (pm, version, target) => {
|
|
1121
|
+
if (version === void 0) return void 0;
|
|
1122
|
+
const availability = settingAvailability.find((setting) => setting.pm === pm && sameSetting(setting, target));
|
|
1123
|
+
return availability === void 0 ? void 0 : !lt(version, availability.since);
|
|
1124
|
+
};
|
|
1125
|
+
/** Keep a proposal atomic and make known version prerequisites explicit. */
|
|
1126
|
+
const guardRemediationAvailability = (pm, version, remediation, targets = remediation?.kind === "automatic" ? remediation.operations : []) => {
|
|
1127
|
+
if (version === void 0 || remediation === void 0) return remediation;
|
|
1128
|
+
const unsupported = settingAvailability.filter((setting) => setting.pm === pm && lt(version, setting.since) && targets.some((target) => sameSetting(setting, target)));
|
|
1129
|
+
if (unsupported.length === 0) return remediation;
|
|
1130
|
+
return {
|
|
1131
|
+
kind: "manual",
|
|
1132
|
+
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)}.`)]
|
|
1133
|
+
};
|
|
507
1134
|
};
|
|
508
|
-
/** Look up the codec for a parseable kind. Total — every CodecKind has one. */
|
|
509
|
-
const codecFor = (kind) => CODECS[kind];
|
|
510
1135
|
//#endregion
|
|
511
1136
|
//#region src/domain/rules/remediation.ts
|
|
512
1137
|
/** Scalar writes must not discard an existing container or replace a non-object parent. */
|
|
@@ -561,10 +1186,14 @@ const checkKeyValue = (spec, config) => {
|
|
|
561
1186
|
}])
|
|
562
1187
|
};
|
|
563
1188
|
};
|
|
564
|
-
const buildBinding = (spec, applies) => ({
|
|
1189
|
+
const buildBinding = (spec, pm, applies) => ({
|
|
565
1190
|
check(ctx, config) {
|
|
566
1191
|
if (typeof applies !== "undefined" && !applies(ctx)) return { state: "na" };
|
|
567
|
-
|
|
1192
|
+
const status = checkKeyValue(spec, config);
|
|
1193
|
+
return status.state === "violation" ? {
|
|
1194
|
+
...status,
|
|
1195
|
+
remediation: guardRemediationAvailability(pm, ctx.pmVersion, status.remediation, [spec])
|
|
1196
|
+
} : status;
|
|
568
1197
|
},
|
|
569
1198
|
docs: spec.docs,
|
|
570
1199
|
file: spec.file,
|
|
@@ -578,7 +1207,7 @@ const requireConfigKey = (options) => {
|
|
|
578
1207
|
const spec = options.bindings[pm];
|
|
579
1208
|
if (typeof spec !== "undefined") {
|
|
580
1209
|
if ("extraFix" in spec) throw new TypeError("extraFix is no longer supported; use a custom binding.");
|
|
581
|
-
bindings[pm] = buildBinding(spec, options.applies);
|
|
1210
|
+
bindings[pm] = buildBinding(spec, pm, options.applies);
|
|
582
1211
|
}
|
|
583
1212
|
}
|
|
584
1213
|
return {
|
|
@@ -605,39 +1234,6 @@ const withAubeParanoid = (rule) => {
|
|
|
605
1234
|
} });
|
|
606
1235
|
};
|
|
607
1236
|
//#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
1237
|
//#region src/domain/rules/advisory-check.ts
|
|
642
1238
|
const { aubeWorkspace: aubeWorkspace$8 } = CONFIG_FILES;
|
|
643
1239
|
const advisoryCheck = withAubeParanoid(requireConfigKey({
|
|
@@ -808,7 +1404,7 @@ const blockExoticSubdeps = overrideBindings(requireConfigKey({
|
|
|
808
1404
|
const { bunfig: bunfig$4 } = CONFIG_FILES;
|
|
809
1405
|
const bunSecurityScanner = defineRule({
|
|
810
1406
|
bindings: { bun: {
|
|
811
|
-
check(
|
|
1407
|
+
check(ctx, config) {
|
|
812
1408
|
const scanner = getByPath(config, [
|
|
813
1409
|
"install",
|
|
814
1410
|
"security",
|
|
@@ -816,10 +1412,17 @@ const bunSecurityScanner = defineRule({
|
|
|
816
1412
|
]);
|
|
817
1413
|
if (isNonBlankString(scanner)) return { state: "ok" };
|
|
818
1414
|
return {
|
|
819
|
-
remediation: {
|
|
1415
|
+
remediation: guardRemediationAvailability("bun", ctx.pmVersion, {
|
|
820
1416
|
kind: "manual",
|
|
821
1417
|
steps: ["Add `[install.security] scanner = \"@socketsecurity/bun-security-scanner\"` (or another bun-compatible scanner) to bunfig.toml."]
|
|
822
|
-
},
|
|
1418
|
+
}, [{
|
|
1419
|
+
file: bunfig$4,
|
|
1420
|
+
keyPath: [
|
|
1421
|
+
"install",
|
|
1422
|
+
"security",
|
|
1423
|
+
"scanner"
|
|
1424
|
+
]
|
|
1425
|
+
}]),
|
|
823
1426
|
actual: scanner,
|
|
824
1427
|
message: "Configure `[install.security] scanner = \"...\"` in bunfig.toml (e.g. `@socketsecurity/bun-security-scanner`) to scan new packages on install.",
|
|
825
1428
|
state: "violation"
|
|
@@ -827,7 +1430,7 @@ const bunSecurityScanner = defineRule({
|
|
|
827
1430
|
},
|
|
828
1431
|
docs: "https://bun.com/docs/pm/security-scanner-api",
|
|
829
1432
|
file: bunfig$4,
|
|
830
|
-
versionNote: { configAvailableSince: "bun 1.
|
|
1433
|
+
versionNote: { configAvailableSince: "bun 1.2.21" }
|
|
831
1434
|
} },
|
|
832
1435
|
description: "Bun supports a Security Scanner API that intercepts new packages at install time (e.g. Socket Firewall).",
|
|
833
1436
|
docs: "https://github.com/bodadotsh/npm-security-best-practices#preinstall-scanners",
|
|
@@ -1008,31 +1611,74 @@ const dependencyOverrides = defineRule({
|
|
|
1008
1611
|
//#region src/domain/rules/disable-lifecycle-scripts.ts
|
|
1009
1612
|
const { npmrc: npmrc$5, pnpmWorkspace: pnpmWorkspace$8, yarnrc: yarnrc$6, aubeWorkspace: aubeWorkspace$5, bunfig: bunfig$3 } = CONFIG_FILES;
|
|
1010
1613
|
const pnpmBinding$4 = {
|
|
1011
|
-
check(
|
|
1614
|
+
check(ctx, config) {
|
|
1012
1615
|
if (getByPath(config, ["ignoreScripts"]) === true) return { state: "ok" };
|
|
1013
1616
|
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
1617
|
const strict = getByPath(config, ["strictDepBuilds"]);
|
|
1618
|
+
if (bypass === true) {
|
|
1619
|
+
const bypassTarget = {
|
|
1620
|
+
file: pnpmWorkspace$8,
|
|
1621
|
+
keyPath: ["dangerouslyAllowAllBuilds"]
|
|
1622
|
+
};
|
|
1623
|
+
if (settingSupportedByTarget("pnpm", ctx.pmVersion, bypassTarget) === false) {
|
|
1624
|
+
const proposal = {
|
|
1625
|
+
kind: "manual",
|
|
1626
|
+
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."]]
|
|
1627
|
+
};
|
|
1628
|
+
return {
|
|
1629
|
+
state: "violation",
|
|
1630
|
+
actual: bypass,
|
|
1631
|
+
expected: false,
|
|
1632
|
+
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.",
|
|
1633
|
+
remediation: strict === true ? proposal : guardRemediationAvailability("pnpm", ctx.pmVersion, proposal, [{
|
|
1634
|
+
file: pnpmWorkspace$8,
|
|
1635
|
+
keyPath: ["strictDepBuilds"]
|
|
1636
|
+
}])
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
const proposal = {
|
|
1640
|
+
kind: "manual",
|
|
1641
|
+
steps: ["After upgrading, remove `dangerouslyAllowAllBuilds: true` (or set it to false) and configure lifecycle-script gating."]
|
|
1642
|
+
};
|
|
1643
|
+
const guarded = guardRemediationAvailability("pnpm", ctx.pmVersion, proposal, [{
|
|
1644
|
+
file: pnpmWorkspace$8,
|
|
1645
|
+
keyPath: ["strictDepBuilds"]
|
|
1646
|
+
}, {
|
|
1647
|
+
file: pnpmWorkspace$8,
|
|
1648
|
+
keyPath: ["dangerouslyAllowAllBuilds"]
|
|
1649
|
+
}]);
|
|
1650
|
+
if (guarded !== proposal) return {
|
|
1651
|
+
state: "violation",
|
|
1652
|
+
actual: bypass,
|
|
1653
|
+
expected: false,
|
|
1654
|
+
message: "The target cannot use the proposed YAML lifecycle-script controls. Removing an unsupported bypass alone does not provide protection.",
|
|
1655
|
+
remediation: guarded
|
|
1656
|
+
};
|
|
1657
|
+
return {
|
|
1658
|
+
actual: bypass,
|
|
1659
|
+
expected: false,
|
|
1660
|
+
remediation: {
|
|
1661
|
+
kind: "manual",
|
|
1662
|
+
steps: ["Remove `dangerouslyAllowAllBuilds: true` from pnpm-workspace.yaml (or set it to `false`). Setting `strictDepBuilds: true` alone has no effect while the bypass remains."]
|
|
1663
|
+
},
|
|
1664
|
+
message: "`dangerouslyAllowAllBuilds: true` in pnpm-workspace.yaml bypasses strictDepBuilds — remove it (or set it to false) to restore lifecycle-script gating.",
|
|
1665
|
+
state: "violation"
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1025
1668
|
if (strict === true) return { state: "ok" };
|
|
1026
1669
|
return {
|
|
1027
1670
|
state: "violation",
|
|
1028
1671
|
actual: strict,
|
|
1029
1672
|
expected: true,
|
|
1030
1673
|
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, [{
|
|
1674
|
+
remediation: guardRemediationAvailability("pnpm", ctx.pmVersion, proposeChanges(config, [{
|
|
1032
1675
|
file: pnpmWorkspace$8,
|
|
1033
1676
|
keyPath: ["strictDepBuilds"],
|
|
1034
1677
|
op: "setKey",
|
|
1035
1678
|
value: true
|
|
1679
|
+
}]), [{
|
|
1680
|
+
file: pnpmWorkspace$8,
|
|
1681
|
+
keyPath: ["strictDepBuilds"]
|
|
1036
1682
|
}])
|
|
1037
1683
|
};
|
|
1038
1684
|
},
|
|
@@ -1040,6 +1686,7 @@ const pnpmBinding$4 = {
|
|
|
1040
1686
|
file: pnpmWorkspace$8,
|
|
1041
1687
|
versionNote: {
|
|
1042
1688
|
configAvailableSince: "pnpm 10.3.0",
|
|
1689
|
+
note: "pnpm-workspace.yaml settings require pnpm 10.6.0",
|
|
1043
1690
|
defaultSafeSince: "pnpm 11.0.0"
|
|
1044
1691
|
}
|
|
1045
1692
|
};
|
|
@@ -1049,31 +1696,37 @@ const aubeBinding$2 = {
|
|
|
1049
1696
|
const jail = getByPath(config, ["jailBuilds"]);
|
|
1050
1697
|
const strict = getByPath(npmConfig, ["strictDepBuilds"]);
|
|
1051
1698
|
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 {
|
|
1699
|
+
const violations = [];
|
|
1700
|
+
if (jail !== true) violations.push({
|
|
1065
1701
|
state: "violation",
|
|
1066
|
-
file:
|
|
1067
|
-
actual: jail
|
|
1702
|
+
file: aubeWorkspace$5.path,
|
|
1703
|
+
actual: jail,
|
|
1068
1704
|
expected: true,
|
|
1069
|
-
message: "Set `jailBuilds: true` in aube-workspace.yaml
|
|
1070
|
-
remediation:
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1705
|
+
message: "Set `jailBuilds: true` in aube-workspace.yaml to sandbox approved builds.",
|
|
1706
|
+
remediation: proposeChanges(config, [{
|
|
1707
|
+
file: aubeWorkspace$5,
|
|
1708
|
+
keyPath: ["jailBuilds"],
|
|
1709
|
+
op: "setKey",
|
|
1710
|
+
value: true
|
|
1711
|
+
}])
|
|
1712
|
+
});
|
|
1713
|
+
if (strict !== true) violations.push({
|
|
1714
|
+
state: "violation",
|
|
1715
|
+
file: npmrc$5.path,
|
|
1716
|
+
actual: strict,
|
|
1717
|
+
expected: true,
|
|
1718
|
+
message: "Set `strictDepBuilds=true` in .npmrc to reject unreviewed lifecycle scripts.",
|
|
1719
|
+
remediation: proposeChanges(npmConfig, [{
|
|
1720
|
+
file: npmrc$5,
|
|
1721
|
+
keyPath: ["strictDepBuilds"],
|
|
1722
|
+
op: "setKey",
|
|
1723
|
+
value: true
|
|
1724
|
+
}])
|
|
1725
|
+
});
|
|
1726
|
+
const [first, ...rest] = violations;
|
|
1727
|
+
return !first ? { state: "ok" } : rest.length === 0 ? first : {
|
|
1728
|
+
state: "violations",
|
|
1729
|
+
violations: [first, ...rest]
|
|
1077
1730
|
};
|
|
1078
1731
|
},
|
|
1079
1732
|
docs: "https://aube.jdx.dev/security.html",
|
|
@@ -1196,7 +1849,7 @@ const resolvePackageJsonProjectType = (ctx) => {
|
|
|
1196
1849
|
const inferredPackage = pkg?.private !== true && isPublishableName(pkg?.name);
|
|
1197
1850
|
return resolveProjectType(ctx.projectType, inferredPackage);
|
|
1198
1851
|
};
|
|
1199
|
-
const resolveDenoProjectType = (ctx, denoConfig) => resolveProjectType(ctx.projectType, isPublishableName(getByPath(denoConfig, ["name"])));
|
|
1852
|
+
const resolveDenoProjectType = (ctx, denoConfig) => resolveProjectType(ctx.projectType, getByPath(denoConfig, ["publish"]) !== false && isPublishableName(getByPath(denoConfig, ["name"])));
|
|
1200
1853
|
//#endregion
|
|
1201
1854
|
//#region src/domain/rules/publishable.ts
|
|
1202
1855
|
const isPublishable = (ctx) => resolvePackageJsonProjectType(ctx) === "package";
|
|
@@ -1319,14 +1972,17 @@ const frozenLockfile = overrideBindings(requireConfigKey({
|
|
|
1319
1972
|
const { pnpmWorkspace: pnpmWorkspace$6 } = CONFIG_FILES;
|
|
1320
1973
|
const frozenStore = defineRule({
|
|
1321
1974
|
bindings: { pnpm: {
|
|
1322
|
-
check(
|
|
1975
|
+
check(ctx, config) {
|
|
1323
1976
|
const value = getByPath(config, ["frozenStore"]);
|
|
1324
1977
|
if (value === true) return { state: "ok" };
|
|
1325
1978
|
return {
|
|
1326
|
-
remediation: {
|
|
1979
|
+
remediation: guardRemediationAvailability("pnpm", ctx.pmVersion, {
|
|
1327
1980
|
kind: "manual",
|
|
1328
1981
|
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
|
-
},
|
|
1982
|
+
}, [{
|
|
1983
|
+
file: pnpmWorkspace$6,
|
|
1984
|
+
keyPath: ["frozenStore"]
|
|
1985
|
+
}]),
|
|
1330
1986
|
actual: value,
|
|
1331
1987
|
message: "For a pre-populated read-only store, consider `frozenStore` in pnpm-workspace.yaml. Normal installs need a writable store.",
|
|
1332
1988
|
state: "violation"
|
|
@@ -1416,6 +2072,14 @@ const isNonDisabledDenoDuration = (value) => {
|
|
|
1416
2072
|
if (value.exclude !== void 0 && !isStringList(value.exclude)) return false;
|
|
1417
2073
|
return value.age == null || isActiveDenoAge(value.age);
|
|
1418
2074
|
};
|
|
2075
|
+
const denoAgeUsesFallback = (value) => {
|
|
2076
|
+
if (value === void 0 || value === null) return true;
|
|
2077
|
+
if (!isPlainRecord(value)) return false;
|
|
2078
|
+
if (Object.keys(value).some((key) => key !== "age" && key !== "exclude")) return false;
|
|
2079
|
+
if (value.exclude !== void 0 && !isStringList(value.exclude)) return false;
|
|
2080
|
+
return value.age == null;
|
|
2081
|
+
};
|
|
2082
|
+
const isPositiveDenoNpmrcDays = (value) => typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
1419
2083
|
const minimumReleaseAge = overrideBindings(requireConfigKey({
|
|
1420
2084
|
bindings: {
|
|
1421
2085
|
aube: {
|
|
@@ -1473,7 +2137,7 @@ const minimumReleaseAge = overrideBindings(requireConfigKey({
|
|
|
1473
2137
|
file: npmrc$3,
|
|
1474
2138
|
docs: "https://docs.npmjs.com/cli/v12/using-npm/config#min-release-age",
|
|
1475
2139
|
versionNote: { note: "min-release-age available since npm 11.10.0" },
|
|
1476
|
-
check(
|
|
2140
|
+
check(ctx, config) {
|
|
1477
2141
|
const now = Date.now();
|
|
1478
2142
|
if (Object.hasOwn(config, "before")) {
|
|
1479
2143
|
const actual = config.before;
|
|
@@ -1485,7 +2149,13 @@ const minimumReleaseAge = overrideBindings(requireConfigKey({
|
|
|
1485
2149
|
message: "Use a valid past before cutoff, or remove before and set min-release-age.",
|
|
1486
2150
|
remediation: {
|
|
1487
2151
|
kind: "manual",
|
|
1488
|
-
steps: [
|
|
2152
|
+
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, {
|
|
2153
|
+
kind: "manual",
|
|
2154
|
+
steps: [`Alternatively, remove before and set min-release-age to ~${RECOMMENDED_RELEASE_AGE_DAYS} days.`]
|
|
2155
|
+
}, [{
|
|
2156
|
+
file: npmrc$3,
|
|
2157
|
+
keyPath: ["min-release-age"]
|
|
2158
|
+
}])?.steps ?? []]
|
|
1489
2159
|
}
|
|
1490
2160
|
};
|
|
1491
2161
|
}
|
|
@@ -1498,11 +2168,14 @@ const minimumReleaseAge = overrideBindings(requireConfigKey({
|
|
|
1498
2168
|
actual,
|
|
1499
2169
|
expected: RECOMMENDED_RELEASE_AGE_DAYS,
|
|
1500
2170
|
message: `Set min-release-age to ~${RECOMMENDED_RELEASE_AGE_DAYS} days to quarantine brand-new releases.`,
|
|
1501
|
-
remediation: proposeChanges(config, [{
|
|
2171
|
+
remediation: guardRemediationAvailability("npm", ctx.pmVersion, proposeChanges(config, [{
|
|
1502
2172
|
file: npmrc$3,
|
|
1503
2173
|
op: "setKey",
|
|
1504
2174
|
keyPath: ["min-release-age"],
|
|
1505
2175
|
value: RECOMMENDED_RELEASE_AGE_DAYS
|
|
2176
|
+
}]), [{
|
|
2177
|
+
file: npmrc$3,
|
|
2178
|
+
keyPath: ["min-release-age"]
|
|
1506
2179
|
}])
|
|
1507
2180
|
};
|
|
1508
2181
|
}
|
|
@@ -1512,10 +2185,28 @@ const minimumReleaseAge = overrideBindings(requireConfigKey({
|
|
|
1512
2185
|
docs: "https://docs.deno.com/runtime/reference/deno_json/",
|
|
1513
2186
|
versionNote: {
|
|
1514
2187
|
defaultSafeSince: "deno 2.9.0 (1440 minutes)",
|
|
1515
|
-
note: "object age may be omitted"
|
|
2188
|
+
note: "object age may be omitted; project .npmrc fallback available since deno 2.8.1"
|
|
1516
2189
|
},
|
|
1517
|
-
check(
|
|
2190
|
+
check(ctx, config) {
|
|
1518
2191
|
const actual = getByPath(config, ["minimumDependencyAge"]);
|
|
2192
|
+
if (denoAgeUsesFallback(actual)) {
|
|
2193
|
+
const npmrcConfig = ctx.readConfig(npmrc$3);
|
|
2194
|
+
const npmrcAge = getByPath(npmrcConfig, ["min-release-age"]);
|
|
2195
|
+
if (isPositiveDenoNpmrcDays(npmrcAge)) return { state: "ok" };
|
|
2196
|
+
if (npmrcAge === 0) return {
|
|
2197
|
+
state: "violation",
|
|
2198
|
+
actual: npmrcAge,
|
|
2199
|
+
expected: RECOMMENDED_RELEASE_AGE_DAYS,
|
|
2200
|
+
file: npmrc$3.path,
|
|
2201
|
+
message: `Set min-release-age to ~${RECOMMENDED_RELEASE_AGE_DAYS} days in .npmrc, or set minimumDependencyAge in deno.json.`,
|
|
2202
|
+
remediation: proposeChanges(npmrcConfig, [{
|
|
2203
|
+
file: npmrc$3,
|
|
2204
|
+
op: "setKey",
|
|
2205
|
+
keyPath: ["min-release-age"],
|
|
2206
|
+
value: RECOMMENDED_RELEASE_AGE_DAYS
|
|
2207
|
+
}])
|
|
2208
|
+
};
|
|
2209
|
+
}
|
|
1519
2210
|
if (isNonDisabledDenoDuration(actual)) return { state: "ok" };
|
|
1520
2211
|
const objectAge = isPlainRecord(actual);
|
|
1521
2212
|
const invalidObject = objectAge && (Object.keys(actual).some((key) => key !== "age" && key !== "exclude") || actual.exclude !== void 0 && !isStringList(actual.exclude));
|
|
@@ -1943,6 +2634,57 @@ const strictStoreIntegrity = defineRule({
|
|
|
1943
2634
|
//#endregion
|
|
1944
2635
|
//#region src/domain/rules/trust-policy.ts
|
|
1945
2636
|
const { aubeWorkspace, pnpmWorkspace } = CONFIG_FILES;
|
|
2637
|
+
const trustPolicy = withAubeParanoid(requireConfigKey({
|
|
2638
|
+
bindings: {
|
|
2639
|
+
aube: {
|
|
2640
|
+
docs: "https://aube.jdx.dev/security.html",
|
|
2641
|
+
documentedDefault: "no-downgrade",
|
|
2642
|
+
file: aubeWorkspace,
|
|
2643
|
+
keyPath: ["trustPolicy"],
|
|
2644
|
+
message: "Set `trustPolicy: no-downgrade` in aube-workspace.yaml to block packages whose trust level has decreased.",
|
|
2645
|
+
value: "no-downgrade"
|
|
2646
|
+
},
|
|
2647
|
+
pnpm: {
|
|
2648
|
+
docs: "https://pnpm.io/settings#trustpolicy",
|
|
2649
|
+
file: pnpmWorkspace,
|
|
2650
|
+
keyPath: ["trustPolicy"],
|
|
2651
|
+
message: "Set `trustPolicy: no-downgrade` in pnpm-workspace.yaml to block packages whose trust level has decreased.",
|
|
2652
|
+
value: "no-downgrade",
|
|
2653
|
+
versionNote: { configAvailableSince: "pnpm 10.21.0" }
|
|
2654
|
+
}
|
|
2655
|
+
},
|
|
2656
|
+
description: "Fail installation when a package trust level has decreased compared to previous releases, catching publisher credential downgrades.",
|
|
2657
|
+
docs: "https://pnpm.io/settings#trustpolicy",
|
|
2658
|
+
id: "trust-policy",
|
|
2659
|
+
severity: "warn",
|
|
2660
|
+
title: "Enforce trust policy on dependency updates"
|
|
2661
|
+
}));
|
|
2662
|
+
//#endregion
|
|
2663
|
+
//#region src/domain/rules/unsupported-settings.ts
|
|
2664
|
+
const bindingFor = (pm) => {
|
|
2665
|
+
const settings = settingAvailability.filter((setting) => setting.pm === pm);
|
|
2666
|
+
return { check(ctx) {
|
|
2667
|
+
const version = ctx.pmVersion;
|
|
2668
|
+
if (version === void 0) return { state: "na" };
|
|
2669
|
+
const unsupported = settings.filter((setting) => lt(version, setting.since) && getByPath(ctx.readConfig(setting.file), setting.keyPath) !== void 0);
|
|
2670
|
+
const [first, ...rest] = [...new Set(unsupported.map((setting) => setting.file.path))].map((file) => {
|
|
2671
|
+
const fileSettings = unsupported.filter((setting) => setting.file.path === file);
|
|
2672
|
+
return {
|
|
2673
|
+
state: "violation",
|
|
2674
|
+
file,
|
|
2675
|
+
message: `Target ${pm} ${version} predates support for ${file}: ${fileSettings.map((setting) => `${setting.keyPath.join(".")} (requires ${pm} >=${setting.since})`).join("; ")}.`,
|
|
2676
|
+
remediation: {
|
|
2677
|
+
kind: "manual",
|
|
2678
|
+
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}`)]
|
|
2679
|
+
}
|
|
2680
|
+
};
|
|
2681
|
+
});
|
|
2682
|
+
return first ? {
|
|
2683
|
+
state: "violations",
|
|
2684
|
+
violations: [first, ...rest]
|
|
2685
|
+
} : { state: "ok" };
|
|
2686
|
+
} };
|
|
2687
|
+
};
|
|
1946
2688
|
//#endregion
|
|
1947
2689
|
//#region src/domain/builtin-rules.ts
|
|
1948
2690
|
const rules = [
|
|
@@ -1972,31 +2714,21 @@ const rules = [
|
|
|
1972
2714
|
strictAllowScripts,
|
|
1973
2715
|
strictReleaseAge,
|
|
1974
2716
|
strictStoreIntegrity,
|
|
1975
|
-
|
|
2717
|
+
trustPolicy,
|
|
2718
|
+
defineRule({
|
|
2719
|
+
id: "unsupported-settings",
|
|
2720
|
+
title: "Use settings available in the target PM version",
|
|
2721
|
+
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.",
|
|
2722
|
+
severity: "error",
|
|
2723
|
+
docs: "https://github.com/pHo9UBenaA/siro/blob/main/docs/rules.md#unsupported-settings--error",
|
|
1976
2724
|
bindings: {
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
},
|
|
1985
|
-
pnpm: {
|
|
1986
|
-
docs: "https://pnpm.io/settings#trustpolicy",
|
|
1987
|
-
file: pnpmWorkspace,
|
|
1988
|
-
keyPath: ["trustPolicy"],
|
|
1989
|
-
message: "Set `trustPolicy: no-downgrade` in pnpm-workspace.yaml to block packages whose trust level has decreased.",
|
|
1990
|
-
value: "no-downgrade",
|
|
1991
|
-
versionNote: { configAvailableSince: "pnpm 10.21.0" }
|
|
1992
|
-
}
|
|
1993
|
-
},
|
|
1994
|
-
description: "Fail installation when a package trust level has decreased compared to previous releases, catching publisher credential downgrades.",
|
|
1995
|
-
docs: "https://pnpm.io/settings#trustpolicy",
|
|
1996
|
-
id: "trust-policy",
|
|
1997
|
-
severity: "warn",
|
|
1998
|
-
title: "Enforce trust policy on dependency updates"
|
|
1999
|
-
}))
|
|
2725
|
+
npm: bindingFor("npm"),
|
|
2726
|
+
pnpm: bindingFor("pnpm"),
|
|
2727
|
+
yarn: bindingFor("yarn"),
|
|
2728
|
+
bun: bindingFor("bun"),
|
|
2729
|
+
deno: bindingFor("deno")
|
|
2730
|
+
}
|
|
2731
|
+
})
|
|
2000
2732
|
];
|
|
2001
2733
|
//#endregion
|
|
2002
2734
|
//#region src/domain/services/apply-config.ts
|
|
@@ -2076,27 +2808,6 @@ const resolvePMs = (ctx, opts) => {
|
|
|
2076
2808
|
/** Resolve the most specific severity signal for one finding. */
|
|
2077
2809
|
const decideSeverity = (status, binding, rule, userOverride) => userOverride ?? status.severity ?? binding.severity ?? rule.severity;
|
|
2078
2810
|
//#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
2811
|
//#region src/domain/services/render-version-note.ts
|
|
2101
2812
|
const buildVersionNoteParts = (versionNote) => {
|
|
2102
2813
|
const parts = [];
|
|
@@ -2128,29 +2839,33 @@ const runLint = (opts) => {
|
|
|
2128
2839
|
warn: 0
|
|
2129
2840
|
};
|
|
2130
2841
|
const parseConfig = createConfigParser(codecFor, ctx);
|
|
2131
|
-
const ruleContext = {
|
|
2132
|
-
...ctx,
|
|
2133
|
-
readConfig: parseConfig
|
|
2134
|
-
};
|
|
2135
2842
|
for (const rule of ruleSet) for (const pm of pms) {
|
|
2136
2843
|
const binding = rule.bindings[pm];
|
|
2137
2844
|
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
|
|
2845
|
+
const ruleContext = {
|
|
2846
|
+
...ctx,
|
|
2847
|
+
readConfig: parseConfig,
|
|
2848
|
+
pmVersion: opts.pmVersions?.[pm]
|
|
2151
2849
|
};
|
|
2152
|
-
|
|
2153
|
-
|
|
2850
|
+
const response = binding.check(ruleContext, parseConfig(binding.file));
|
|
2851
|
+
if (!isCheckStatusShape(response)) throw new ConfigError(`Rule '${rule.id}' returned an invalid check result.`);
|
|
2852
|
+
const statuses = response.state === "violations" ? response.violations : [response];
|
|
2853
|
+
for (const status of statuses) {
|
|
2854
|
+
if (status.state !== "violation") continue;
|
|
2855
|
+
const finding = {
|
|
2856
|
+
ruleId: rule.id,
|
|
2857
|
+
pm,
|
|
2858
|
+
severity: decideSeverity(status, binding, rule, severityOverrides?.get(rule.id)),
|
|
2859
|
+
message: renderVersionNoteMessage(status.message, binding.versionNote),
|
|
2860
|
+
file: status.file ?? binding.file?.path,
|
|
2861
|
+
docs: binding.docs ?? rule.docs,
|
|
2862
|
+
actual: status.actual,
|
|
2863
|
+
expected: status.expected,
|
|
2864
|
+
remediation: guardRemediationAvailability(pm, ruleContext.pmVersion, status.remediation)
|
|
2865
|
+
};
|
|
2866
|
+
findings.push(finding);
|
|
2867
|
+
summary[finding.severity] += 1;
|
|
2868
|
+
}
|
|
2154
2869
|
}
|
|
2155
2870
|
return {
|
|
2156
2871
|
findings,
|
|
@@ -2165,6 +2880,8 @@ const prepareLint = (options) => {
|
|
|
2165
2880
|
if ("customRules" in options || "reporters" in options) throw new UsageError("Pass customRules and reporters inside the config option.");
|
|
2166
2881
|
if (options.pm !== void 0 && !isPM(options.pm)) throw new UsageError(`Unknown package manager: ${String(options.pm)}`);
|
|
2167
2882
|
if (options.projectType !== void 0 && !isProjectType(options.projectType)) throw new UsageError(`Unknown project type: ${String(options.projectType)}`);
|
|
2883
|
+
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.");
|
|
2884
|
+
if (options.workspaces !== void 0 && typeof options.workspaces !== "boolean") throw new UsageError("workspaces must be a boolean.");
|
|
2168
2885
|
const config = options.config === void 0 ? void 0 : parseConfig(options.config);
|
|
2169
2886
|
const ctx = createRepoContext(options.cwd, options.fs, options.projectType ?? config?.projectType);
|
|
2170
2887
|
const pms = resolvePMs(ctx, {
|
|
@@ -2173,16 +2890,61 @@ const prepareLint = (options) => {
|
|
|
2173
2890
|
});
|
|
2174
2891
|
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
2892
|
const configured = applyConfig(rules, config);
|
|
2893
|
+
const pmVersions = {
|
|
2894
|
+
...declaredPMVersion(ctx.packageJson?.packageManager),
|
|
2895
|
+
...config?.pmVersions,
|
|
2896
|
+
...options.pm && options.pmVersion ? { [options.pm]: options.pmVersion } : {}
|
|
2897
|
+
};
|
|
2898
|
+
const fs = options.fs ?? nodeFileSystem;
|
|
2176
2899
|
return {
|
|
2177
2900
|
ctx,
|
|
2178
2901
|
pms,
|
|
2902
|
+
pmVersions,
|
|
2903
|
+
members: options.workspaces ? collectWorkspaceMembers(ctx, fs, pms, options.projectType ?? config?.projectType) : [],
|
|
2179
2904
|
ruleSet: configured.rules,
|
|
2180
2905
|
severityOverrides: configured.severityOverrides,
|
|
2181
2906
|
codecFor,
|
|
2182
2907
|
reporters: config?.reporters ?? []
|
|
2183
2908
|
};
|
|
2184
2909
|
};
|
|
2185
|
-
|
|
2910
|
+
/** Reuse the existing manifest checks; installation policy remains rooted at cwd. */
|
|
2911
|
+
const runPreparedLint = (prepared) => {
|
|
2912
|
+
const result = runLint(prepared);
|
|
2913
|
+
const findings = [...result.findings];
|
|
2914
|
+
const summary = { ...result.summary };
|
|
2915
|
+
const memberRules = prepared.ruleSet.filter((rule) => [
|
|
2916
|
+
"files-field",
|
|
2917
|
+
"publish-access",
|
|
2918
|
+
"unsupported-settings"
|
|
2919
|
+
].includes(rule.id));
|
|
2920
|
+
for (const member of prepared.members) {
|
|
2921
|
+
const child = runLint({
|
|
2922
|
+
...prepared,
|
|
2923
|
+
ctx: member.ctx,
|
|
2924
|
+
pms: [member.pm],
|
|
2925
|
+
ruleSet: memberRules
|
|
2926
|
+
});
|
|
2927
|
+
for (const finding of child.findings) findings.push({
|
|
2928
|
+
...finding,
|
|
2929
|
+
file: `${member.directory}/${finding.file ?? "package.json"}`,
|
|
2930
|
+
message: `${member.directory}: ${finding.message}`,
|
|
2931
|
+
remediation: finding.remediation?.kind === "manual" ? {
|
|
2932
|
+
kind: "manual",
|
|
2933
|
+
steps: [`Work in ${member.directory} for this finding.`, ...finding.remediation.steps]
|
|
2934
|
+
} : finding.remediation
|
|
2935
|
+
});
|
|
2936
|
+
for (const level of [
|
|
2937
|
+
"error",
|
|
2938
|
+
"warn",
|
|
2939
|
+
"info"
|
|
2940
|
+
]) summary[level] += child.summary[level];
|
|
2941
|
+
}
|
|
2942
|
+
return {
|
|
2943
|
+
findings,
|
|
2944
|
+
summary
|
|
2945
|
+
};
|
|
2946
|
+
};
|
|
2947
|
+
const lint = (options) => runPreparedLint(prepareLint(options));
|
|
2186
2948
|
//#endregion
|
|
2187
2949
|
//#region src/application/commands/lint.ts
|
|
2188
2950
|
/** Evaluate and report. Executable config loading belongs to the CLI adapter. */
|
|
@@ -2194,9 +2956,9 @@ const lintCommand = async (options, io) => {
|
|
|
2194
2956
|
const selection = options.reporter ?? DEFAULT_REPORTER_NAME;
|
|
2195
2957
|
const reporter = typeof selection === "string" ? registry.get(selection) : selection;
|
|
2196
2958
|
if (!isReporterShape(reporter)) throw new UsageError(`${typeof selection === "string" ? "Unknown" : "Invalid"} reporter: ${String(selection)} (available: ${[...registry.keys()].join(", ")})`);
|
|
2197
|
-
const result =
|
|
2959
|
+
const result = runPreparedLint(prepared);
|
|
2198
2960
|
const exitCode = exitCodeForLint(result, options.severity ?? "error");
|
|
2199
|
-
reporter.format(filterBySeverity(result, options.severity ?? "info"), io);
|
|
2961
|
+
await reporter.format(filterBySeverity(result, options.severity ?? "info"), io);
|
|
2200
2962
|
return exitCode;
|
|
2201
2963
|
};
|
|
2202
2964
|
//#endregion
|
|
@@ -2206,6 +2968,6 @@ const nodeIO = {
|
|
|
2206
2968
|
stdout: (line) => process.stdout.write(`${line}\n`)
|
|
2207
2969
|
};
|
|
2208
2970
|
//#endregion
|
|
2209
|
-
export { ConfigError as A, SUPPORTED_NODE_RANGE as C, isNodeError as D, nodeFileSystem as E, UsageError as M, asAbsPath as O, isSeverity as S, assertDirectory as T, PROJECT_TYPES as _,
|
|
2971
|
+
export { ConfigError as A, SUPPORTED_NODE_RANGE as C, isNodeError as D, nodeFileSystem as E, UsageError as M, asAbsPath as O, isSeverity as S, assertDirectory as T, PROJECT_TYPES as _, requireConfigKey as a, SEVERITIES as b, DEFAULT_REPORTER_NAME as c, jsonReporter as d, version as f, getByPath as g, defineRule as h, overrideBindings as i, SiroError as j, asRelPath as k, JSON_REPORTER_NAME as l, loadConfig as m, lintCommand as n, CONFIG_FILES as o, githubReporter as p, lint as r, BUILTIN_REPORTER_NAMES as s, nodeIO as t, prettyReporter as u, isProjectType as v, isSupportedNodeVersion as w, isPM as x, PMS as y };
|
|
2210
2972
|
|
|
2211
|
-
//# sourceMappingURL=node-io-
|
|
2973
|
+
//# sourceMappingURL=node-io-CaqobxYp.mjs.map
|