workspace-conformance 1.7.0 → 2.0.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/README.md +27 -13
- package/dist/cli.js +252 -67
- package/dist/index.cjs +249 -117
- package/dist/index.d.cts +40 -18
- package/dist/index.d.ts +41 -19
- package/dist/index.js +249 -118
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -10,8 +10,8 @@ import { LineCounter, isMap, isNode, isScalar, isSeq, parse, parseDocument } fro
|
|
|
10
10
|
import { execFile } from "node:child_process";
|
|
11
11
|
import { findNodeAtLocation, parse as parse$1, parseTree, printParseErrorCode } from "jsonc-parser";
|
|
12
12
|
import { parse as parse$2 } from "smol-toml";
|
|
13
|
-
import { defineSection, layoutSection, loadSection } from "@exadev/config";
|
|
14
13
|
import { z } from "zod";
|
|
14
|
+
import { defineSection, layoutSection, loadSection } from "@exadev/config";
|
|
15
15
|
//#region src/files.ts
|
|
16
16
|
/**
|
|
17
17
|
* Directories no check descends into: installed dependencies are not the workspace's own files.
|
|
@@ -781,8 +781,18 @@ const dockerfilePackageManager = async ({ cwd, options }) => {
|
|
|
781
781
|
}
|
|
782
782
|
return violations;
|
|
783
783
|
};
|
|
784
|
-
|
|
785
|
-
|
|
784
|
+
/**
|
|
785
|
+
* What every path inside the package in `dir` starts with, relative to the workspace root: `dir` and a `/`, or nothing for the root package, which holds every path. The trailing `/` keeps `packages/a` from holding `packages/ab`.
|
|
786
|
+
*/
|
|
787
|
+
function packagePathPrefix(dir) {
|
|
788
|
+
return dir === "." ? "" : `${dir}/`;
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Whether `path`, relative to the workspace root, is inside the package in `dir`.
|
|
792
|
+
*/
|
|
793
|
+
function isInPackage(dir, path) {
|
|
794
|
+
return path.startsWith(packagePathPrefix(dir));
|
|
795
|
+
}
|
|
786
796
|
function segmentsOf(path) {
|
|
787
797
|
return path.split("/").filter((segment) => segment !== "" && segment !== ".");
|
|
788
798
|
}
|
|
@@ -831,7 +841,7 @@ function sliceByNamePrefix(name, known) {
|
|
|
831
841
|
/**
|
|
832
842
|
* Classify discovered packages by the layout: group, rank and slice. Pure.
|
|
833
843
|
*
|
|
834
|
-
* A package no group owns is a configuration error, because every check would otherwise skip it without saying so. Nested package directories are one too: the checks attribute a file to the package whose directory is its prefix, which is ambiguous for a package inside another. A `namePrefix` slice takes the longest slice value that a `segment` group produced and that prefixes the package's unscoped name; a package without a name has no such slice.
|
|
844
|
+
* A package no group owns is a configuration error, because every check would otherwise skip it without saying so. Nested package directories are one too: the checks attribute a file to the package whose directory is its prefix, which is ambiguous for a package inside another. The root package holds every other directory, so it is a package only in a single-package repository. A `namePrefix` slice takes the longest slice value that a `segment` group produced and that prefixes the package's unscoped name; a package without a name has no such slice.
|
|
835
845
|
*/
|
|
836
846
|
function classifyPackages(discovered, layout) {
|
|
837
847
|
const owned = discovered.map((found) => {
|
|
@@ -859,7 +869,7 @@ function classifyPackages(discovered, layout) {
|
|
|
859
869
|
};
|
|
860
870
|
});
|
|
861
871
|
for (const outer of packages) {
|
|
862
|
-
const nested = packages.find((inner) => inner.dir !== outer.dir &&
|
|
872
|
+
const nested = packages.find((inner) => inner.dir !== outer.dir && isInPackage(outer.dir, inner.dir));
|
|
863
873
|
if (nested !== void 0) throw new ConformanceError(`the package directory '${nested.dir}' is inside the package directory '${outer.dir}'; workspace packages must not nest`);
|
|
864
874
|
}
|
|
865
875
|
return packages;
|
|
@@ -908,7 +918,7 @@ async function readName(manifest) {
|
|
|
908
918
|
if (typeof content === "object" && content !== null && "name" in content && typeof content.name === "string") return content.name;
|
|
909
919
|
}
|
|
910
920
|
/**
|
|
911
|
-
* The directories under `root` that the pnpm-style `patterns` select and that hold a `package.json`, sorted. A `!` pattern excludes. The root package
|
|
921
|
+
* The directories under `root` that the pnpm-style `patterns` select and that hold a `package.json`, sorted. A `!` pattern excludes. The root package is found, as {@link ROOT_PACKAGE_DIR}, when a pattern selects it (`.`, or `**`).
|
|
912
922
|
*/
|
|
913
923
|
async function discoverPackages(root, patterns) {
|
|
914
924
|
const manifests = patterns.map((pattern) => pattern.startsWith("!") ? `!${pattern.slice(1)}/**` : `${pattern}/package.json`);
|
|
@@ -917,18 +927,24 @@ async function discoverPackages(root, patterns) {
|
|
|
917
927
|
dot: true,
|
|
918
928
|
ignore: [INSTALLED_DEPENDENCIES_GLOB],
|
|
919
929
|
onlyFiles: true
|
|
920
|
-
})).map((manifest) =>
|
|
930
|
+
})).map((manifest) => {
|
|
931
|
+
const dir = manifest.replace(/\/?package\.json$/u, "");
|
|
932
|
+
return dir === "" ? "." : dir;
|
|
933
|
+
});
|
|
921
934
|
return Promise.all([...new Set(dirs)].sort().map(async (dir) => ({
|
|
922
935
|
dir,
|
|
923
936
|
name: await readName(join(root, dir, "package.json"))
|
|
924
937
|
})));
|
|
925
938
|
}
|
|
926
939
|
/**
|
|
927
|
-
* The layout's packages found under the workspace root.
|
|
940
|
+
* The layout's packages found under the workspace root. Finding none is a configuration error: every check that reads the packages would otherwise look at nothing and report a clean workspace.
|
|
928
941
|
*/
|
|
929
942
|
async function readWorkspacePackages(cwd, layout) {
|
|
930
943
|
const root = workspaceRoot(cwd, layout);
|
|
931
|
-
|
|
944
|
+
const patterns = await workspacePatterns(root, layout);
|
|
945
|
+
const discovered = await discoverPackages(root, patterns);
|
|
946
|
+
if (discovered.length === 0) throw new ConformanceError(`no workspace package found: ${patterns.length === 0 ? "the package patterns are empty, so no directory is selected" : `no directory the patterns ${patterns.join(", ")} select holds a package.json`}; for a single-package repository, set the layout's packages to ['.']`);
|
|
947
|
+
return classifyPackages(discovered, layout);
|
|
932
948
|
}
|
|
933
949
|
//#endregion
|
|
934
950
|
//#region src/checks/engines-floor.ts
|
|
@@ -1182,40 +1198,61 @@ async function sampleViolations(eslint, sample, shared) {
|
|
|
1182
1198
|
function isNothingLinted(error) {
|
|
1183
1199
|
return error instanceof Error && isRecord(error) && (error["messageTemplate"] === "file-not-found" || error["messageTemplate"] === "all-matched-files-ignored");
|
|
1184
1200
|
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Where a message is, or `undefined` for a message about the whole file. ESLint types `line` and `column` as numbers, but a parsing error copies them from the parser's error, which may carry none: typescript-eslint's project service reports a file outside every tsconfig that way.
|
|
1203
|
+
*/
|
|
1204
|
+
function locationOf(message) {
|
|
1205
|
+
const line = message.line;
|
|
1206
|
+
const column = message.column;
|
|
1207
|
+
return typeof line === "number" && typeof column === "number" ? {
|
|
1208
|
+
line,
|
|
1209
|
+
column
|
|
1210
|
+
} : void 0;
|
|
1211
|
+
}
|
|
1185
1212
|
function messageViolation(cwd, result, message) {
|
|
1186
1213
|
const file = relativePosix(cwd, result.filePath);
|
|
1187
|
-
const location =
|
|
1188
|
-
|
|
1189
|
-
|
|
1214
|
+
const location = locationOf(message);
|
|
1215
|
+
const at = location === void 0 ? { file } : {
|
|
1216
|
+
file,
|
|
1217
|
+
location
|
|
1190
1218
|
};
|
|
1191
1219
|
if (message.ruleId !== null) {
|
|
1192
1220
|
const severity = message.severity === 1 ? "warn" : "error";
|
|
1193
1221
|
return {
|
|
1194
1222
|
code: `eslint/lint/${message.ruleId}`,
|
|
1195
1223
|
message: `${message.ruleId} (${severity}): ${message.message}`,
|
|
1196
|
-
|
|
1197
|
-
location
|
|
1224
|
+
...at
|
|
1198
1225
|
};
|
|
1199
1226
|
}
|
|
1200
1227
|
return message.fatal === true ? {
|
|
1201
1228
|
code: "eslint/fatal",
|
|
1202
1229
|
message: message.message,
|
|
1203
|
-
|
|
1204
|
-
location
|
|
1230
|
+
...at
|
|
1205
1231
|
} : {
|
|
1206
1232
|
code: "eslint/lint-message",
|
|
1207
1233
|
message: message.message,
|
|
1208
|
-
|
|
1209
|
-
location
|
|
1234
|
+
...at
|
|
1210
1235
|
};
|
|
1211
1236
|
}
|
|
1212
1237
|
function byPosition(left, right) {
|
|
1213
1238
|
return left.file.localeCompare(right.file) || (left.location?.line ?? 0) - (right.location?.line ?? 0) || (left.location?.column ?? 0) - (right.location?.column ?? 0) || left.code.localeCompare(right.code);
|
|
1214
1239
|
}
|
|
1240
|
+
function files(count) {
|
|
1241
|
+
return count === 1 ? "1 file" : `${String(count)} files`;
|
|
1242
|
+
}
|
|
1215
1243
|
/**
|
|
1216
|
-
*
|
|
1244
|
+
* The files below `cwd` that ESLint has a configuration for (not ignored, and matched by a configuration block) and that the lint did not reach: the patterns are narrower than what the config covers.
|
|
1217
1245
|
*/
|
|
1218
|
-
async function
|
|
1246
|
+
async function unreachedFiles(cwd, eslint, linted) {
|
|
1247
|
+
const candidates = (await findFiles(cwd, ["**"])).filter((file) => !linted.has(resolve(cwd, file)));
|
|
1248
|
+
const unreached = [];
|
|
1249
|
+
for (const file of candidates) if (!await eslint.isPathIgnored(file)) unreached.push(file);
|
|
1250
|
+
return unreached;
|
|
1251
|
+
}
|
|
1252
|
+
/**
|
|
1253
|
+
* Lints the workspace through the Node API, with the repository's own config, and maps every message to a violation. A pattern that selects no linted file is itself a violation, since the workspace is then not linted at all. It notes how many files it linted and how many files the config covers that the patterns did not reach.
|
|
1254
|
+
*/
|
|
1255
|
+
async function lintViolations(cwd, eslint, patterns, note) {
|
|
1219
1256
|
const violations = [];
|
|
1220
1257
|
const results = /* @__PURE__ */ new Map();
|
|
1221
1258
|
for (const pattern of patterns) try {
|
|
@@ -1229,6 +1266,10 @@ async function lintViolations(cwd, eslint, patterns) {
|
|
|
1229
1266
|
});
|
|
1230
1267
|
}
|
|
1231
1268
|
for (const result of results.values()) for (const message of result.messages) violations.push(messageViolation(cwd, result, message));
|
|
1269
|
+
if (note !== void 0) {
|
|
1270
|
+
const unreached = await unreachedFiles(cwd, eslint, new Set(results.keys()));
|
|
1271
|
+
note(unreached.length === 0 ? `linted ${files(results.size)}` : `linted ${files(results.size)}; ${unreached.length === 1 ? "1 other file has" : `${String(unreached.length)} other files have`} an ESLint configuration and ${unreached.length === 1 ? "was" : "were"} not reached by lintPatterns (${patterns.join(", ")})`);
|
|
1272
|
+
}
|
|
1232
1273
|
return violations.sort(byPosition);
|
|
1233
1274
|
}
|
|
1234
1275
|
/**
|
|
@@ -1238,7 +1279,9 @@ async function lintViolations(cwd, eslint, patterns) {
|
|
|
1238
1279
|
*
|
|
1239
1280
|
* The config is a black box: the check sees which rules are active on a file, not why. It does not embed a config, so a repository without one is reported and nothing is substituted.
|
|
1240
1281
|
*/
|
|
1241
|
-
const eslint = async (
|
|
1282
|
+
const eslint = async (context) => {
|
|
1283
|
+
const { options } = context;
|
|
1284
|
+
const cwd = resolve(context.cwd);
|
|
1242
1285
|
const instance = new (loadEslint(cwd))({
|
|
1243
1286
|
cwd,
|
|
1244
1287
|
...options.configFile === void 0 ? {} : { overrideConfigFile: resolve(cwd, options.configFile) }
|
|
@@ -1246,7 +1289,7 @@ const eslint = async ({ cwd, options }) => {
|
|
|
1246
1289
|
const shared = options.rules ?? {};
|
|
1247
1290
|
const violations = [];
|
|
1248
1291
|
for (const sample of options.samples.map(sampleOf)) violations.push(...await sampleViolations(instance, sample, shared));
|
|
1249
|
-
if (options.lint === true) violations.push(...await lintViolations(cwd, instance, options.lintPatterns ?? DEFAULT_LINT_PATTERNS));
|
|
1292
|
+
if (options.lint === true) violations.push(...await lintViolations(cwd, instance, options.lintPatterns ?? DEFAULT_LINT_PATTERNS, context.note));
|
|
1250
1293
|
return violations;
|
|
1251
1294
|
};
|
|
1252
1295
|
//#endregion
|
|
@@ -1256,10 +1299,10 @@ function escapeRegExp(text) {
|
|
|
1256
1299
|
return text.replace(REGEXP_SPECIAL, "\\$&");
|
|
1257
1300
|
}
|
|
1258
1301
|
/**
|
|
1259
|
-
* A regular expression, as source text, matching every file inside any of the packages
|
|
1302
|
+
* A regular expression, as source text, matching every file inside any of the packages: the path starts with one's {@link packagePathPrefix}.
|
|
1260
1303
|
*/
|
|
1261
1304
|
function packagesPattern(packages) {
|
|
1262
|
-
return `^(${packages.map((member) =>
|
|
1305
|
+
return `^(${packages.map((member) => escapeRegExp(packagePathPrefix(member.dir))).join("|")})`;
|
|
1263
1306
|
}
|
|
1264
1307
|
/**
|
|
1265
1308
|
* A regular expression, as source text, matching the name an import uses for any of the named packages: the name itself or a subpath of it. It matches nothing but those names, and is `undefined` when no package has a name.
|
|
@@ -1418,7 +1461,7 @@ const DEFAULT_DO_NOT_FOLLOW = ["(^|/)dist/"];
|
|
|
1418
1461
|
* The package a path in the graph belongs to: the one whose directory holds the file, and only when no directory does, the one whose name the path is (an import that resolved to no file is known only by the name it was written with, or a subpath of it). The directory wins because an unscoped package name can equal the leading directory of another package's files. It throws `ConformanceError` for a path the graph should not contain.
|
|
1419
1462
|
*/
|
|
1420
1463
|
function packageOfPath(packages, path) {
|
|
1421
|
-
const found = packages.find((member) =>
|
|
1464
|
+
const found = packages.find((member) => isInPackage(member.dir, path)) ?? packages.find((member) => member.name !== void 0 && (path === member.name || path.startsWith(`${member.name}/`)));
|
|
1422
1465
|
if (found === void 0) throw new ConformanceError(`${path} is in no workspace package, although the graph is limited to the packages' files and names`);
|
|
1423
1466
|
return found;
|
|
1424
1467
|
}
|
|
@@ -1574,7 +1617,7 @@ function describePackage(member) {
|
|
|
1574
1617
|
*/
|
|
1575
1618
|
function describeTarget(edge) {
|
|
1576
1619
|
const { to, toFile } = edge;
|
|
1577
|
-
return !
|
|
1620
|
+
return !isInPackage(to.dir, toFile) && to.name !== void 0 && (toFile === to.name || toFile.startsWith(`${to.name}/`)) ? toFile : `${toFile} in ${describePackage(to)}`;
|
|
1578
1621
|
}
|
|
1579
1622
|
//#endregion
|
|
1580
1623
|
//#region src/checks/import-cross-slice.ts
|
|
@@ -1849,7 +1892,7 @@ async function leadsTo(cwd, file, entry, destination) {
|
|
|
1849
1892
|
}
|
|
1850
1893
|
}
|
|
1851
1894
|
/**
|
|
1852
|
-
* Each agent instruction file is tracked by git as a symbolic link (mode 120000) whose target is the README beside it, so the two cannot diverge. The link may be written in any form that resolves there (`./README.md`), and may lead through other tracked links, as in `CLAUDE.md` to `AGENTS.md` to `README.md`. It reads the index, not the working tree, so a checkout with `core.symlinks` false, where a link is written out as a plain file, is judged by what is committed.
|
|
1895
|
+
* Each agent instruction file is tracked by git as a symbolic link (mode 120000) whose target is the README beside it, so the two cannot diverge. A directory without a tracked README has nothing to link to, so a missing instruction file there is not reported; one that exists is still judged. The link may be written in any form that resolves there (`./README.md`), and may lead through other tracked links, as in `CLAUDE.md` to `AGENTS.md` to `README.md`. It reads the index, not the working tree, so a checkout with `core.symlinks` false, where a link is written out as a plain file, is judged by what is committed.
|
|
1853
1896
|
*/
|
|
1854
1897
|
const instructionSymlinks = async ({ cwd, options }) => {
|
|
1855
1898
|
const files = options.files ?? DEFAULT_INSTRUCTION_FILES;
|
|
@@ -1857,15 +1900,17 @@ const instructionSymlinks = async ({ cwd, options }) => {
|
|
|
1857
1900
|
const violations = [];
|
|
1858
1901
|
for (const directory of await directoriesOf(cwd, options.directories ?? ["."])) {
|
|
1859
1902
|
const readme = posix.join(directory, target);
|
|
1903
|
+
const readmeTracked = await indexEntry(cwd, readme) !== void 0;
|
|
1860
1904
|
for (const name of files) {
|
|
1861
1905
|
const file = posix.join(directory, name);
|
|
1862
1906
|
const entry = await indexEntry(cwd, file);
|
|
1863
|
-
if (entry === void 0)
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1907
|
+
if (entry === void 0) {
|
|
1908
|
+
if (readmeTracked) violations.push({
|
|
1909
|
+
code: "instruction-symlinks/missing",
|
|
1910
|
+
message: `${file} is not tracked; commit it as a symbolic link to ${target}`,
|
|
1911
|
+
file
|
|
1912
|
+
});
|
|
1913
|
+
} else if (entry.mode !== "120000") violations.push({
|
|
1869
1914
|
code: "instruction-symlinks/not-a-symlink",
|
|
1870
1915
|
message: `${file} is committed as a regular file (mode ${entry.mode}); commit it as a symbolic link to ${target}`,
|
|
1871
1916
|
file
|
|
@@ -1877,7 +1922,7 @@ const instructionSymlinks = async ({ cwd, options }) => {
|
|
|
1877
1922
|
message: `${file} links to ${linked}; it should link to ${target}`,
|
|
1878
1923
|
file
|
|
1879
1924
|
});
|
|
1880
|
-
} else if (
|
|
1925
|
+
} else if (!readmeTracked) violations.push({
|
|
1881
1926
|
code: "instruction-symlinks/dangling",
|
|
1882
1927
|
message: `${file} links to ${target}, which is not tracked`,
|
|
1883
1928
|
file
|
|
@@ -2089,6 +2134,116 @@ const migrationsDirectory = async ({ cwd, options, configFiles }) => {
|
|
|
2089
2134
|
return violations;
|
|
2090
2135
|
};
|
|
2091
2136
|
//#endregion
|
|
2137
|
+
//#region src/github.ts
|
|
2138
|
+
/**
|
|
2139
|
+
* GitHub's refusal to read a branch's rules because the repository's plan does not offer rulesets, as for a private repository on a plan without them: no ruleset applies to its branches, so a check that requires one cannot be met. The message is GitHub's own. A {@link GitHubClient} rejects `branchRules` with it, and with any other error for every other refusal.
|
|
2140
|
+
*/
|
|
2141
|
+
var RulesetsUnavailableError = class extends ConformanceError {
|
|
2142
|
+
constructor(message, options) {
|
|
2143
|
+
super(message, options);
|
|
2144
|
+
this.name = "RulesetsUnavailableError";
|
|
2145
|
+
}
|
|
2146
|
+
};
|
|
2147
|
+
const repositorySchema = z.object({
|
|
2148
|
+
default_branch: z.string(),
|
|
2149
|
+
allow_rebase_merge: z.optional(z.boolean()),
|
|
2150
|
+
allow_squash_merge: z.optional(z.boolean()),
|
|
2151
|
+
allow_merge_commit: z.optional(z.boolean())
|
|
2152
|
+
});
|
|
2153
|
+
const rulesSchema = z.array(z.looseObject({
|
|
2154
|
+
type: z.string(),
|
|
2155
|
+
parameters: z.optional(z.looseObject({
|
|
2156
|
+
required_status_checks: z.optional(z.array(z.object({ context: z.string() }))),
|
|
2157
|
+
strict_required_status_checks_policy: z.optional(z.boolean()),
|
|
2158
|
+
required_review_thread_resolution: z.optional(z.boolean()),
|
|
2159
|
+
allowed_merge_methods: z.optional(z.array(z.string()))
|
|
2160
|
+
}))
|
|
2161
|
+
}));
|
|
2162
|
+
/**
|
|
2163
|
+
* The body of an error response of the REST API: `message` explains the refusal.
|
|
2164
|
+
*/
|
|
2165
|
+
const errorBodySchema = z.looseObject({ message: z.string() });
|
|
2166
|
+
/**
|
|
2167
|
+
* The 403 status of a request that GitHub refuses, for a missing permission, a rate limit or a feature the plan lacks alike.
|
|
2168
|
+
*/
|
|
2169
|
+
const FORBIDDEN = 403;
|
|
2170
|
+
/**
|
|
2171
|
+
* How GitHub's message starts when it refuses a request for a feature the repository's plan does not include ('Upgrade to GitHub Pro or make this repository public to enable this feature.'). The REST documentation of the branch rules endpoint lists no error status, and a 403 also means a missing permission ('Resource not accessible by personal access token') or a rate limit, so the message is the only thing that tells a plan limit apart.
|
|
2172
|
+
*/
|
|
2173
|
+
const PLAN_LIMIT_PREFIX = "Upgrade to GitHub ";
|
|
2174
|
+
/**
|
|
2175
|
+
* The plan-limit refusal of a branch's rules as {@link RulesetsUnavailableError}.
|
|
2176
|
+
*/
|
|
2177
|
+
const rulesRefusal = (status, githubMessage) => status === FORBIDDEN && githubMessage?.startsWith(PLAN_LIMIT_PREFIX) === true ? new RulesetsUnavailableError(githubMessage) : void 0;
|
|
2178
|
+
async function githubMessageOf(response) {
|
|
2179
|
+
const text = await response.text();
|
|
2180
|
+
let body;
|
|
2181
|
+
try {
|
|
2182
|
+
body = JSON.parse(text);
|
|
2183
|
+
} catch {
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
2186
|
+
const parsed = errorBodySchema.safeParse(body);
|
|
2187
|
+
return parsed.success ? parsed.data.message : void 0;
|
|
2188
|
+
}
|
|
2189
|
+
const API_ROOT = "https://api.github.com";
|
|
2190
|
+
const API_VERSION = "2022-11-28";
|
|
2191
|
+
/**
|
|
2192
|
+
* How many rules the API returns in one page, its maximum.
|
|
2193
|
+
*/
|
|
2194
|
+
const PAGE_SIZE = 100;
|
|
2195
|
+
/**
|
|
2196
|
+
* A {@link GitHubClient} over the REST API.
|
|
2197
|
+
*/
|
|
2198
|
+
function createGitHubClient(options) {
|
|
2199
|
+
const request = options.fetch ?? fetch;
|
|
2200
|
+
const root = options.apiUrl ?? API_ROOT;
|
|
2201
|
+
async function get(path, refusal) {
|
|
2202
|
+
const response = await request(`${root}${path}`, { headers: {
|
|
2203
|
+
Accept: "application/vnd.github+json",
|
|
2204
|
+
Authorization: `Bearer ${options.token}`,
|
|
2205
|
+
"X-GitHub-Api-Version": API_VERSION
|
|
2206
|
+
} });
|
|
2207
|
+
if (!response.ok) {
|
|
2208
|
+
const githubMessage = await githubMessageOf(response);
|
|
2209
|
+
throw refusal?.(response.status, githubMessage) ?? new ConformanceError(`GET ${path} failed: ${String(response.status)} ${response.statusText}${githubMessage === void 0 ? "" : `: ${githubMessage}`}`);
|
|
2210
|
+
}
|
|
2211
|
+
return response.json();
|
|
2212
|
+
}
|
|
2213
|
+
return {
|
|
2214
|
+
repository: async (slug) => {
|
|
2215
|
+
const parsed = repositorySchema.parse(await get(`/repos/${slug.owner}/${slug.name}`));
|
|
2216
|
+
const { allow_rebase_merge: allowRebaseMerge, allow_squash_merge: allowSquashMerge, allow_merge_commit: allowMergeCommit } = parsed;
|
|
2217
|
+
if (allowRebaseMerge === void 0 || allowSquashMerge === void 0 || allowMergeCommit === void 0) throw new ConformanceError(`GET /repos/${slug.owner}/${slug.name} did not report the merge-method settings, which GitHub shows only to a token with write access to the repository`);
|
|
2218
|
+
return {
|
|
2219
|
+
defaultBranch: parsed.default_branch,
|
|
2220
|
+
allowRebaseMerge,
|
|
2221
|
+
allowSquashMerge,
|
|
2222
|
+
allowMergeCommit
|
|
2223
|
+
};
|
|
2224
|
+
},
|
|
2225
|
+
branchRules: async (slug, branch) => {
|
|
2226
|
+
const rules = [];
|
|
2227
|
+
for (let page = 1, more = true; more; page += 1) {
|
|
2228
|
+
const body = await get(`/repos/${slug.owner}/${slug.name}/rules/branches/${encodeURIComponent(branch)}?per_page=${String(PAGE_SIZE)}&page=${String(page)}`, rulesRefusal);
|
|
2229
|
+
const found = rulesSchema.parse(body);
|
|
2230
|
+
rules.push(...found);
|
|
2231
|
+
more = found.length === PAGE_SIZE;
|
|
2232
|
+
}
|
|
2233
|
+
return {
|
|
2234
|
+
requiredStatusChecks: rules.filter((rule) => rule.type === "required_status_checks").map((rule) => ({
|
|
2235
|
+
contexts: (rule.parameters?.required_status_checks ?? []).map((check) => check.context),
|
|
2236
|
+
strict: rule.parameters?.strict_required_status_checks_policy === true
|
|
2237
|
+
})),
|
|
2238
|
+
pullRequests: rules.filter((rule) => rule.type === "pull_request").map((rule) => ({
|
|
2239
|
+
requiredReviewThreadResolution: rule.parameters?.required_review_thread_resolution === true,
|
|
2240
|
+
allowedMergeMethods: rule.parameters?.allowed_merge_methods
|
|
2241
|
+
}))
|
|
2242
|
+
};
|
|
2243
|
+
}
|
|
2244
|
+
};
|
|
2245
|
+
}
|
|
2246
|
+
//#endregion
|
|
2092
2247
|
//#region src/settings/repository.ts
|
|
2093
2248
|
const SLUG = /^([^/\s]+)\/([^/\s]+)$/u;
|
|
2094
2249
|
const REMOTE = /github\.com[:/]([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/u;
|
|
@@ -2125,6 +2280,24 @@ async function branchOf(github, slug, options) {
|
|
|
2125
2280
|
function repositoryFile(slug) {
|
|
2126
2281
|
return `${slug.owner}/${slug.name}`;
|
|
2127
2282
|
}
|
|
2283
|
+
/**
|
|
2284
|
+
* The rules of the branch, or GitHub's message when the repository's plan has no rulesets, in which case no rule applies to the branch and none can be added. Any other refusal rejects.
|
|
2285
|
+
*/
|
|
2286
|
+
async function branchRulesOf(github, slug, options) {
|
|
2287
|
+
const branch = await branchOf(github, slug, options);
|
|
2288
|
+
try {
|
|
2289
|
+
return { rules: await github.branchRules(slug, branch) };
|
|
2290
|
+
} catch (error) {
|
|
2291
|
+
if (error instanceof RulesetsUnavailableError) return { unavailable: error.message };
|
|
2292
|
+
throw error;
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
/**
|
|
2296
|
+
* The message of a `rules-unavailable` violation: the branch rules of the repository cannot be read or enforced on its plan, so `what` cannot be required.
|
|
2297
|
+
*/
|
|
2298
|
+
function rulesUnavailableMessage(file, what, githubMessage) {
|
|
2299
|
+
return `the branch rules of ${file} cannot be read or enforced on its plan, so ${what} cannot be required (GitHub: ${githubMessage})`;
|
|
2300
|
+
}
|
|
2128
2301
|
//#endregion
|
|
2129
2302
|
//#region src/checks/settings-merge-methods.ts
|
|
2130
2303
|
const METHODS = [
|
|
@@ -2135,20 +2308,23 @@ const METHODS = [
|
|
|
2135
2308
|
/**
|
|
2136
2309
|
* Only one merge method is available for a pull request into the branch, `rebase` unless the options say otherwise. A method is available when the repository allows it and no `pull_request` rule that applies to the branch leaves it out of its allowed methods.
|
|
2137
2310
|
*
|
|
2311
|
+
* When the repository's plan has no rulesets, no rule applies to the branch and the repository settings alone decide.
|
|
2312
|
+
*
|
|
2138
2313
|
* Limits: it reads the repository settings and the rules of rulesets that apply to the branch; classic branch protection and a merge queue's own merge method are not read, so a repository whose queue owns the strategy is judged by what it still allows for a direct merge.
|
|
2139
2314
|
*/
|
|
2140
2315
|
const settingsMergeMethods = async ({ cwd, options, github }) => {
|
|
2141
2316
|
const slug = await repositoryOf(cwd, options);
|
|
2142
2317
|
const file = repositoryFile(slug);
|
|
2143
2318
|
const settings = await github.repository(slug);
|
|
2144
|
-
const
|
|
2319
|
+
const read = await branchRulesOf(github, slug, options);
|
|
2320
|
+
const pullRequests = "unavailable" in read ? [] : read.rules.pullRequests;
|
|
2145
2321
|
const wanted = options.allowed ?? "rebase";
|
|
2146
2322
|
const repositoryAllows = {
|
|
2147
2323
|
rebase: settings.allowRebaseMerge,
|
|
2148
2324
|
squash: settings.allowSquashMerge,
|
|
2149
2325
|
merge: settings.allowMergeCommit
|
|
2150
2326
|
};
|
|
2151
|
-
const available = METHODS.filter((method) => repositoryAllows[method] &&
|
|
2327
|
+
const available = METHODS.filter((method) => repositoryAllows[method] && pullRequests.every((rule) => rule.allowedMergeMethods === void 0 || rule.allowedMergeMethods.includes(method)));
|
|
2152
2328
|
const violations = available.filter((method) => method !== wanted).map((method) => ({
|
|
2153
2329
|
code: "settings-merge-methods/method-enabled",
|
|
2154
2330
|
message: `${file} allows ${method} merges; only ${wanted} should be allowed`,
|
|
@@ -2547,14 +2723,20 @@ const REGISTRY_TOKEN_VARIABLES = ["NODE_AUTH_TOKEN", "NPM_TOKEN"];
|
|
|
2547
2723
|
/**
|
|
2548
2724
|
* The branch requires the check of every junction job of the workflows, and requires branches to be up to date before merging (strict mode). The check name of a job is its `name`, else its id.
|
|
2549
2725
|
*
|
|
2550
|
-
* The junction job is the one that reports for the whole workflow, so requiring it requires everything it needs, including jobs a path filter may skip. Codes: `no-required-checks` (no `required_status_checks` rule applies), `required-check-missing` (a junction job's check is not among the required ones), `not-strict` (no rule sets the strict policy)
|
|
2726
|
+
* The junction job is the one that reports for the whole workflow, so requiring it requires everything it needs, including jobs a path filter may skip. Codes: `no-required-checks` (no `required_status_checks` rule applies), `required-check-missing` (a junction job's check is not among the required ones), `not-strict` (no rule sets the strict policy), `no-junction-job` (no workflow has a job with an id from `junctionJobs`) and `rules-unavailable` (the repository's plan has no rulesets, so no check can be required; reported alone, with GitHub's message).
|
|
2551
2727
|
*
|
|
2552
2728
|
* Limits: it reads the rules of rulesets that apply to the branch; classic branch protection is not read, so a repository that uses it is reported as having no required checks. A junction job in a reusable workflow, or a name that is an expression, is not resolved.
|
|
2553
2729
|
*/
|
|
2554
2730
|
const settingsRequiredChecks = async ({ cwd, options, github }) => {
|
|
2555
2731
|
const slug = await repositoryOf(cwd, options);
|
|
2556
2732
|
const file = repositoryFile(slug);
|
|
2557
|
-
const
|
|
2733
|
+
const read = await branchRulesOf(github, slug, options);
|
|
2734
|
+
if ("unavailable" in read) return [{
|
|
2735
|
+
code: "settings-required-checks/rules-unavailable",
|
|
2736
|
+
message: rulesUnavailableMessage(file, "a status check", read.unavailable),
|
|
2737
|
+
file
|
|
2738
|
+
}];
|
|
2739
|
+
const { rules } = read;
|
|
2558
2740
|
const junctions = (await loadWorkflows(cwd, options)).flatMap((workflow) => jobsNamed(workflow, options.junctionJobs ?? DEFAULT_JUNCTION_JOBS).map((job) => ({
|
|
2559
2741
|
workflow,
|
|
2560
2742
|
name: job.name ?? job.id
|
|
@@ -2591,13 +2773,20 @@ const settingsRequiredChecks = async ({ cwd, options, github }) => {
|
|
|
2591
2773
|
/**
|
|
2592
2774
|
* A pull request cannot merge while a review conversation is unresolved: a `pull_request` rule that applies to the branch sets `required_review_thread_resolution`.
|
|
2593
2775
|
*
|
|
2776
|
+
* When the repository's plan has no rulesets it reports `rules-unavailable` with GitHub's message, since nothing can require it.
|
|
2777
|
+
*
|
|
2594
2778
|
* Limits: it reads the rules of rulesets; classic branch protection is not read.
|
|
2595
2779
|
*/
|
|
2596
2780
|
const settingsReviewThreadResolution = async ({ cwd, options, github }) => {
|
|
2597
2781
|
const slug = await repositoryOf(cwd, options);
|
|
2598
|
-
const
|
|
2782
|
+
const read = await branchRulesOf(github, slug, options);
|
|
2599
2783
|
const file = repositoryFile(slug);
|
|
2600
|
-
|
|
2784
|
+
if ("unavailable" in read) return [{
|
|
2785
|
+
code: "settings-review-thread-resolution/rules-unavailable",
|
|
2786
|
+
message: rulesUnavailableMessage(file, "resolving review conversations", read.unavailable),
|
|
2787
|
+
file
|
|
2788
|
+
}];
|
|
2789
|
+
return read.rules.pullRequests.some((rule) => rule.requiredReviewThreadResolution) ? [] : [{
|
|
2601
2790
|
code: "settings-review-thread-resolution/not-required",
|
|
2602
2791
|
message: `no rule requires review conversations to be resolved before merging on the branch of ${file}`,
|
|
2603
2792
|
file
|
|
@@ -3577,76 +3766,6 @@ const conformanceSchema = z.strictObject({ checks });
|
|
|
3577
3766
|
*/
|
|
3578
3767
|
const conformanceSection = defineSection("conformance", conformanceSchema);
|
|
3579
3768
|
//#endregion
|
|
3580
|
-
//#region src/github.ts
|
|
3581
|
-
const repositorySchema = z.object({
|
|
3582
|
-
default_branch: z.string(),
|
|
3583
|
-
allow_rebase_merge: z.optional(z.boolean()),
|
|
3584
|
-
allow_squash_merge: z.optional(z.boolean()),
|
|
3585
|
-
allow_merge_commit: z.optional(z.boolean())
|
|
3586
|
-
});
|
|
3587
|
-
const rulesSchema = z.array(z.looseObject({
|
|
3588
|
-
type: z.string(),
|
|
3589
|
-
parameters: z.optional(z.looseObject({
|
|
3590
|
-
required_status_checks: z.optional(z.array(z.object({ context: z.string() }))),
|
|
3591
|
-
strict_required_status_checks_policy: z.optional(z.boolean()),
|
|
3592
|
-
required_review_thread_resolution: z.optional(z.boolean()),
|
|
3593
|
-
allowed_merge_methods: z.optional(z.array(z.string()))
|
|
3594
|
-
}))
|
|
3595
|
-
}));
|
|
3596
|
-
const API_ROOT = "https://api.github.com";
|
|
3597
|
-
const API_VERSION = "2022-11-28";
|
|
3598
|
-
/**
|
|
3599
|
-
* How many rules the API returns in one page, its maximum.
|
|
3600
|
-
*/
|
|
3601
|
-
const PAGE_SIZE = 100;
|
|
3602
|
-
/**
|
|
3603
|
-
* A {@link GitHubClient} over the REST API.
|
|
3604
|
-
*/
|
|
3605
|
-
function createGitHubClient(options) {
|
|
3606
|
-
const request = options.fetch ?? fetch;
|
|
3607
|
-
const root = options.apiUrl ?? API_ROOT;
|
|
3608
|
-
async function get(path) {
|
|
3609
|
-
const response = await request(`${root}${path}`, { headers: {
|
|
3610
|
-
Accept: "application/vnd.github+json",
|
|
3611
|
-
Authorization: `Bearer ${options.token}`,
|
|
3612
|
-
"X-GitHub-Api-Version": API_VERSION
|
|
3613
|
-
} });
|
|
3614
|
-
if (!response.ok) throw new ConformanceError(`GET ${path} failed: ${String(response.status)} ${response.statusText}`);
|
|
3615
|
-
return response.json();
|
|
3616
|
-
}
|
|
3617
|
-
return {
|
|
3618
|
-
repository: async (slug) => {
|
|
3619
|
-
const parsed = repositorySchema.parse(await get(`/repos/${slug.owner}/${slug.name}`));
|
|
3620
|
-
const { allow_rebase_merge: allowRebaseMerge, allow_squash_merge: allowSquashMerge, allow_merge_commit: allowMergeCommit } = parsed;
|
|
3621
|
-
if (allowRebaseMerge === void 0 || allowSquashMerge === void 0 || allowMergeCommit === void 0) throw new ConformanceError(`GET /repos/${slug.owner}/${slug.name} did not report the merge-method settings, which GitHub shows only to a token with write access to the repository`);
|
|
3622
|
-
return {
|
|
3623
|
-
defaultBranch: parsed.default_branch,
|
|
3624
|
-
allowRebaseMerge,
|
|
3625
|
-
allowSquashMerge,
|
|
3626
|
-
allowMergeCommit
|
|
3627
|
-
};
|
|
3628
|
-
},
|
|
3629
|
-
branchRules: async (slug, branch) => {
|
|
3630
|
-
const rules = [];
|
|
3631
|
-
for (let page = 1, more = true; more; page += 1) {
|
|
3632
|
-
const found = rulesSchema.parse(await get(`/repos/${slug.owner}/${slug.name}/rules/branches/${encodeURIComponent(branch)}?per_page=${String(PAGE_SIZE)}&page=${String(page)}`));
|
|
3633
|
-
rules.push(...found);
|
|
3634
|
-
more = found.length === PAGE_SIZE;
|
|
3635
|
-
}
|
|
3636
|
-
return {
|
|
3637
|
-
requiredStatusChecks: rules.filter((rule) => rule.type === "required_status_checks").map((rule) => ({
|
|
3638
|
-
contexts: (rule.parameters?.required_status_checks ?? []).map((check) => check.context),
|
|
3639
|
-
strict: rule.parameters?.strict_required_status_checks_policy === true
|
|
3640
|
-
})),
|
|
3641
|
-
pullRequests: rules.filter((rule) => rule.type === "pull_request").map((rule) => ({
|
|
3642
|
-
requiredReviewThreadResolution: rule.parameters?.required_review_thread_resolution === true,
|
|
3643
|
-
allowedMergeMethods: rule.parameters?.allowed_merge_methods
|
|
3644
|
-
}))
|
|
3645
|
-
};
|
|
3646
|
-
}
|
|
3647
|
-
};
|
|
3648
|
-
}
|
|
3649
|
-
//#endregion
|
|
3650
3769
|
//#region src/registry.ts
|
|
3651
3770
|
/**
|
|
3652
3771
|
* The options of an enabled check, or `undefined` when the setting is absent or `false`.
|
|
@@ -3661,12 +3780,13 @@ function plain(spec) {
|
|
|
3661
3780
|
requiresLayout: false,
|
|
3662
3781
|
requiresGitHub: false,
|
|
3663
3782
|
isEnabled: (checks) => spec.select(checks) !== void 0,
|
|
3664
|
-
run: async ({ cwd, checks, configFiles }) => {
|
|
3783
|
+
run: async ({ cwd, checks, configFiles, note }) => {
|
|
3665
3784
|
const options = spec.select(checks);
|
|
3666
3785
|
if (options === void 0) throw new ConformanceError(`the check '${spec.name}' is not enabled`);
|
|
3667
3786
|
return spec.check({
|
|
3668
3787
|
cwd,
|
|
3669
3788
|
options,
|
|
3789
|
+
note,
|
|
3670
3790
|
...configFiles === void 0 ? {} : { configFiles }
|
|
3671
3791
|
});
|
|
3672
3792
|
}
|
|
@@ -3679,7 +3799,7 @@ function online(spec) {
|
|
|
3679
3799
|
requiresLayout: false,
|
|
3680
3800
|
requiresGitHub: true,
|
|
3681
3801
|
isEnabled: (checks) => spec.select(checks) !== void 0,
|
|
3682
|
-
run: async ({ cwd, checks, configFiles, github }) => {
|
|
3802
|
+
run: async ({ cwd, checks, configFiles, github, note }) => {
|
|
3683
3803
|
const options = spec.select(checks);
|
|
3684
3804
|
if (options === void 0) throw new ConformanceError(`the check '${spec.name}' is not enabled`);
|
|
3685
3805
|
if (github === void 0) throw new ConformanceError(`the check '${spec.name}' reads the repository's settings and needs a GitHub client`);
|
|
@@ -3687,6 +3807,7 @@ function online(spec) {
|
|
|
3687
3807
|
cwd,
|
|
3688
3808
|
options,
|
|
3689
3809
|
github,
|
|
3810
|
+
note,
|
|
3690
3811
|
...configFiles === void 0 ? {} : { configFiles }
|
|
3691
3812
|
});
|
|
3692
3813
|
}
|
|
@@ -3911,7 +4032,7 @@ async function conformanceOf(options) {
|
|
|
3911
4032
|
cwd: options.cwd
|
|
3912
4033
|
});
|
|
3913
4034
|
if (loaded === void 0) throw new ConformanceError(`no 'conformance' section in ${options.cwd}; define it in exadev.config.ts or in exadev.conformance.config.ts`);
|
|
3914
|
-
return loaded;
|
|
4035
|
+
return loaded.value;
|
|
3915
4036
|
}
|
|
3916
4037
|
async function layoutOf(options, needed) {
|
|
3917
4038
|
if (options.layout !== void 0) return validateStandard(layoutSection.schema, options.layout, `'${layoutSection.name}' section passed to runChecks`);
|
|
@@ -3921,7 +4042,7 @@ async function layoutOf(options, needed) {
|
|
|
3921
4042
|
cwd: options.cwd
|
|
3922
4043
|
});
|
|
3923
4044
|
if (loaded === void 0) throw new ConformanceError(`${needed.join(", ")} read the workspace layout, and there is no 'layout' section in ${options.cwd}; define it in exadev.config.ts or in exadev.layout.config.ts`);
|
|
3924
|
-
return loaded;
|
|
4045
|
+
return loaded.value;
|
|
3925
4046
|
}
|
|
3926
4047
|
function selected(config, requested, online) {
|
|
3927
4048
|
const enabled = checkNames.filter((name) => registry[name].isEnabled(config.checks));
|
|
@@ -3971,7 +4092,11 @@ function readImported(imported, name) {
|
|
|
3971
4092
|
*
|
|
3972
4093
|
* Violations do not throw. It throws `ConformanceError` when the checks cannot run (nothing enabled, a requested check disabled, a section missing) and `ConfigValidationError` when a section fails its schema, whether it was loaded from `cwd` or passed in.
|
|
3973
4094
|
*/
|
|
3974
|
-
async function runChecks(
|
|
4095
|
+
async function runChecks(given) {
|
|
4096
|
+
const options = {
|
|
4097
|
+
...given,
|
|
4098
|
+
cwd: resolve(given.cwd)
|
|
4099
|
+
};
|
|
3975
4100
|
const config = await conformanceOf(options);
|
|
3976
4101
|
const names = selected(config, options.checks, options.github !== void 0);
|
|
3977
4102
|
const layout = await layoutOf(options, names.filter((name) => registry[name].requiresLayout));
|
|
@@ -3979,16 +4104,22 @@ async function runChecks(options) {
|
|
|
3979
4104
|
const results = [];
|
|
3980
4105
|
for (const name of names) {
|
|
3981
4106
|
const check = registry[name];
|
|
4107
|
+
const notes = [];
|
|
4108
|
+
const note = (text) => {
|
|
4109
|
+
notes.push(text);
|
|
4110
|
+
};
|
|
3982
4111
|
const violations = "run" in check ? await check.run({
|
|
3983
4112
|
cwd: options.cwd,
|
|
3984
4113
|
checks: config.checks,
|
|
3985
4114
|
layout,
|
|
3986
4115
|
configFiles: options.configFiles,
|
|
3987
|
-
github: options.github
|
|
4116
|
+
github: options.github,
|
|
4117
|
+
note
|
|
3988
4118
|
}) : readImported(imported, name);
|
|
3989
4119
|
results.push({
|
|
3990
4120
|
check: name,
|
|
3991
|
-
violations
|
|
4121
|
+
violations,
|
|
4122
|
+
notes
|
|
3992
4123
|
});
|
|
3993
4124
|
}
|
|
3994
4125
|
const violations = results.flatMap((result) => result.violations);
|
|
@@ -4092,4 +4223,4 @@ function proveExhaustive(input) {
|
|
|
4092
4223
|
}) };
|
|
4093
4224
|
}
|
|
4094
4225
|
//#endregion
|
|
4095
|
-
export { ConformanceError, DEFAULT_DERIVED_TYPE_INFERENCES, DEFAULT_INFERENCES, DEFAULT_WORKFLOWS, EXIT_CODES, ExhaustivenessError, PRESET_COMMIT_TYPES, accessOf, aggregateMappers, checkNames, classifyPackages, codecPairs, commandTypes, commitTypes, conformanceSchema, conformanceSection, createGitHubClient, crossSliceRules, cycleRule, deployTools, derivedTypes, dockerfilePackageManager, effectiveEnv, effectivePermissions, enginesFloor, eslint, generators, handlerCases, importCrossSlice, importCycles, importIsolatedGroups, importRankSkip, importUphill, instructionSymlinks, isCheckName, isolatedGroupRules, loadWorkflows, migrationsDirectory, packagesPattern, parseUses, parseWorkflow, proveExhaustive, rankSkipRules, registry, removeCase, runChecks, settingsMergeMethods, settingsRequiredChecks, settingsReviewThreadResolution, singleStorybook, transitiveNeeds, typeErrors, uphillRules, workflowActionPinning, workflowCredentials, workflowJobOrdering, workflowMergeGroup, workflowReleaseJob, workflowRepositoryDispatch, workflowRunnerResolution, workflowSkippableJobs, workflowUpdateBotCooldown, workflowVersionSingleSource };
|
|
4226
|
+
export { ConformanceError, DEFAULT_DERIVED_TYPE_INFERENCES, DEFAULT_INFERENCES, DEFAULT_WORKFLOWS, EXIT_CODES, ExhaustivenessError, PRESET_COMMIT_TYPES, RulesetsUnavailableError, accessOf, aggregateMappers, checkNames, classifyPackages, codecPairs, commandTypes, commitTypes, conformanceSchema, conformanceSection, createGitHubClient, crossSliceRules, cycleRule, deployTools, derivedTypes, dockerfilePackageManager, effectiveEnv, effectivePermissions, enginesFloor, eslint, generators, handlerCases, importCrossSlice, importCycles, importIsolatedGroups, importRankSkip, importUphill, instructionSymlinks, isCheckName, isolatedGroupRules, loadWorkflows, migrationsDirectory, packagesPattern, parseUses, parseWorkflow, proveExhaustive, rankSkipRules, registry, removeCase, runChecks, settingsMergeMethods, settingsRequiredChecks, settingsReviewThreadResolution, singleStorybook, transitiveNeeds, typeErrors, uphillRules, workflowActionPinning, workflowCredentials, workflowJobOrdering, workflowMergeGroup, workflowReleaseJob, workflowRepositoryDispatch, workflowRunnerResolution, workflowSkippableJobs, workflowUpdateBotCooldown, workflowVersionSingleSource };
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "workspace-conformance",
|
|
3
3
|
"description": "Conformance checks for a pnpm workspace that ESLint cannot express: type-graph, import-graph and cross-file checks driven by the shared layout section of @exadev/config",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "2.0.1",
|
|
5
5
|
"bin": {
|
|
6
6
|
"workspace-conformance": "./dist/cli.js"
|
|
7
7
|
},
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@exadev/config": "^1.0
|
|
9
|
+
"@exadev/config": "^2.1.0",
|
|
10
10
|
"cosmiconfig-extends": "^1.0.1",
|
|
11
11
|
"dependency-cruiser": "^18.4.0",
|
|
12
12
|
"jsonc-parser": "^3.3.1",
|