workspace-conformance 1.2.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -2
- package/dist/cli.js +192 -0
- package/dist/index.cjs +193 -0
- package/dist/index.d.cts +54 -1
- package/dist/index.d.ts +54 -1
- package/dist/index.js +193 -1
- package/package.json +7 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ It complements the workspace architecture rules of [`@exadev/eslint-config`](htt
|
|
|
10
10
|
pnpm add -D workspace-conformance @exadev/config cosmiconfig 'typescript@^6'
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
`cosmiconfig` (`^9.0.0 || ^10.0.0`) is a peer dependency because `@exadev/config` loads config files through it. `typescript` (`^5.9.0 || ^6.0.0`) is a peer dependency because dependency-cruiser needs it to parse TypeScript and supports versions below 7 (the range is in the install command because an unpinned `typescript` installs a newer major, which pnpm only warns about); the import checks fail with a configuration error when dependency-cruiser cannot load it. ts-morph bundles its own compiler and does not use yours. The package supports the Node lines dependency-cruiser and its own dependencies support: 22.13 and later 22, 24, and 26 and later.
|
|
13
|
+
`cosmiconfig` (`^9.0.0 || ^10.0.0`) is a peer dependency because `@exadev/config` loads config files through it. `typescript` (`^5.9.0 || ^6.0.0`) is a peer dependency because dependency-cruiser needs it to parse TypeScript and supports versions below 7 (the range is in the install command because an unpinned `typescript` installs a newer major, which pnpm only warns about); the import checks fail with a configuration error when dependency-cruiser cannot load it. ts-morph bundles its own compiler and does not use yours. `eslint` (`^9.0.0 || ^10.0.0`) is an optional peer dependency, needed only by the `eslint` check, which uses the copy installed in the repository it checks. The package supports the Node lines dependency-cruiser and its own dependencies support: 22.13 and later 22, 24, and 26 and later.
|
|
14
14
|
|
|
15
15
|
Configure the checks in `exadev.config.ts`:
|
|
16
16
|
|
|
@@ -161,6 +161,33 @@ False positives and blind spots:
|
|
|
161
161
|
- Config variants: only the directory searched is looked at, whereas wrangler also finds a config in a parent directory and follows a redirected config; a repository with both `wrangler.json` and `wrangler.toml` is judged by the first, and a `migrations_pattern` that narrows which files are applied is not read. A generator config that computes `out` from the environment is judged by its value in the process running the check.
|
|
162
162
|
- Scripts: a script is matched by the text of the command, so `drizzle-kit migrate` run against a local database for tests is reported, and a command assembled in a shell variable or run from a file the script calls is not seen. Narrow `packageJsons` to exempt a package.
|
|
163
163
|
|
|
164
|
+
### ESLint
|
|
165
|
+
|
|
166
|
+
**`eslint`** proves that the repository's own ESLint is applied. Every other lint rule runs inside ESLint, so none of them can notice a config that dropped a shared preset, switched its rules off or narrowed `ignores` until nothing is linted; the repository then looks configured and enforces nothing. This check asks ESLint itself, through its Node API, and carries no ESLint config of its own: a flat config holds plugin objects, a parser and functions, and editors and CI expect `eslint.config.*` where it already is.
|
|
167
|
+
|
|
168
|
+
ESLint (`^9.0.0 || ^10.0.0`) is an optional peer dependency, needed only by this check. It is resolved from the directory the checks run in, the way a module there would find it, and not from this package, so the answer is the one the repository's own lint script gets. When `eslint` does not resolve from there the run fails with a configuration error. When ESLint finds no config file for a sample the check reports `eslint/no-config` and does not substitute one.
|
|
169
|
+
|
|
170
|
+
| Option | Meaning |
|
|
171
|
+
|---|---|
|
|
172
|
+
| `samples` | Required. One file per kind of source the repository lints (`src/index.ts`, `package.json`, `eslint.config.ts`), as a path or `{ path, rules }`. A path need not exist: ESLint resolves a file's configuration from its path alone. |
|
|
173
|
+
| `rules` | Rules required for every sample, each with the severity it must have at least: `warn` is met by `warn` or `error`, `error` only by `error`. Rule entries in the form `[severity, ...options]` and numeric severities are read as their severity. A sample's own `rules` are added to these and win. |
|
|
174
|
+
| `configFile` | A config file to judge instead of the one ESLint finds from the working directory. |
|
|
175
|
+
| `lint` | Also lint the workspace and report every message it produces. Off when omitted. |
|
|
176
|
+
| `lintPatterns` | What `lint` lints, as file and directory patterns. `.` when omitted. |
|
|
177
|
+
|
|
178
|
+
The default level lints nothing. For each sample it calls `calculateConfigForFile`, the same call the CLI's `--print-config` makes, which applies `files` and `ignores`. A file that is ignored, or that no configuration block matches, resolves to no configuration (`isPathIgnored` is that call returning nothing), and that is `eslint/not-linted`. Otherwise each required rule must be configured at the required severity: `eslint/rule-missing`, `eslint/rule-off` or `eslint/rule-too-weak`, each naming the rule and the file.
|
|
179
|
+
|
|
180
|
+
With `lint` the check also lints the workspace through `lintFiles` and maps every message to a violation at its position. The code names the rule, `eslint/lint/<rule id>`, so `eslint/lint/no-console` and `eslint/lint/@scope/plugin/rule` are stable; a parsing error is `eslint/fatal` and a message with no rule, such as an unused disable directive, is `eslint/lint-message`. A pattern that matches no file, or only ignored files, is `eslint/nothing-linted`. Warnings are reported as well as errors, since a repository that fails its lint script on warnings (`--max-warnings 0`) is judged by what that script reports.
|
|
181
|
+
|
|
182
|
+
Limits:
|
|
183
|
+
|
|
184
|
+
- An external config is a black box. The check sees which rules are active on a file, not why, so it cannot tell a rule enabled by a shared preset from one written locally. Require the rules that matter and let the repository decide how it gets them.
|
|
185
|
+
- It reads the config ESLint resolves for each sample, and a sample stands for the files like it. A rule can be off for one directory the samples do not cover, so choose a sample for each kind of source and each directory with its own override.
|
|
186
|
+
- An ignored file and a file that no configuration block matches both resolve to nothing and are reported the same way.
|
|
187
|
+
- Only rule severities are compared, not rule options.
|
|
188
|
+
- Flat config only: the ESLint 9 and later `ESLint` class. Legacy `.eslintrc` configuration is not read.
|
|
189
|
+
- A config that is a TypeScript file needs whatever ESLint itself needs to load it (`jiti`), installed where ESLint is.
|
|
190
|
+
|
|
164
191
|
### Workflows
|
|
165
192
|
|
|
166
193
|
The `workflow-*` checks read GitHub Actions workflow files and run offline. Each file under `.github/workflows` is parsed into a model of its events, jobs, the transitive `needs` graph, `if` conditions, effective permissions (a job's own declaration replaces the workflow's, it is not merged with it) and effective environment (workflow, then job, then step), and the checks read that model. A file that is not YAML, or is not shaped like a workflow, fails the run with a configuration error instead of being skipped. Expressions (`${{ ... }}`) are never evaluated: a value that is an expression is treated as unknown, and a check that needs a literal ignores it.
|
|
@@ -256,7 +283,7 @@ it('conforms to the workspace layout', async () => {
|
|
|
256
283
|
});
|
|
257
284
|
```
|
|
258
285
|
|
|
259
|
-
Every check is also exported as a function taking `{ cwd, options }` (and `layout` for the import checks): `aggregateMappers`, `commandTypes`, `importUphill`, `importRankSkip`, `importCrossSlice`, `importIsolatedGroups`, `importCycles`, `instructionSymlinks`, `singleStorybook`, `commitTypes`, `dockerfilePackageManager`, `migrationsDirectory`, the workflow checks (`workflowJobOrdering`, `workflowSkippableJobs`, `workflowRunnerResolution`, `workflowVersionSingleSource`, `workflowCredentials`, `workflowRepositoryDispatch`, `workflowUpdateBotCooldown`, `workflowMergeGroup` and `workflowActionPinning`) and, taking `{ cwd, options, github }`, the settings checks (`settingsMergeMethods`, `settingsRequiredChecks` and `settingsReviewThreadResolution`). These take options and a layout as already validated and do not check them again; go through `runChecks` for validation. `parseWorkflow` and `loadWorkflows` expose the workflow model the workflow checks read. A check is a function that returns `Promise<readonly Violation[]>`, where a `Violation` is `{ code, message, file, location? }`.
|
|
286
|
+
Every check is also exported as a function taking `{ cwd, options }` (and `layout` for the import checks): `aggregateMappers`, `commandTypes`, `importUphill`, `importRankSkip`, `importCrossSlice`, `importIsolatedGroups`, `importCycles`, `instructionSymlinks`, `singleStorybook`, `commitTypes`, `dockerfilePackageManager`, `migrationsDirectory`, `eslint`, the workflow checks (`workflowJobOrdering`, `workflowSkippableJobs`, `workflowRunnerResolution`, `workflowVersionSingleSource`, `workflowCredentials`, `workflowRepositoryDispatch`, `workflowUpdateBotCooldown`, `workflowMergeGroup` and `workflowActionPinning`) and, taking `{ cwd, options, github }`, the settings checks (`settingsMergeMethods`, `settingsRequiredChecks` and `settingsReviewThreadResolution`). These take options and a layout as already validated and do not check them again; go through `runChecks` for validation. `parseWorkflow` and `loadWorkflows` expose the workflow model the workflow checks read. A check is a function that returns `Promise<readonly Violation[]>`, where a `Violation` is `{ code, message, file, location? }`.
|
|
260
287
|
|
|
261
288
|
### Proving a handler map is exhaustive
|
|
262
289
|
|
|
@@ -303,6 +330,10 @@ CI also runs `pnpm exec publint` and `pnpm exec attw --pack` after the build, an
|
|
|
303
330
|
|
|
304
331
|
Each check has a fixture that violates it and one that does not, under `test/fixtures`; the file-tree checks that need git build a repository in a temporary directory, the workflow checks have their fixtures under `test/fixtures/workflows`, and the settings checks run against an in-memory `GitHubClient`. The fixtures are excluded from the project's own typecheck and lint.
|
|
305
332
|
|
|
333
|
+
### Releases
|
|
334
|
+
|
|
335
|
+
Every push to `main` runs semantic-release once the commitlint, lint, typecheck and package jobs pass. It derives the next version from the Conventional Commits since the last tag, publishes to npm through trusted publishing with provenance, creates the tag and the GitHub Release, and pushes a `chore(release)` commit that updates `CHANGELOG.md` and `package.json`. `main` is protected by a ruleset that requires the Required Checks status, a branch up to date with `main`, linear history, and forbids deletion and non-fast-forward pushes. Pull requests therefore reach `main` by rebase merge once checks pass, and the release commit and tag are pushed over SSH with the repository deploy key, which the ruleset lets bypass it (as it does organisation admins). The key exists on disk only for the release step.
|
|
336
|
+
|
|
306
337
|
CI selects its runner with `ExaDev/runner-fallback-action` (self-hosted fleet first, Blacksmith as fallback). The release job stays on a GitHub-hosted runner because npm trusted publishing needs one, and publishes with provenance through OIDC, with no stored token. The repository deploy key (the `RELEASE_DEPLOY_KEY` secret) is a ruleset bypass actor, so it is written to disk by the step immediately before the release step, and only the release step's own environment carries `GIT_SSH_COMMAND` (absolute `/usr/bin/ssh`, no ssh config, a pinned GitHub host key, only the ssh protocol allowed); the key is deleted afterwards. The build and package checks run before the key exists and the release step sets `NPM_CONFIG_IGNORE_SCRIPTS`, so no dev dependency runs beside the key, the job's actions are pinned by commit SHA, and the checkout, the action setup and the dependency install run with no write-capable credential in git config. That limits what code from those steps can read; it is not a sandbox, since a process started earlier in the job still runs as the same user. `release.config.ts` sets an SSH `repositoryUrl`, so semantic-release pushes the release commit and tag over SSH. The default `GITHUB_TOKEN` cannot bypass a branch ruleset on `main`, and semantic-release falls back to pushing as it only when the SSH push fails, so the job dry-run pushes first and fails before publishing if the key does not authenticate, and refuses the https URL outright. Commits follow [Conventional Commits](https://www.conventionalcommits.org); semantic-release derives the version from them.
|
|
307
338
|
|
|
308
339
|
`README.md`, `AGENTS.md` and `CLAUDE.md` are one file: the two instruction files are tracked symlinks to this one.
|
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
2
3
|
import { dirname, extname, join, posix, relative, resolve, sep } from "node:path";
|
|
3
4
|
import { parseArgs } from "node:util";
|
|
4
5
|
import { z } from "zod";
|
|
@@ -2498,6 +2499,179 @@ const importUphillSpec = {
|
|
|
2498
2499
|
})
|
|
2499
2500
|
};
|
|
2500
2501
|
//#endregion
|
|
2502
|
+
//#region src/checks/eslint.ts
|
|
2503
|
+
/**
|
|
2504
|
+
* The patterns the full level lints when the options name none.
|
|
2505
|
+
*/
|
|
2506
|
+
const DEFAULT_LINT_PATTERNS = ["."];
|
|
2507
|
+
const SEVERITY_RANK = {
|
|
2508
|
+
off: 0,
|
|
2509
|
+
warn: 1,
|
|
2510
|
+
error: 2
|
|
2511
|
+
};
|
|
2512
|
+
function hasEslintClass(value) {
|
|
2513
|
+
return isRecord(value) && typeof value["ESLint"] === "function";
|
|
2514
|
+
}
|
|
2515
|
+
/**
|
|
2516
|
+
* The `ESLint` class of the ESLint installed where the checks run, found the way a module in `cwd` finds it. It is not this package's own copy, so the results agree with the repository's own lint script.
|
|
2517
|
+
*/
|
|
2518
|
+
function loadEslint(cwd) {
|
|
2519
|
+
const requireFromCwd = createRequire(join(cwd, "package.json"));
|
|
2520
|
+
try {
|
|
2521
|
+
requireFromCwd.resolve("eslint");
|
|
2522
|
+
} catch (error) {
|
|
2523
|
+
throw new ConformanceError(`the eslint check uses the repository's own ESLint, and 'eslint' does not resolve from ${cwd}; install it there`, { cause: error });
|
|
2524
|
+
}
|
|
2525
|
+
const loaded = requireFromCwd("eslint");
|
|
2526
|
+
if (!hasEslintClass(loaded)) throw new ConformanceError(`the 'eslint' that resolves from ${cwd} does not export the ESLint class of the Node API`);
|
|
2527
|
+
return loaded.ESLint;
|
|
2528
|
+
}
|
|
2529
|
+
/**
|
|
2530
|
+
* The severity a rule entry resolves to. An entry is a severity or an array whose first element is one, and a severity is `0`, `1`, `2`, `'off'`, `'warn'` or `'error'`; `calculateConfigForFile` returns whichever form the config was written in.
|
|
2531
|
+
*/
|
|
2532
|
+
function normaliseSeverity(entry) {
|
|
2533
|
+
const severity = Array.isArray(entry) ? entry[0] : entry;
|
|
2534
|
+
if (severity === 0 || severity === "off") return "off";
|
|
2535
|
+
if (severity === 1 || severity === "warn") return "warn";
|
|
2536
|
+
if (severity === 2 || severity === "error") return "error";
|
|
2537
|
+
throw new ConformanceError(`ESLint resolved ${JSON.stringify(entry)} as a rule entry, which is not a severity or an array that starts with one`);
|
|
2538
|
+
}
|
|
2539
|
+
/**
|
|
2540
|
+
* The severity of each rule in a resolved configuration.
|
|
2541
|
+
*/
|
|
2542
|
+
function resolvedSeverities(config) {
|
|
2543
|
+
if (!isRecord(config)) throw new ConformanceError("ESLint resolved a configuration that is not an object");
|
|
2544
|
+
const { rules } = config;
|
|
2545
|
+
if (rules !== void 0 && !isRecord(rules)) throw new ConformanceError("ESLint resolved a configuration whose 'rules' is not an object");
|
|
2546
|
+
return new Map(Object.entries(rules ?? {}).map(([rule, entry]) => [rule, normaliseSeverity(entry)]));
|
|
2547
|
+
}
|
|
2548
|
+
/**
|
|
2549
|
+
* Whether `error` is the one `calculateConfigForFile` throws when ESLint finds no config file for the path.
|
|
2550
|
+
*/
|
|
2551
|
+
function isMissingConfig(error) {
|
|
2552
|
+
return isRecord(error) && error["messageTemplate"] === "config-file-missing";
|
|
2553
|
+
}
|
|
2554
|
+
function sampleOf(sample) {
|
|
2555
|
+
return typeof sample === "string" ? { path: sample } : sample;
|
|
2556
|
+
}
|
|
2557
|
+
function ruleViolations(file, required, actual) {
|
|
2558
|
+
return Object.entries(required).flatMap(([rule, severity]) => {
|
|
2559
|
+
const found = actual.get(rule);
|
|
2560
|
+
if (found === void 0) return [{
|
|
2561
|
+
code: "eslint/rule-missing",
|
|
2562
|
+
message: `the rule '${rule}' is not configured for ${file} (required: ${severity})`,
|
|
2563
|
+
file
|
|
2564
|
+
}];
|
|
2565
|
+
if (found === "off") return [{
|
|
2566
|
+
code: "eslint/rule-off",
|
|
2567
|
+
message: `the rule '${rule}' is off for ${file} (required: ${severity})`,
|
|
2568
|
+
file
|
|
2569
|
+
}];
|
|
2570
|
+
if (SEVERITY_RANK[found] < SEVERITY_RANK[severity]) return [{
|
|
2571
|
+
code: "eslint/rule-too-weak",
|
|
2572
|
+
message: `the rule '${rule}' is '${found}' for ${file} (required: ${severity})`,
|
|
2573
|
+
file
|
|
2574
|
+
}];
|
|
2575
|
+
return [];
|
|
2576
|
+
});
|
|
2577
|
+
}
|
|
2578
|
+
async function sampleViolations(eslint, sample, shared) {
|
|
2579
|
+
const file = sample.path;
|
|
2580
|
+
let config;
|
|
2581
|
+
try {
|
|
2582
|
+
config = await eslint.calculateConfigForFile(file);
|
|
2583
|
+
} catch (error) {
|
|
2584
|
+
if (isMissingConfig(error)) return [{
|
|
2585
|
+
code: "eslint/no-config",
|
|
2586
|
+
message: `ESLint finds no config file for ${file}; the check does not fall back to a config of its own`,
|
|
2587
|
+
file
|
|
2588
|
+
}];
|
|
2589
|
+
throw new ConformanceError(`ESLint could not resolve its configuration for ${file}`, { cause: error });
|
|
2590
|
+
}
|
|
2591
|
+
if (config === void 0) return [{
|
|
2592
|
+
code: "eslint/not-linted",
|
|
2593
|
+
message: `${file} is not linted: it is ignored, or no configuration block matches it`,
|
|
2594
|
+
file
|
|
2595
|
+
}];
|
|
2596
|
+
return ruleViolations(file, {
|
|
2597
|
+
...shared,
|
|
2598
|
+
...sample.rules
|
|
2599
|
+
}, resolvedSeverities(config));
|
|
2600
|
+
}
|
|
2601
|
+
/**
|
|
2602
|
+
* Whether `error` is ESLint refusing a pattern because it selects no file that is linted.
|
|
2603
|
+
*/
|
|
2604
|
+
function isNothingLinted(error) {
|
|
2605
|
+
return error instanceof Error && isRecord(error) && (error["messageTemplate"] === "file-not-found" || error["messageTemplate"] === "all-matched-files-ignored");
|
|
2606
|
+
}
|
|
2607
|
+
function messageViolation(cwd, result, message) {
|
|
2608
|
+
const file = relativePosix(cwd, result.filePath);
|
|
2609
|
+
const location = {
|
|
2610
|
+
line: message.line,
|
|
2611
|
+
column: message.column
|
|
2612
|
+
};
|
|
2613
|
+
if (message.ruleId !== null) {
|
|
2614
|
+
const severity = message.severity === 1 ? "warn" : "error";
|
|
2615
|
+
return {
|
|
2616
|
+
code: `eslint/lint/${message.ruleId}`,
|
|
2617
|
+
message: `${message.ruleId} (${severity}): ${message.message}`,
|
|
2618
|
+
file,
|
|
2619
|
+
location
|
|
2620
|
+
};
|
|
2621
|
+
}
|
|
2622
|
+
return message.fatal === true ? {
|
|
2623
|
+
code: "eslint/fatal",
|
|
2624
|
+
message: message.message,
|
|
2625
|
+
file,
|
|
2626
|
+
location
|
|
2627
|
+
} : {
|
|
2628
|
+
code: "eslint/lint-message",
|
|
2629
|
+
message: message.message,
|
|
2630
|
+
file,
|
|
2631
|
+
location
|
|
2632
|
+
};
|
|
2633
|
+
}
|
|
2634
|
+
function byPosition(left, right) {
|
|
2635
|
+
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);
|
|
2636
|
+
}
|
|
2637
|
+
/**
|
|
2638
|
+
* 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.
|
|
2639
|
+
*/
|
|
2640
|
+
async function lintViolations(cwd, eslint, patterns) {
|
|
2641
|
+
const violations = [];
|
|
2642
|
+
const results = /* @__PURE__ */ new Map();
|
|
2643
|
+
for (const pattern of patterns) try {
|
|
2644
|
+
for (const result of await eslint.lintFiles([pattern])) results.set(result.filePath, result);
|
|
2645
|
+
} catch (error) {
|
|
2646
|
+
if (!isNothingLinted(error)) throw new ConformanceError(`ESLint could not lint '${pattern}'`, { cause: error });
|
|
2647
|
+
violations.push({
|
|
2648
|
+
code: "eslint/nothing-linted",
|
|
2649
|
+
message: error.message,
|
|
2650
|
+
file: pattern
|
|
2651
|
+
});
|
|
2652
|
+
}
|
|
2653
|
+
for (const result of results.values()) for (const message of result.messages) violations.push(messageViolation(cwd, result, message));
|
|
2654
|
+
return violations.sort(byPosition);
|
|
2655
|
+
}
|
|
2656
|
+
/**
|
|
2657
|
+
* Proves the repository's own ESLint is applied, which no check that runs inside ESLint can notice: a config that dropped a shared preset, switched its rules off or narrowed `ignores` until nothing is linted still passes its own lint.
|
|
2658
|
+
*
|
|
2659
|
+
* It loads the ESLint that resolves from the directory the checks run in and resolves the repository's own config through the Node API, so the answer is the one the repository's lint script gets. By default it lints nothing: for each sample it reads the resolved configuration (the object `--print-config` prints) and reports a sample that is not linted, and each required rule that is missing, off or weaker than required. With `lint` it also lints the workspace and reports every message.
|
|
2660
|
+
*
|
|
2661
|
+
* 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.
|
|
2662
|
+
*/
|
|
2663
|
+
const eslint = async ({ cwd, options }) => {
|
|
2664
|
+
const instance = new (loadEslint(cwd))({
|
|
2665
|
+
cwd,
|
|
2666
|
+
...options.configFile === void 0 ? {} : { overrideConfigFile: resolve(cwd, options.configFile) }
|
|
2667
|
+
});
|
|
2668
|
+
const shared = options.rules ?? {};
|
|
2669
|
+
const violations = [];
|
|
2670
|
+
for (const sample of options.samples.map(sampleOf)) violations.push(...await sampleViolations(instance, sample, shared));
|
|
2671
|
+
if (options.lint === true) violations.push(...await lintViolations(cwd, instance, options.lintPatterns ?? DEFAULT_LINT_PATTERNS));
|
|
2672
|
+
return violations;
|
|
2673
|
+
};
|
|
2674
|
+
//#endregion
|
|
2501
2675
|
//#region src/checks/instruction-symlinks.ts
|
|
2502
2676
|
/**
|
|
2503
2677
|
* The instruction file names checked when the options name none.
|
|
@@ -2920,6 +3094,12 @@ const registry = {
|
|
|
2920
3094
|
select: (checks) => enabledOptions(checks["migrations-directory"]),
|
|
2921
3095
|
check: migrationsDirectory
|
|
2922
3096
|
}),
|
|
3097
|
+
eslint: plain({
|
|
3098
|
+
name: "eslint",
|
|
3099
|
+
description: "The repository's own ESLint lints the sample files with the required rules enabled at the required severity",
|
|
3100
|
+
select: (checks) => enabledOptions(checks.eslint),
|
|
3101
|
+
check: eslint
|
|
3102
|
+
}),
|
|
2923
3103
|
"workflow-job-ordering": plain({
|
|
2924
3104
|
name: "workflow-job-ordering",
|
|
2925
3105
|
description: "Release, deploy and junction jobs are ordered so a failure stops what follows",
|
|
@@ -3021,6 +3201,8 @@ function compiles(source) {
|
|
|
3021
3201
|
*/
|
|
3022
3202
|
const pattern = z.string().min(1).refine(compiles, "not a valid regular expression");
|
|
3023
3203
|
const patterns = z.array(pattern);
|
|
3204
|
+
const severity = z.enum(["warn", "error"]);
|
|
3205
|
+
const requiredRules = z.record(z.string().min(1), severity);
|
|
3024
3206
|
const selection = {
|
|
3025
3207
|
workflows: z.exactOptional(paths),
|
|
3026
3208
|
exclude: z.exactOptional(paths)
|
|
@@ -3080,6 +3262,16 @@ const checks = z.strictObject({
|
|
|
3080
3262
|
exclude: z.exactOptional(paths),
|
|
3081
3263
|
packageJson: z.exactOptional(path)
|
|
3082
3264
|
}))),
|
|
3265
|
+
eslint: z.exactOptional(enabled(z.strictObject({
|
|
3266
|
+
samples: z.array(z.union([path, z.strictObject({
|
|
3267
|
+
path,
|
|
3268
|
+
rules: z.exactOptional(requiredRules)
|
|
3269
|
+
})])).min(1),
|
|
3270
|
+
rules: z.exactOptional(requiredRules),
|
|
3271
|
+
configFile: z.exactOptional(path),
|
|
3272
|
+
lint: z.exactOptional(z.boolean()),
|
|
3273
|
+
lintPatterns: z.exactOptional(paths)
|
|
3274
|
+
}))),
|
|
3083
3275
|
"migrations-directory": z.exactOptional(enabled(z.strictObject({
|
|
3084
3276
|
generator: z.enum(generatorNames),
|
|
3085
3277
|
deployTool: z.enum(deployToolNames),
|
package/dist/index.cjs
CHANGED
|
@@ -5,6 +5,7 @@ let tinyglobby = require("tinyglobby");
|
|
|
5
5
|
let ts_morph = require("ts-morph");
|
|
6
6
|
let cosmiconfig_extends = require("cosmiconfig-extends");
|
|
7
7
|
let node_fs_promises = require("node:fs/promises");
|
|
8
|
+
let node_module = require("node:module");
|
|
8
9
|
let yaml = require("yaml");
|
|
9
10
|
let node_child_process = require("node:child_process");
|
|
10
11
|
let jsonc_parser = require("jsonc-parser");
|
|
@@ -543,6 +544,179 @@ const dockerfilePackageManager = async ({ cwd, options }) => {
|
|
|
543
544
|
return violations;
|
|
544
545
|
};
|
|
545
546
|
//#endregion
|
|
547
|
+
//#region src/checks/eslint.ts
|
|
548
|
+
/**
|
|
549
|
+
* The patterns the full level lints when the options name none.
|
|
550
|
+
*/
|
|
551
|
+
const DEFAULT_LINT_PATTERNS = ["."];
|
|
552
|
+
const SEVERITY_RANK = {
|
|
553
|
+
off: 0,
|
|
554
|
+
warn: 1,
|
|
555
|
+
error: 2
|
|
556
|
+
};
|
|
557
|
+
function hasEslintClass(value) {
|
|
558
|
+
return isRecord(value) && typeof value["ESLint"] === "function";
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* The `ESLint` class of the ESLint installed where the checks run, found the way a module in `cwd` finds it. It is not this package's own copy, so the results agree with the repository's own lint script.
|
|
562
|
+
*/
|
|
563
|
+
function loadEslint(cwd) {
|
|
564
|
+
const requireFromCwd = (0, node_module.createRequire)((0, node_path.join)(cwd, "package.json"));
|
|
565
|
+
try {
|
|
566
|
+
requireFromCwd.resolve("eslint");
|
|
567
|
+
} catch (error) {
|
|
568
|
+
throw new ConformanceError(`the eslint check uses the repository's own ESLint, and 'eslint' does not resolve from ${cwd}; install it there`, { cause: error });
|
|
569
|
+
}
|
|
570
|
+
const loaded = requireFromCwd("eslint");
|
|
571
|
+
if (!hasEslintClass(loaded)) throw new ConformanceError(`the 'eslint' that resolves from ${cwd} does not export the ESLint class of the Node API`);
|
|
572
|
+
return loaded.ESLint;
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* The severity a rule entry resolves to. An entry is a severity or an array whose first element is one, and a severity is `0`, `1`, `2`, `'off'`, `'warn'` or `'error'`; `calculateConfigForFile` returns whichever form the config was written in.
|
|
576
|
+
*/
|
|
577
|
+
function normaliseSeverity(entry) {
|
|
578
|
+
const severity = Array.isArray(entry) ? entry[0] : entry;
|
|
579
|
+
if (severity === 0 || severity === "off") return "off";
|
|
580
|
+
if (severity === 1 || severity === "warn") return "warn";
|
|
581
|
+
if (severity === 2 || severity === "error") return "error";
|
|
582
|
+
throw new ConformanceError(`ESLint resolved ${JSON.stringify(entry)} as a rule entry, which is not a severity or an array that starts with one`);
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* The severity of each rule in a resolved configuration.
|
|
586
|
+
*/
|
|
587
|
+
function resolvedSeverities(config) {
|
|
588
|
+
if (!isRecord(config)) throw new ConformanceError("ESLint resolved a configuration that is not an object");
|
|
589
|
+
const { rules } = config;
|
|
590
|
+
if (rules !== void 0 && !isRecord(rules)) throw new ConformanceError("ESLint resolved a configuration whose 'rules' is not an object");
|
|
591
|
+
return new Map(Object.entries(rules ?? {}).map(([rule, entry]) => [rule, normaliseSeverity(entry)]));
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Whether `error` is the one `calculateConfigForFile` throws when ESLint finds no config file for the path.
|
|
595
|
+
*/
|
|
596
|
+
function isMissingConfig(error) {
|
|
597
|
+
return isRecord(error) && error["messageTemplate"] === "config-file-missing";
|
|
598
|
+
}
|
|
599
|
+
function sampleOf(sample) {
|
|
600
|
+
return typeof sample === "string" ? { path: sample } : sample;
|
|
601
|
+
}
|
|
602
|
+
function ruleViolations(file, required, actual) {
|
|
603
|
+
return Object.entries(required).flatMap(([rule, severity]) => {
|
|
604
|
+
const found = actual.get(rule);
|
|
605
|
+
if (found === void 0) return [{
|
|
606
|
+
code: "eslint/rule-missing",
|
|
607
|
+
message: `the rule '${rule}' is not configured for ${file} (required: ${severity})`,
|
|
608
|
+
file
|
|
609
|
+
}];
|
|
610
|
+
if (found === "off") return [{
|
|
611
|
+
code: "eslint/rule-off",
|
|
612
|
+
message: `the rule '${rule}' is off for ${file} (required: ${severity})`,
|
|
613
|
+
file
|
|
614
|
+
}];
|
|
615
|
+
if (SEVERITY_RANK[found] < SEVERITY_RANK[severity]) return [{
|
|
616
|
+
code: "eslint/rule-too-weak",
|
|
617
|
+
message: `the rule '${rule}' is '${found}' for ${file} (required: ${severity})`,
|
|
618
|
+
file
|
|
619
|
+
}];
|
|
620
|
+
return [];
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
async function sampleViolations(eslint, sample, shared) {
|
|
624
|
+
const file = sample.path;
|
|
625
|
+
let config;
|
|
626
|
+
try {
|
|
627
|
+
config = await eslint.calculateConfigForFile(file);
|
|
628
|
+
} catch (error) {
|
|
629
|
+
if (isMissingConfig(error)) return [{
|
|
630
|
+
code: "eslint/no-config",
|
|
631
|
+
message: `ESLint finds no config file for ${file}; the check does not fall back to a config of its own`,
|
|
632
|
+
file
|
|
633
|
+
}];
|
|
634
|
+
throw new ConformanceError(`ESLint could not resolve its configuration for ${file}`, { cause: error });
|
|
635
|
+
}
|
|
636
|
+
if (config === void 0) return [{
|
|
637
|
+
code: "eslint/not-linted",
|
|
638
|
+
message: `${file} is not linted: it is ignored, or no configuration block matches it`,
|
|
639
|
+
file
|
|
640
|
+
}];
|
|
641
|
+
return ruleViolations(file, {
|
|
642
|
+
...shared,
|
|
643
|
+
...sample.rules
|
|
644
|
+
}, resolvedSeverities(config));
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Whether `error` is ESLint refusing a pattern because it selects no file that is linted.
|
|
648
|
+
*/
|
|
649
|
+
function isNothingLinted(error) {
|
|
650
|
+
return error instanceof Error && isRecord(error) && (error["messageTemplate"] === "file-not-found" || error["messageTemplate"] === "all-matched-files-ignored");
|
|
651
|
+
}
|
|
652
|
+
function messageViolation(cwd, result, message) {
|
|
653
|
+
const file = relativePosix(cwd, result.filePath);
|
|
654
|
+
const location = {
|
|
655
|
+
line: message.line,
|
|
656
|
+
column: message.column
|
|
657
|
+
};
|
|
658
|
+
if (message.ruleId !== null) {
|
|
659
|
+
const severity = message.severity === 1 ? "warn" : "error";
|
|
660
|
+
return {
|
|
661
|
+
code: `eslint/lint/${message.ruleId}`,
|
|
662
|
+
message: `${message.ruleId} (${severity}): ${message.message}`,
|
|
663
|
+
file,
|
|
664
|
+
location
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
return message.fatal === true ? {
|
|
668
|
+
code: "eslint/fatal",
|
|
669
|
+
message: message.message,
|
|
670
|
+
file,
|
|
671
|
+
location
|
|
672
|
+
} : {
|
|
673
|
+
code: "eslint/lint-message",
|
|
674
|
+
message: message.message,
|
|
675
|
+
file,
|
|
676
|
+
location
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
function byPosition(left, right) {
|
|
680
|
+
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);
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* 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.
|
|
684
|
+
*/
|
|
685
|
+
async function lintViolations(cwd, eslint, patterns) {
|
|
686
|
+
const violations = [];
|
|
687
|
+
const results = /* @__PURE__ */ new Map();
|
|
688
|
+
for (const pattern of patterns) try {
|
|
689
|
+
for (const result of await eslint.lintFiles([pattern])) results.set(result.filePath, result);
|
|
690
|
+
} catch (error) {
|
|
691
|
+
if (!isNothingLinted(error)) throw new ConformanceError(`ESLint could not lint '${pattern}'`, { cause: error });
|
|
692
|
+
violations.push({
|
|
693
|
+
code: "eslint/nothing-linted",
|
|
694
|
+
message: error.message,
|
|
695
|
+
file: pattern
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
for (const result of results.values()) for (const message of result.messages) violations.push(messageViolation(cwd, result, message));
|
|
699
|
+
return violations.sort(byPosition);
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Proves the repository's own ESLint is applied, which no check that runs inside ESLint can notice: a config that dropped a shared preset, switched its rules off or narrowed `ignores` until nothing is linted still passes its own lint.
|
|
703
|
+
*
|
|
704
|
+
* It loads the ESLint that resolves from the directory the checks run in and resolves the repository's own config through the Node API, so the answer is the one the repository's lint script gets. By default it lints nothing: for each sample it reads the resolved configuration (the object `--print-config` prints) and reports a sample that is not linted, and each required rule that is missing, off or weaker than required. With `lint` it also lints the workspace and reports every message.
|
|
705
|
+
*
|
|
706
|
+
* 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.
|
|
707
|
+
*/
|
|
708
|
+
const eslint = async ({ cwd, options }) => {
|
|
709
|
+
const instance = new (loadEslint(cwd))({
|
|
710
|
+
cwd,
|
|
711
|
+
...options.configFile === void 0 ? {} : { overrideConfigFile: (0, node_path.resolve)(cwd, options.configFile) }
|
|
712
|
+
});
|
|
713
|
+
const shared = options.rules ?? {};
|
|
714
|
+
const violations = [];
|
|
715
|
+
for (const sample of options.samples.map(sampleOf)) violations.push(...await sampleViolations(instance, sample, shared));
|
|
716
|
+
if (options.lint === true) violations.push(...await lintViolations(cwd, instance, options.lintPatterns ?? DEFAULT_LINT_PATTERNS));
|
|
717
|
+
return violations;
|
|
718
|
+
};
|
|
719
|
+
//#endregion
|
|
546
720
|
//#region src/workspace/rules.ts
|
|
547
721
|
const REGEXP_SPECIAL = /[.*+?^${}()|[\]\\]/gu;
|
|
548
722
|
function escapeRegExp(text) {
|
|
@@ -2767,6 +2941,8 @@ function compiles(source) {
|
|
|
2767
2941
|
*/
|
|
2768
2942
|
const pattern = zod.z.string().min(1).refine(compiles, "not a valid regular expression");
|
|
2769
2943
|
const patterns = zod.z.array(pattern);
|
|
2944
|
+
const severity = zod.z.enum(["warn", "error"]);
|
|
2945
|
+
const requiredRules = zod.z.record(zod.z.string().min(1), severity);
|
|
2770
2946
|
const selection = {
|
|
2771
2947
|
workflows: zod.z.exactOptional(paths),
|
|
2772
2948
|
exclude: zod.z.exactOptional(paths)
|
|
@@ -2826,6 +3002,16 @@ const checks = zod.z.strictObject({
|
|
|
2826
3002
|
exclude: zod.z.exactOptional(paths),
|
|
2827
3003
|
packageJson: zod.z.exactOptional(path)
|
|
2828
3004
|
}))),
|
|
3005
|
+
eslint: zod.z.exactOptional(enabled(zod.z.strictObject({
|
|
3006
|
+
samples: zod.z.array(zod.z.union([path, zod.z.strictObject({
|
|
3007
|
+
path,
|
|
3008
|
+
rules: zod.z.exactOptional(requiredRules)
|
|
3009
|
+
})])).min(1),
|
|
3010
|
+
rules: zod.z.exactOptional(requiredRules),
|
|
3011
|
+
configFile: zod.z.exactOptional(path),
|
|
3012
|
+
lint: zod.z.exactOptional(zod.z.boolean()),
|
|
3013
|
+
lintPatterns: zod.z.exactOptional(paths)
|
|
3014
|
+
}))),
|
|
2829
3015
|
"migrations-directory": zod.z.exactOptional(enabled(zod.z.strictObject({
|
|
2830
3016
|
generator: zod.z.enum(generatorNames),
|
|
2831
3017
|
deployTool: zod.z.enum(deployToolNames),
|
|
@@ -3100,6 +3286,12 @@ const registry = {
|
|
|
3100
3286
|
select: (checks) => enabledOptions(checks["migrations-directory"]),
|
|
3101
3287
|
check: migrationsDirectory
|
|
3102
3288
|
}),
|
|
3289
|
+
eslint: plain({
|
|
3290
|
+
name: "eslint",
|
|
3291
|
+
description: "The repository's own ESLint lints the sample files with the required rules enabled at the required severity",
|
|
3292
|
+
select: (checks) => enabledOptions(checks.eslint),
|
|
3293
|
+
check: eslint
|
|
3294
|
+
}),
|
|
3103
3295
|
"workflow-job-ordering": plain({
|
|
3104
3296
|
name: "workflow-job-ordering",
|
|
3105
3297
|
description: "Release, deploy and junction jobs are ordered so a failure stops what follows",
|
|
@@ -3402,6 +3594,7 @@ exports.deployTools = deployTools;
|
|
|
3402
3594
|
exports.dockerfilePackageManager = dockerfilePackageManager;
|
|
3403
3595
|
exports.effectiveEnv = effectiveEnv;
|
|
3404
3596
|
exports.effectivePermissions = effectivePermissions;
|
|
3597
|
+
exports.eslint = eslint;
|
|
3405
3598
|
exports.generators = generators;
|
|
3406
3599
|
exports.handlerCases = handlerCases;
|
|
3407
3600
|
exports.importCrossSlice = importCrossSlice;
|
package/dist/index.d.cts
CHANGED
|
@@ -474,6 +474,48 @@ interface DockerfilePackageManagerOptions {
|
|
|
474
474
|
*/
|
|
475
475
|
readonly packageJson?: string;
|
|
476
476
|
}
|
|
477
|
+
/**
|
|
478
|
+
* The severity a rule must have at least: `warn` is met by `warn` or `error`, `error` only by `error`.
|
|
479
|
+
*/
|
|
480
|
+
type EslintRequiredSeverity = "warn" | "error";
|
|
481
|
+
/**
|
|
482
|
+
* A file whose resolved ESLint configuration is checked.
|
|
483
|
+
*/
|
|
484
|
+
interface EslintSample {
|
|
485
|
+
/**
|
|
486
|
+
* The file ESLint would lint, relative to the directory the checks run in. It need not exist: ESLint resolves a file's configuration from its path alone.
|
|
487
|
+
*/
|
|
488
|
+
readonly path: string;
|
|
489
|
+
/**
|
|
490
|
+
* Rules required for this file on top of the check's `rules`, which this file's entries override.
|
|
491
|
+
*/
|
|
492
|
+
readonly rules?: Readonly<Record<string, EslintRequiredSeverity>>;
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Options of the `eslint` check: the repository's own ESLint, resolved from the directory the checks run in, lints the sample files with the required rules.
|
|
496
|
+
*/
|
|
497
|
+
interface EslintOptions {
|
|
498
|
+
/**
|
|
499
|
+
* One file per kind of source the repository lints (`src/index.ts`, `package.json`, `eslint.config.ts`). A bare string is `{ path }`. Each must be linted: not ignored, and matched by a configuration block.
|
|
500
|
+
*/
|
|
501
|
+
readonly samples: readonly (string | EslintSample)[];
|
|
502
|
+
/**
|
|
503
|
+
* Rules required for every sample, with the severity each must have at least.
|
|
504
|
+
*/
|
|
505
|
+
readonly rules?: Readonly<Record<string, EslintRequiredSeverity>>;
|
|
506
|
+
/**
|
|
507
|
+
* The config file under test, relative to the directory the checks run in, instead of the one ESLint finds from there.
|
|
508
|
+
*/
|
|
509
|
+
readonly configFile?: string;
|
|
510
|
+
/**
|
|
511
|
+
* Also lint the workspace and report every message ESLint produces. Off when omitted: the default level lints nothing and only reads resolved configuration.
|
|
512
|
+
*/
|
|
513
|
+
readonly lint?: boolean;
|
|
514
|
+
/**
|
|
515
|
+
* What the full level lints, as file and directory patterns relative to the directory the checks run in. `.` when omitted. Used only with `lint`.
|
|
516
|
+
*/
|
|
517
|
+
readonly lintPatterns?: readonly string[];
|
|
518
|
+
}
|
|
477
519
|
/**
|
|
478
520
|
* Options of the `migrations-directory` check: the directory a schema generator writes migrations to is the one the deploy tool applies them from, and no script applies them with the generator.
|
|
479
521
|
*/
|
|
@@ -672,6 +714,7 @@ interface CheckOptionsByName {
|
|
|
672
714
|
readonly "commit-types": CommitTypesOptions;
|
|
673
715
|
readonly "dockerfile-package-manager": DockerfilePackageManagerOptions;
|
|
674
716
|
readonly "migrations-directory": MigrationsDirectoryOptions;
|
|
717
|
+
readonly eslint: EslintOptions;
|
|
675
718
|
readonly "workflow-job-ordering": WorkflowJobOrderingOptions;
|
|
676
719
|
readonly "workflow-skippable-jobs": WorkflowSkippableJobsOptions;
|
|
677
720
|
readonly "workflow-runner-resolution": WorkflowRunnerResolutionOptions;
|
|
@@ -730,6 +773,16 @@ export declare const commitTypes: CheckFunction<CommitTypesOptions>;
|
|
|
730
773
|
*/
|
|
731
774
|
export declare const dockerfilePackageManager: CheckFunction<DockerfilePackageManagerOptions>;
|
|
732
775
|
//#endregion
|
|
776
|
+
//#region src/checks/eslint.d.ts
|
|
777
|
+
/**
|
|
778
|
+
* Proves the repository's own ESLint is applied, which no check that runs inside ESLint can notice: a config that dropped a shared preset, switched its rules off or narrowed `ignores` until nothing is linted still passes its own lint.
|
|
779
|
+
*
|
|
780
|
+
* It loads the ESLint that resolves from the directory the checks run in and resolves the repository's own config through the Node API, so the answer is the one the repository's lint script gets. By default it lints nothing: for each sample it reads the resolved configuration (the object `--print-config` prints) and reports a sample that is not linted, and each required rule that is missing, off or weaker than required. With `lint` it also lints the workspace and reports every message.
|
|
781
|
+
*
|
|
782
|
+
* 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.
|
|
783
|
+
*/
|
|
784
|
+
export declare const eslint: CheckFunction<EslintOptions>;
|
|
785
|
+
//#endregion
|
|
733
786
|
//#region src/workspace/packages.d.ts
|
|
734
787
|
/**
|
|
735
788
|
* A package directory found under the workspace root, before the layout classifies it.
|
|
@@ -1222,4 +1275,4 @@ export declare function proveExhaustive(input: {
|
|
|
1222
1275
|
readonly compilerOptions?: CompilerOptions;
|
|
1223
1276
|
}): ExhaustivenessProof;
|
|
1224
1277
|
//#endregion
|
|
1225
|
-
export type { AggregateMappersOptions, BranchRules, CaseRemoval, CheckContext, CheckFunction, CheckName, CheckOptionsByName, CheckResult, ChecksConfig, CommandTypesOptions, CommitTypesOptions, ConformanceConfig, DeployDirectory, DeploySelector, DeployToolAdapter, DeployToolName, DiscoveredPackage, DockerfilePackageManagerOptions, ExhaustivenessProof, GeneratorAdapter, GeneratorName, GitHubCheckContext, GitHubCheckFunction, GitHubClient, GitHubClientOptions, ImportGraphOptions, InstructionSymlinksOptions, Job, LayoutCheckContext, LayoutCheckFunction, MigrationsDirectoryOptions, PermissionAccess, Permissions, PinningLevel, RegisteredCheck, RepositorySettings, RepositorySlug, RunChecksOptions, RunResult, RunsOn, SettingsMergeMethodsOptions, SettingsOptions, SettingsRequiredChecksOptions, SettingsReviewThreadResolutionOptions, SingleStorybookOptions, SourceLocation, Step, TypeErrorReport, UsesReference, Violation, Workflow, WorkflowActionPinningOptions, WorkflowCredentialsOptions, WorkflowJobOrderingOptions, WorkflowMergeGroupOptions, WorkflowRepositoryDispatchOptions, WorkflowRunnerResolutionOptions, WorkflowSelection, WorkflowSkippableJobsOptions, WorkflowUpdateBotCooldownOptions, WorkflowVersionSingleSourceOptions, WorkspacePackage };
|
|
1278
|
+
export type { AggregateMappersOptions, BranchRules, CaseRemoval, CheckContext, CheckFunction, CheckName, CheckOptionsByName, CheckResult, ChecksConfig, CommandTypesOptions, CommitTypesOptions, ConformanceConfig, DeployDirectory, DeploySelector, DeployToolAdapter, DeployToolName, DiscoveredPackage, DockerfilePackageManagerOptions, EslintOptions, EslintRequiredSeverity, EslintSample, ExhaustivenessProof, GeneratorAdapter, GeneratorName, GitHubCheckContext, GitHubCheckFunction, GitHubClient, GitHubClientOptions, ImportGraphOptions, InstructionSymlinksOptions, Job, LayoutCheckContext, LayoutCheckFunction, MigrationsDirectoryOptions, PermissionAccess, Permissions, PinningLevel, RegisteredCheck, RepositorySettings, RepositorySlug, RunChecksOptions, RunResult, RunsOn, SettingsMergeMethodsOptions, SettingsOptions, SettingsRequiredChecksOptions, SettingsReviewThreadResolutionOptions, SingleStorybookOptions, SourceLocation, Step, TypeErrorReport, UsesReference, Violation, Workflow, WorkflowActionPinningOptions, WorkflowCredentialsOptions, WorkflowJobOrderingOptions, WorkflowMergeGroupOptions, WorkflowRepositoryDispatchOptions, WorkflowRunnerResolutionOptions, WorkflowSelection, WorkflowSkippableJobsOptions, WorkflowUpdateBotCooldownOptions, WorkflowVersionSingleSourceOptions, WorkspacePackage };
|
package/dist/index.d.ts
CHANGED
|
@@ -474,6 +474,48 @@ interface DockerfilePackageManagerOptions {
|
|
|
474
474
|
*/
|
|
475
475
|
readonly packageJson?: string;
|
|
476
476
|
}
|
|
477
|
+
/**
|
|
478
|
+
* The severity a rule must have at least: `warn` is met by `warn` or `error`, `error` only by `error`.
|
|
479
|
+
*/
|
|
480
|
+
type EslintRequiredSeverity = "warn" | "error";
|
|
481
|
+
/**
|
|
482
|
+
* A file whose resolved ESLint configuration is checked.
|
|
483
|
+
*/
|
|
484
|
+
interface EslintSample {
|
|
485
|
+
/**
|
|
486
|
+
* The file ESLint would lint, relative to the directory the checks run in. It need not exist: ESLint resolves a file's configuration from its path alone.
|
|
487
|
+
*/
|
|
488
|
+
readonly path: string;
|
|
489
|
+
/**
|
|
490
|
+
* Rules required for this file on top of the check's `rules`, which this file's entries override.
|
|
491
|
+
*/
|
|
492
|
+
readonly rules?: Readonly<Record<string, EslintRequiredSeverity>>;
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Options of the `eslint` check: the repository's own ESLint, resolved from the directory the checks run in, lints the sample files with the required rules.
|
|
496
|
+
*/
|
|
497
|
+
interface EslintOptions {
|
|
498
|
+
/**
|
|
499
|
+
* One file per kind of source the repository lints (`src/index.ts`, `package.json`, `eslint.config.ts`). A bare string is `{ path }`. Each must be linted: not ignored, and matched by a configuration block.
|
|
500
|
+
*/
|
|
501
|
+
readonly samples: readonly (string | EslintSample)[];
|
|
502
|
+
/**
|
|
503
|
+
* Rules required for every sample, with the severity each must have at least.
|
|
504
|
+
*/
|
|
505
|
+
readonly rules?: Readonly<Record<string, EslintRequiredSeverity>>;
|
|
506
|
+
/**
|
|
507
|
+
* The config file under test, relative to the directory the checks run in, instead of the one ESLint finds from there.
|
|
508
|
+
*/
|
|
509
|
+
readonly configFile?: string;
|
|
510
|
+
/**
|
|
511
|
+
* Also lint the workspace and report every message ESLint produces. Off when omitted: the default level lints nothing and only reads resolved configuration.
|
|
512
|
+
*/
|
|
513
|
+
readonly lint?: boolean;
|
|
514
|
+
/**
|
|
515
|
+
* What the full level lints, as file and directory patterns relative to the directory the checks run in. `.` when omitted. Used only with `lint`.
|
|
516
|
+
*/
|
|
517
|
+
readonly lintPatterns?: readonly string[];
|
|
518
|
+
}
|
|
477
519
|
/**
|
|
478
520
|
* Options of the `migrations-directory` check: the directory a schema generator writes migrations to is the one the deploy tool applies them from, and no script applies them with the generator.
|
|
479
521
|
*/
|
|
@@ -672,6 +714,7 @@ interface CheckOptionsByName {
|
|
|
672
714
|
readonly "commit-types": CommitTypesOptions;
|
|
673
715
|
readonly "dockerfile-package-manager": DockerfilePackageManagerOptions;
|
|
674
716
|
readonly "migrations-directory": MigrationsDirectoryOptions;
|
|
717
|
+
readonly eslint: EslintOptions;
|
|
675
718
|
readonly "workflow-job-ordering": WorkflowJobOrderingOptions;
|
|
676
719
|
readonly "workflow-skippable-jobs": WorkflowSkippableJobsOptions;
|
|
677
720
|
readonly "workflow-runner-resolution": WorkflowRunnerResolutionOptions;
|
|
@@ -730,6 +773,16 @@ export declare const commitTypes: CheckFunction<CommitTypesOptions>;
|
|
|
730
773
|
*/
|
|
731
774
|
export declare const dockerfilePackageManager: CheckFunction<DockerfilePackageManagerOptions>;
|
|
732
775
|
//#endregion
|
|
776
|
+
//#region src/checks/eslint.d.ts
|
|
777
|
+
/**
|
|
778
|
+
* Proves the repository's own ESLint is applied, which no check that runs inside ESLint can notice: a config that dropped a shared preset, switched its rules off or narrowed `ignores` until nothing is linted still passes its own lint.
|
|
779
|
+
*
|
|
780
|
+
* It loads the ESLint that resolves from the directory the checks run in and resolves the repository's own config through the Node API, so the answer is the one the repository's lint script gets. By default it lints nothing: for each sample it reads the resolved configuration (the object `--print-config` prints) and reports a sample that is not linted, and each required rule that is missing, off or weaker than required. With `lint` it also lints the workspace and reports every message.
|
|
781
|
+
*
|
|
782
|
+
* 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.
|
|
783
|
+
*/
|
|
784
|
+
export declare const eslint: CheckFunction<EslintOptions>;
|
|
785
|
+
//#endregion
|
|
733
786
|
//#region src/workspace/packages.d.ts
|
|
734
787
|
/**
|
|
735
788
|
* A package directory found under the workspace root, before the layout classifies it.
|
|
@@ -1222,4 +1275,4 @@ export declare function proveExhaustive(input: {
|
|
|
1222
1275
|
readonly compilerOptions?: CompilerOptions;
|
|
1223
1276
|
}): ExhaustivenessProof;
|
|
1224
1277
|
//#endregion
|
|
1225
|
-
export type { AggregateMappersOptions, BranchRules, CaseRemoval, CheckContext, CheckFunction, CheckName, CheckOptionsByName, CheckResult, ChecksConfig, CommandTypesOptions, CommitTypesOptions, ConformanceConfig, DeployDirectory, DeploySelector, DeployToolAdapter, DeployToolName, DiscoveredPackage, DockerfilePackageManagerOptions, ExhaustivenessProof, GeneratorAdapter, GeneratorName, GitHubCheckContext, GitHubCheckFunction, GitHubClient, GitHubClientOptions, ImportGraphOptions, InstructionSymlinksOptions, Job, LayoutCheckContext, LayoutCheckFunction, MigrationsDirectoryOptions, PermissionAccess, Permissions, PinningLevel, RegisteredCheck, RepositorySettings, RepositorySlug, RunChecksOptions, RunResult, RunsOn, SettingsMergeMethodsOptions, SettingsOptions, SettingsRequiredChecksOptions, SettingsReviewThreadResolutionOptions, SingleStorybookOptions, SourceLocation, Step, TypeErrorReport, UsesReference, Violation, Workflow, WorkflowActionPinningOptions, WorkflowCredentialsOptions, WorkflowJobOrderingOptions, WorkflowMergeGroupOptions, WorkflowRepositoryDispatchOptions, WorkflowRunnerResolutionOptions, WorkflowSelection, WorkflowSkippableJobsOptions, WorkflowUpdateBotCooldownOptions, WorkflowVersionSingleSourceOptions, WorkspacePackage };
|
|
1278
|
+
export type { AggregateMappersOptions, BranchRules, CaseRemoval, CheckContext, CheckFunction, CheckName, CheckOptionsByName, CheckResult, ChecksConfig, CommandTypesOptions, CommitTypesOptions, ConformanceConfig, DeployDirectory, DeploySelector, DeployToolAdapter, DeployToolName, DiscoveredPackage, DockerfilePackageManagerOptions, EslintOptions, EslintRequiredSeverity, EslintSample, ExhaustivenessProof, GeneratorAdapter, GeneratorName, GitHubCheckContext, GitHubCheckFunction, GitHubClient, GitHubClientOptions, ImportGraphOptions, InstructionSymlinksOptions, Job, LayoutCheckContext, LayoutCheckFunction, MigrationsDirectoryOptions, PermissionAccess, Permissions, PinningLevel, RegisteredCheck, RepositorySettings, RepositorySlug, RunChecksOptions, RunResult, RunsOn, SettingsMergeMethodsOptions, SettingsOptions, SettingsRequiredChecksOptions, SettingsReviewThreadResolutionOptions, SingleStorybookOptions, SourceLocation, Step, TypeErrorReport, UsesReference, Violation, Workflow, WorkflowActionPinningOptions, WorkflowCredentialsOptions, WorkflowJobOrderingOptions, WorkflowMergeGroupOptions, WorkflowRepositoryDispatchOptions, WorkflowRunnerResolutionOptions, WorkflowSelection, WorkflowSkippableJobsOptions, WorkflowUpdateBotCooldownOptions, WorkflowVersionSingleSourceOptions, WorkspacePackage };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
1
2
|
import { existsSync } from "node:fs";
|
|
2
3
|
import { dirname, extname, join, posix, relative, resolve, sep } from "node:path";
|
|
3
4
|
import { escapePath, glob, isDynamicPattern } from "tinyglobby";
|
|
@@ -542,6 +543,179 @@ const dockerfilePackageManager = async ({ cwd, options }) => {
|
|
|
542
543
|
return violations;
|
|
543
544
|
};
|
|
544
545
|
//#endregion
|
|
546
|
+
//#region src/checks/eslint.ts
|
|
547
|
+
/**
|
|
548
|
+
* The patterns the full level lints when the options name none.
|
|
549
|
+
*/
|
|
550
|
+
const DEFAULT_LINT_PATTERNS = ["."];
|
|
551
|
+
const SEVERITY_RANK = {
|
|
552
|
+
off: 0,
|
|
553
|
+
warn: 1,
|
|
554
|
+
error: 2
|
|
555
|
+
};
|
|
556
|
+
function hasEslintClass(value) {
|
|
557
|
+
return isRecord(value) && typeof value["ESLint"] === "function";
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* The `ESLint` class of the ESLint installed where the checks run, found the way a module in `cwd` finds it. It is not this package's own copy, so the results agree with the repository's own lint script.
|
|
561
|
+
*/
|
|
562
|
+
function loadEslint(cwd) {
|
|
563
|
+
const requireFromCwd = createRequire(join(cwd, "package.json"));
|
|
564
|
+
try {
|
|
565
|
+
requireFromCwd.resolve("eslint");
|
|
566
|
+
} catch (error) {
|
|
567
|
+
throw new ConformanceError(`the eslint check uses the repository's own ESLint, and 'eslint' does not resolve from ${cwd}; install it there`, { cause: error });
|
|
568
|
+
}
|
|
569
|
+
const loaded = requireFromCwd("eslint");
|
|
570
|
+
if (!hasEslintClass(loaded)) throw new ConformanceError(`the 'eslint' that resolves from ${cwd} does not export the ESLint class of the Node API`);
|
|
571
|
+
return loaded.ESLint;
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* The severity a rule entry resolves to. An entry is a severity or an array whose first element is one, and a severity is `0`, `1`, `2`, `'off'`, `'warn'` or `'error'`; `calculateConfigForFile` returns whichever form the config was written in.
|
|
575
|
+
*/
|
|
576
|
+
function normaliseSeverity(entry) {
|
|
577
|
+
const severity = Array.isArray(entry) ? entry[0] : entry;
|
|
578
|
+
if (severity === 0 || severity === "off") return "off";
|
|
579
|
+
if (severity === 1 || severity === "warn") return "warn";
|
|
580
|
+
if (severity === 2 || severity === "error") return "error";
|
|
581
|
+
throw new ConformanceError(`ESLint resolved ${JSON.stringify(entry)} as a rule entry, which is not a severity or an array that starts with one`);
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* The severity of each rule in a resolved configuration.
|
|
585
|
+
*/
|
|
586
|
+
function resolvedSeverities(config) {
|
|
587
|
+
if (!isRecord(config)) throw new ConformanceError("ESLint resolved a configuration that is not an object");
|
|
588
|
+
const { rules } = config;
|
|
589
|
+
if (rules !== void 0 && !isRecord(rules)) throw new ConformanceError("ESLint resolved a configuration whose 'rules' is not an object");
|
|
590
|
+
return new Map(Object.entries(rules ?? {}).map(([rule, entry]) => [rule, normaliseSeverity(entry)]));
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Whether `error` is the one `calculateConfigForFile` throws when ESLint finds no config file for the path.
|
|
594
|
+
*/
|
|
595
|
+
function isMissingConfig(error) {
|
|
596
|
+
return isRecord(error) && error["messageTemplate"] === "config-file-missing";
|
|
597
|
+
}
|
|
598
|
+
function sampleOf(sample) {
|
|
599
|
+
return typeof sample === "string" ? { path: sample } : sample;
|
|
600
|
+
}
|
|
601
|
+
function ruleViolations(file, required, actual) {
|
|
602
|
+
return Object.entries(required).flatMap(([rule, severity]) => {
|
|
603
|
+
const found = actual.get(rule);
|
|
604
|
+
if (found === void 0) return [{
|
|
605
|
+
code: "eslint/rule-missing",
|
|
606
|
+
message: `the rule '${rule}' is not configured for ${file} (required: ${severity})`,
|
|
607
|
+
file
|
|
608
|
+
}];
|
|
609
|
+
if (found === "off") return [{
|
|
610
|
+
code: "eslint/rule-off",
|
|
611
|
+
message: `the rule '${rule}' is off for ${file} (required: ${severity})`,
|
|
612
|
+
file
|
|
613
|
+
}];
|
|
614
|
+
if (SEVERITY_RANK[found] < SEVERITY_RANK[severity]) return [{
|
|
615
|
+
code: "eslint/rule-too-weak",
|
|
616
|
+
message: `the rule '${rule}' is '${found}' for ${file} (required: ${severity})`,
|
|
617
|
+
file
|
|
618
|
+
}];
|
|
619
|
+
return [];
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
async function sampleViolations(eslint, sample, shared) {
|
|
623
|
+
const file = sample.path;
|
|
624
|
+
let config;
|
|
625
|
+
try {
|
|
626
|
+
config = await eslint.calculateConfigForFile(file);
|
|
627
|
+
} catch (error) {
|
|
628
|
+
if (isMissingConfig(error)) return [{
|
|
629
|
+
code: "eslint/no-config",
|
|
630
|
+
message: `ESLint finds no config file for ${file}; the check does not fall back to a config of its own`,
|
|
631
|
+
file
|
|
632
|
+
}];
|
|
633
|
+
throw new ConformanceError(`ESLint could not resolve its configuration for ${file}`, { cause: error });
|
|
634
|
+
}
|
|
635
|
+
if (config === void 0) return [{
|
|
636
|
+
code: "eslint/not-linted",
|
|
637
|
+
message: `${file} is not linted: it is ignored, or no configuration block matches it`,
|
|
638
|
+
file
|
|
639
|
+
}];
|
|
640
|
+
return ruleViolations(file, {
|
|
641
|
+
...shared,
|
|
642
|
+
...sample.rules
|
|
643
|
+
}, resolvedSeverities(config));
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Whether `error` is ESLint refusing a pattern because it selects no file that is linted.
|
|
647
|
+
*/
|
|
648
|
+
function isNothingLinted(error) {
|
|
649
|
+
return error instanceof Error && isRecord(error) && (error["messageTemplate"] === "file-not-found" || error["messageTemplate"] === "all-matched-files-ignored");
|
|
650
|
+
}
|
|
651
|
+
function messageViolation(cwd, result, message) {
|
|
652
|
+
const file = relativePosix(cwd, result.filePath);
|
|
653
|
+
const location = {
|
|
654
|
+
line: message.line,
|
|
655
|
+
column: message.column
|
|
656
|
+
};
|
|
657
|
+
if (message.ruleId !== null) {
|
|
658
|
+
const severity = message.severity === 1 ? "warn" : "error";
|
|
659
|
+
return {
|
|
660
|
+
code: `eslint/lint/${message.ruleId}`,
|
|
661
|
+
message: `${message.ruleId} (${severity}): ${message.message}`,
|
|
662
|
+
file,
|
|
663
|
+
location
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
return message.fatal === true ? {
|
|
667
|
+
code: "eslint/fatal",
|
|
668
|
+
message: message.message,
|
|
669
|
+
file,
|
|
670
|
+
location
|
|
671
|
+
} : {
|
|
672
|
+
code: "eslint/lint-message",
|
|
673
|
+
message: message.message,
|
|
674
|
+
file,
|
|
675
|
+
location
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
function byPosition(left, right) {
|
|
679
|
+
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);
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* 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.
|
|
683
|
+
*/
|
|
684
|
+
async function lintViolations(cwd, eslint, patterns) {
|
|
685
|
+
const violations = [];
|
|
686
|
+
const results = /* @__PURE__ */ new Map();
|
|
687
|
+
for (const pattern of patterns) try {
|
|
688
|
+
for (const result of await eslint.lintFiles([pattern])) results.set(result.filePath, result);
|
|
689
|
+
} catch (error) {
|
|
690
|
+
if (!isNothingLinted(error)) throw new ConformanceError(`ESLint could not lint '${pattern}'`, { cause: error });
|
|
691
|
+
violations.push({
|
|
692
|
+
code: "eslint/nothing-linted",
|
|
693
|
+
message: error.message,
|
|
694
|
+
file: pattern
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
for (const result of results.values()) for (const message of result.messages) violations.push(messageViolation(cwd, result, message));
|
|
698
|
+
return violations.sort(byPosition);
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Proves the repository's own ESLint is applied, which no check that runs inside ESLint can notice: a config that dropped a shared preset, switched its rules off or narrowed `ignores` until nothing is linted still passes its own lint.
|
|
702
|
+
*
|
|
703
|
+
* It loads the ESLint that resolves from the directory the checks run in and resolves the repository's own config through the Node API, so the answer is the one the repository's lint script gets. By default it lints nothing: for each sample it reads the resolved configuration (the object `--print-config` prints) and reports a sample that is not linted, and each required rule that is missing, off or weaker than required. With `lint` it also lints the workspace and reports every message.
|
|
704
|
+
*
|
|
705
|
+
* 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.
|
|
706
|
+
*/
|
|
707
|
+
const eslint = async ({ cwd, options }) => {
|
|
708
|
+
const instance = new (loadEslint(cwd))({
|
|
709
|
+
cwd,
|
|
710
|
+
...options.configFile === void 0 ? {} : { overrideConfigFile: resolve(cwd, options.configFile) }
|
|
711
|
+
});
|
|
712
|
+
const shared = options.rules ?? {};
|
|
713
|
+
const violations = [];
|
|
714
|
+
for (const sample of options.samples.map(sampleOf)) violations.push(...await sampleViolations(instance, sample, shared));
|
|
715
|
+
if (options.lint === true) violations.push(...await lintViolations(cwd, instance, options.lintPatterns ?? DEFAULT_LINT_PATTERNS));
|
|
716
|
+
return violations;
|
|
717
|
+
};
|
|
718
|
+
//#endregion
|
|
545
719
|
//#region src/workspace/rules.ts
|
|
546
720
|
const REGEXP_SPECIAL = /[.*+?^${}()|[\]\\]/gu;
|
|
547
721
|
function escapeRegExp(text) {
|
|
@@ -2766,6 +2940,8 @@ function compiles(source) {
|
|
|
2766
2940
|
*/
|
|
2767
2941
|
const pattern = z.string().min(1).refine(compiles, "not a valid regular expression");
|
|
2768
2942
|
const patterns = z.array(pattern);
|
|
2943
|
+
const severity = z.enum(["warn", "error"]);
|
|
2944
|
+
const requiredRules = z.record(z.string().min(1), severity);
|
|
2769
2945
|
const selection = {
|
|
2770
2946
|
workflows: z.exactOptional(paths),
|
|
2771
2947
|
exclude: z.exactOptional(paths)
|
|
@@ -2825,6 +3001,16 @@ const checks = z.strictObject({
|
|
|
2825
3001
|
exclude: z.exactOptional(paths),
|
|
2826
3002
|
packageJson: z.exactOptional(path)
|
|
2827
3003
|
}))),
|
|
3004
|
+
eslint: z.exactOptional(enabled(z.strictObject({
|
|
3005
|
+
samples: z.array(z.union([path, z.strictObject({
|
|
3006
|
+
path,
|
|
3007
|
+
rules: z.exactOptional(requiredRules)
|
|
3008
|
+
})])).min(1),
|
|
3009
|
+
rules: z.exactOptional(requiredRules),
|
|
3010
|
+
configFile: z.exactOptional(path),
|
|
3011
|
+
lint: z.exactOptional(z.boolean()),
|
|
3012
|
+
lintPatterns: z.exactOptional(paths)
|
|
3013
|
+
}))),
|
|
2828
3014
|
"migrations-directory": z.exactOptional(enabled(z.strictObject({
|
|
2829
3015
|
generator: z.enum(generatorNames),
|
|
2830
3016
|
deployTool: z.enum(deployToolNames),
|
|
@@ -3099,6 +3285,12 @@ const registry = {
|
|
|
3099
3285
|
select: (checks) => enabledOptions(checks["migrations-directory"]),
|
|
3100
3286
|
check: migrationsDirectory
|
|
3101
3287
|
}),
|
|
3288
|
+
eslint: plain({
|
|
3289
|
+
name: "eslint",
|
|
3290
|
+
description: "The repository's own ESLint lints the sample files with the required rules enabled at the required severity",
|
|
3291
|
+
select: (checks) => enabledOptions(checks.eslint),
|
|
3292
|
+
check: eslint
|
|
3293
|
+
}),
|
|
3102
3294
|
"workflow-job-ordering": plain({
|
|
3103
3295
|
name: "workflow-job-ordering",
|
|
3104
3296
|
description: "Release, deploy and junction jobs are ordered so a failure stops what follows",
|
|
@@ -3380,4 +3572,4 @@ function proveExhaustive(input) {
|
|
|
3380
3572
|
}) };
|
|
3381
3573
|
}
|
|
3382
3574
|
//#endregion
|
|
3383
|
-
export { ConformanceError, DEFAULT_INFERENCES, DEFAULT_WORKFLOWS, EXIT_CODES, ExhaustivenessError, PRESET_COMMIT_TYPES, accessOf, aggregateMappers, checkNames, classifyPackages, commandTypes, commitTypes, conformanceSchema, conformanceSection, createGitHubClient, crossSliceRules, cycleRule, deployTools, dockerfilePackageManager, effectiveEnv, effectivePermissions, 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, workflowRepositoryDispatch, workflowRunnerResolution, workflowSkippableJobs, workflowUpdateBotCooldown, workflowVersionSingleSource };
|
|
3575
|
+
export { ConformanceError, DEFAULT_INFERENCES, DEFAULT_WORKFLOWS, EXIT_CODES, ExhaustivenessError, PRESET_COMMIT_TYPES, accessOf, aggregateMappers, checkNames, classifyPackages, commandTypes, commitTypes, conformanceSchema, conformanceSection, createGitHubClient, crossSliceRules, cycleRule, deployTools, dockerfilePackageManager, effectiveEnv, effectivePermissions, 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, workflowRepositoryDispatch, workflowRunnerResolution, workflowSkippableJobs, workflowUpdateBotCooldown, workflowVersionSingleSource };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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": "1.
|
|
4
|
+
"version": "1.3.0",
|
|
5
5
|
"bin": {
|
|
6
6
|
"workspace-conformance": "./dist/cli.js"
|
|
7
7
|
},
|
|
@@ -72,8 +72,14 @@
|
|
|
72
72
|
"packageManager": "pnpm@12.4.1+sha512.2e81e399d73fe8390dab25e06aa788ab7a5908248d2f5a370f82b481147a6a7a367bf8048f9a6fdb6460f21a66f0542dedb8b94ca2c8723596741920b1656d4c",
|
|
73
73
|
"peerDependencies": {
|
|
74
74
|
"cosmiconfig": "^9.0.0 || ^10.0.0",
|
|
75
|
+
"eslint": "^9.0.0 || ^10.0.0",
|
|
75
76
|
"typescript": "^5.9.0 || ^6.0.0"
|
|
76
77
|
},
|
|
78
|
+
"peerDependenciesMeta": {
|
|
79
|
+
"eslint": {
|
|
80
|
+
"optional": true
|
|
81
|
+
}
|
|
82
|
+
},
|
|
77
83
|
"publishConfig": {
|
|
78
84
|
"access": "public",
|
|
79
85
|
"provenance": true
|