@shivaedev/quality 0.21.1 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +24 -8
- package/dist/biome/exceptionConfig.d.ts +7 -0
- package/dist/biome/exceptionConfig.d.ts.map +1 -0
- package/dist/biome/exceptionConfig.js +54 -0
- package/dist/biome/exceptionConfig.js.map +1 -0
- package/dist/biome/report.d.ts +2 -1
- package/dist/biome/report.d.ts.map +1 -1
- package/dist/biome/report.js +5 -5
- package/dist/biome/report.js.map +1 -1
- package/dist/biome/run.d.ts +2 -1
- package/dist/biome/run.d.ts.map +1 -1
- package/dist/biome/run.js +5 -4
- package/dist/biome/run.js.map +1 -1
- package/dist/cli/args.d.ts +5 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +6 -27
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/fix.d.ts.map +1 -1
- package/dist/cli/fix.js +9 -9
- package/dist/cli/fix.js.map +1 -1
- package/dist/cli/lint.d.ts.map +1 -1
- package/dist/cli/lint.js +2 -1
- package/dist/cli/lint.js.map +1 -1
- package/dist/cli/messages.d.ts +3 -0
- package/dist/cli/messages.d.ts.map +1 -0
- package/dist/cli/messages.js +30 -0
- package/dist/cli/messages.js.map +1 -0
- package/dist/cli/migrateExceptions.d.ts +4 -0
- package/dist/cli/migrateExceptions.d.ts.map +1 -0
- package/dist/cli/migrateExceptions.js +78 -0
- package/dist/cli/migrateExceptions.js.map +1 -0
- package/dist/cli/program.d.ts +9 -0
- package/dist/cli/program.d.ts.map +1 -0
- package/dist/cli/program.js +44 -0
- package/dist/cli/program.js.map +1 -0
- package/dist/cli/run-main.d.ts +5 -2
- package/dist/cli/run-main.d.ts.map +1 -1
- package/dist/cli/run-main.js +14 -10
- package/dist/cli/run-main.js.map +1 -1
- package/dist/cli.js +2 -46
- package/dist/cli.js.map +1 -1
- package/dist/config/decode.d.ts +6 -0
- package/dist/config/decode.d.ts.map +1 -1
- package/dist/config/decode.js +2 -0
- package/dist/config/decode.js.map +1 -1
- package/dist/config/load.d.ts +4 -0
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +10 -1
- package/dist/config/load.js.map +1 -1
- package/dist/config/resolve.d.ts.map +1 -1
- package/dist/config/resolve.js +11 -1
- package/dist/config/resolve.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/engine/run-rules.d.ts +1 -0
- package/dist/engine/run-rules.d.ts.map +1 -1
- package/dist/engine/run-rules.js +19 -4
- package/dist/engine/run-rules.js.map +1 -1
- package/dist/engine/session.d.ts.map +1 -1
- package/dist/engine/session.js +11 -3
- package/dist/engine/session.js.map +1 -1
- package/dist/exceptions/biomeFindings.d.ts +4 -0
- package/dist/exceptions/biomeFindings.d.ts.map +1 -0
- package/dist/exceptions/biomeFindings.js +14 -0
- package/dist/exceptions/biomeFindings.js.map +1 -0
- package/dist/exceptions/declaration.d.ts +11 -0
- package/dist/exceptions/declaration.d.ts.map +1 -0
- package/dist/exceptions/declaration.js +24 -0
- package/dist/exceptions/declaration.js.map +1 -0
- package/dist/exceptions/migrateBiome.d.ts +3 -0
- package/dist/exceptions/migrateBiome.d.ts.map +1 -0
- package/dist/exceptions/migrateBiome.js +62 -0
- package/dist/exceptions/migrateBiome.js.map +1 -0
- package/dist/exceptions/migrateConfig.d.ts +3 -0
- package/dist/exceptions/migrateConfig.d.ts.map +1 -0
- package/dist/exceptions/migrateConfig.js +57 -0
- package/dist/exceptions/migrateConfig.js.map +1 -0
- package/dist/exceptions/registryEntries.d.ts +4 -0
- package/dist/exceptions/registryEntries.d.ts.map +1 -0
- package/dist/exceptions/registryEntries.js +6 -0
- package/dist/exceptions/registryEntries.js.map +1 -0
- package/dist/rule.d.ts +3 -0
- package/dist/rule.d.ts.map +1 -1
- package/dist/rule.js.map +1 -1
- package/dist/rules/biome.js +2 -2
- package/dist/rules/biome.js.map +1 -1
- package/dist/rules/built-in.d.ts +1 -1
- package/dist/rules/built-in.d.ts.map +1 -1
- package/dist/rules/built-in.js +4 -0
- package/dist/rules/built-in.js.map +1 -1
- package/dist/rules/maxDepth.d.ts +2 -0
- package/dist/rules/maxDepth.d.ts.map +1 -0
- package/dist/rules/maxDepth.js +20 -0
- package/dist/rules/maxDepth.js.map +1 -0
- package/dist/rules/noBarrel.d.ts +2 -0
- package/dist/rules/noBarrel.d.ts.map +1 -0
- package/dist/rules/noBarrel.js +14 -0
- package/dist/rules/noBarrel.js.map +1 -0
- package/dist/rules/suppressions/biome/ConsumerDeclaration.d.ts +7 -0
- package/dist/rules/suppressions/biome/ConsumerDeclaration.d.ts.map +1 -0
- package/dist/rules/suppressions/biome/ConsumerDeclaration.js +15 -0
- package/dist/rules/suppressions/biome/ConsumerDeclaration.js.map +1 -0
- package/dist/rules/suppressions/biome-overrides.d.ts.map +1 -1
- package/dist/rules/suppressions/biome-overrides.js +7 -16
- package/dist/rules/suppressions/biome-overrides.js.map +1 -1
- package/docs/debt-and-hooks.md +31 -11
- package/docs/modules.md +19 -1
- package/docs/north-star.md +4 -4
- package/docs/roadmap.md +2 -2
- package/docs/rules.md +21 -34
- package/docs/tooling.md +2 -2
- package/package.json +2 -2
- package/src/biome/exceptionConfig.ts +70 -0
- package/src/biome/report.ts +11 -5
- package/src/biome/run.ts +17 -10
- package/src/cli/args.ts +7 -29
- package/src/cli/fix.ts +14 -9
- package/src/cli/lint.ts +3 -1
- package/src/cli/messages.ts +31 -0
- package/src/cli/migrateExceptions.ts +87 -0
- package/src/cli/program.ts +50 -0
- package/src/cli/run-main.ts +17 -11
- package/src/cli.ts +2 -48
- package/src/config/decode.ts +2 -0
- package/src/config/load.ts +15 -1
- package/src/config/resolve.ts +12 -1
- package/src/config.ts +2 -0
- package/src/engine/run-rules.ts +21 -4
- package/src/engine/session.ts +11 -3
- package/src/exceptions/biomeFindings.ts +20 -0
- package/src/exceptions/declaration.ts +38 -0
- package/src/exceptions/migrateBiome.ts +78 -0
- package/src/exceptions/migrateConfig.ts +64 -0
- package/src/exceptions/registryEntries.ts +10 -0
- package/src/rule.ts +3 -0
- package/src/rules/biome.ts +2 -2
- package/src/rules/built-in.ts +4 -0
- package/src/rules/maxDepth.ts +24 -0
- package/src/rules/noBarrel.ts +16 -0
- package/src/rules/suppressions/biome/ConsumerDeclaration.ts +24 -0
- package/src/rules/suppressions/biome-overrides.ts +7 -26
package/src/cli/fix.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { relative } from "node:path";
|
|
|
2
2
|
import { Console, Effect, FileSystem } from "effect";
|
|
3
3
|
import { biomeReport, type Report } from "#biome/report.ts";
|
|
4
4
|
import { loadConfig } from "#config/load.ts";
|
|
5
|
+
import type { ExceptionDeclaration } from "#exceptions/declaration.ts";
|
|
5
6
|
import { SetupFailure } from "#failure.ts";
|
|
6
7
|
import { posix } from "#inventory/ignore-scope.ts";
|
|
7
8
|
import { walk } from "#inventory/walk.ts";
|
|
@@ -10,10 +11,10 @@ import { plural } from "#report/plural.ts";
|
|
|
10
11
|
|
|
11
12
|
export const MAX_FIX_ROUNDS = 5;
|
|
12
13
|
|
|
13
|
-
function biome(root: string, args: readonly string[]): Effect.Effect<Report, SetupFailure> {
|
|
14
|
+
function biome(root: string, args: readonly string[], exceptions: readonly ExceptionDeclaration[]): Effect.Effect<Report, SetupFailure> {
|
|
14
15
|
return Effect.tryPromise({
|
|
15
16
|
catch: (cause) => new SetupFailure({ message: cause instanceof Error ? cause.message : String(cause) }),
|
|
16
|
-
try: () => biomeReport(root, args),
|
|
17
|
+
try: () => biomeReport(root, args, exceptions),
|
|
17
18
|
});
|
|
18
19
|
}
|
|
19
20
|
|
|
@@ -43,17 +44,21 @@ export interface Settling<TFailure, TRequirements> {
|
|
|
43
44
|
readonly snapshot: Effect.Effect<ReadonlyMap<string, string>, TFailure, TRequirements>;
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
function changedBy(root: string, args: readonly string[]): Effect.Effect<number, SetupFailure> {
|
|
47
|
-
return Effect.map(biome(root, args), (report) => report.summary.changed);
|
|
47
|
+
function changedBy(root: string, args: readonly string[], exceptions: readonly ExceptionDeclaration[]): Effect.Effect<number, SetupFailure> {
|
|
48
|
+
return Effect.map(biome(root, args, exceptions), (report) => report.summary.changed);
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
function biomePasses(root: string): readonly Pass<SetupFailure, never>[] {
|
|
51
|
+
function biomePasses(root: string, exceptions: readonly ExceptionDeclaration[]): readonly Pass<SetupFailure, never>[] {
|
|
51
52
|
return [
|
|
52
53
|
{
|
|
53
54
|
label: "with fixes",
|
|
54
|
-
run: changedBy(
|
|
55
|
+
run: changedBy(
|
|
56
|
+
root,
|
|
57
|
+
["check", "--write", "--unsafe", "--skip=assist/source/useSortedKeys", "--skip=lint/suspicious/noDuplicateObjectKeys"],
|
|
58
|
+
exceptions,
|
|
59
|
+
),
|
|
55
60
|
},
|
|
56
|
-
{ label: "with the format pass", run: changedBy(root, ["format", "--write"]) },
|
|
61
|
+
{ label: "with the format pass", run: changedBy(root, ["format", "--write"], exceptions) },
|
|
57
62
|
];
|
|
58
63
|
}
|
|
59
64
|
|
|
@@ -122,9 +127,9 @@ export function settle<TFailure, TRequirements>({
|
|
|
122
127
|
|
|
123
128
|
export function fix(cwd: string, config: string | undefined): Effect.Effect<void, SetupFailure, FileSystem.FileSystem> {
|
|
124
129
|
return Effect.gen(function* () {
|
|
125
|
-
const { root } = yield* loadConfig(cwd, config);
|
|
130
|
+
const { root, exceptions } = yield* loadConfig(cwd, config);
|
|
126
131
|
const sorted = yield* Effect.mapError(sortManifests(root), (failure) => new SetupFailure({ message: failure.message }));
|
|
127
132
|
yield* Console.log(`quality: sort-package-json rewrote ${plural(sorted, "manifest")}.`);
|
|
128
|
-
yield* settle({ passes: biomePasses(root), snapshot: snapshotOf(root) });
|
|
133
|
+
yield* settle({ passes: biomePasses(root, exceptions), snapshot: snapshotOf(root) });
|
|
129
134
|
});
|
|
130
135
|
}
|
package/src/cli/lint.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { relative } from "node:path";
|
|
1
2
|
import { Console, Effect, type FileSystem } from "effect";
|
|
2
3
|
import { evaluate, passes } from "#engine/evaluate.ts";
|
|
3
4
|
import { openSession } from "#engine/session.ts";
|
|
@@ -17,7 +18,8 @@ export const lint = (
|
|
|
17
18
|
baseline: session.config.baseline,
|
|
18
19
|
checked: session.inventory.sources.length,
|
|
19
20
|
descriptions: new Map(session.config.active.map((rule) => [rule.id, rule.description])),
|
|
20
|
-
registry:
|
|
21
|
+
registry:
|
|
22
|
+
session.config.exceptions.length === 0 ? session.config.registry : `${relative(session.config.root, session.config.file)} exceptions`,
|
|
21
23
|
warnings,
|
|
22
24
|
}),
|
|
23
25
|
);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const USAGE = `Usage:
|
|
2
|
+
quality lint [--config <file>] [--warnings summary|all]
|
|
3
|
+
quality fix [--config <file>]
|
|
4
|
+
quality exceptions migrate [--config <file>] [--from <registry>]
|
|
5
|
+
quality baseline write [--config <file>] [--rule <id>]...
|
|
6
|
+
quality baseline prune [--config <file>] [--against <ref>]
|
|
7
|
+
quality baseline tighten [--config <file>] [--staged]
|
|
8
|
+
quality baseline migrate [--config <file>] [--from <file>]
|
|
9
|
+
quality hooks install [--config <file>] [--force]
|
|
10
|
+
quality hooks uninstall
|
|
11
|
+
quality hooks pre-commit [--config <file>]
|
|
12
|
+
|
|
13
|
+
lint Run every rule. Exits 1 on an error-level violation, a file over its baseline, a stale registry entry
|
|
14
|
+
or a baseline entry for a rule that is off or unknown.
|
|
15
|
+
fix Sort every package.json, apply Biome's lint fixes, unsafe ones included, its assist actions and formatting, then format again.
|
|
16
|
+
exceptions migrate Move legacy registry entries and Biome declarations into quality.config.ts exceptions and remove matching root overrides.
|
|
17
|
+
baseline write Record current error-level violations. Creates the baseline, or records the named rules again, replacing their entries.
|
|
18
|
+
baseline prune Drop fixed debt, lower entries to what is left and carry entries to files git saw move. Never adds or raises an entry.
|
|
19
|
+
baseline tighten Prune only the entries of files changed since HEAD, or with --staged, in the index.
|
|
20
|
+
baseline migrate Move a baseline from the earlier JSON format (--from, quality/baseline.json by default) to the configured file.
|
|
21
|
+
hooks install Install the git pre-commit hook, shared by every worktree, that runs hooks pre-commit. Keeps a hook that is not quality's
|
|
22
|
+
unless --force replaces it.
|
|
23
|
+
hooks uninstall Remove quality's pre-commit hook. Keeps a hook that is not quality's.
|
|
24
|
+
hooks pre-commit What the hook runs: lint, then package.json scripts typecheck and quality:commit when present.
|
|
25
|
+
|
|
26
|
+
--config <file> Config file; its directory is the repository root. Without this option, uses ./quality.config.ts when present, otherwise defaults.
|
|
27
|
+
--against <ref> The branch the work merges into, whose merge base prune follows moves from. Defaults to origin/HEAD, then origin/main, then origin/master.
|
|
28
|
+
Exit codes: 0 passed, 1 failed the gate, 2 could not run.`;
|
|
29
|
+
|
|
30
|
+
export const ADOPT_REMOVED =
|
|
31
|
+
"`quality adopt` was removed in 0.7.0. To record a rule's existing findings in the baseline, run `quality baseline write --rule <id>`; to apply the fixes Biome can make first, run `quality fix`.";
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { Console, Effect, FileSystem } from "effect";
|
|
3
|
+
import { loadConfig } from "#config/load.ts";
|
|
4
|
+
import { validOrFail } from "#decoded.ts";
|
|
5
|
+
import { readInput } from "#engine/baseline-file.ts";
|
|
6
|
+
import { decodeExceptions, type ExceptionDeclaration } from "#exceptions/declaration.ts";
|
|
7
|
+
import { migrateBiome } from "#exceptions/migrateBiome.ts";
|
|
8
|
+
import { migrateConfig } from "#exceptions/migrateConfig.ts";
|
|
9
|
+
import { decodeRegistry } from "#exceptions/registry.ts";
|
|
10
|
+
import { SetupFailure } from "#failure.ts";
|
|
11
|
+
import { writeText } from "#inventory/filesystem.ts";
|
|
12
|
+
|
|
13
|
+
function merge(entries: readonly ExceptionDeclaration[]): readonly ExceptionDeclaration[] {
|
|
14
|
+
const merged: ExceptionDeclaration[] = [];
|
|
15
|
+
for (const entry of entries) {
|
|
16
|
+
const index = merged.findIndex(
|
|
17
|
+
(other) =>
|
|
18
|
+
other.reason === entry.reason
|
|
19
|
+
&& other.subject === entry.subject
|
|
20
|
+
&& other.files.length === entry.files.length
|
|
21
|
+
&& other.files.every((file) => entry.files.includes(file)),
|
|
22
|
+
);
|
|
23
|
+
const previous = merged[index];
|
|
24
|
+
if (previous === undefined) {
|
|
25
|
+
merged.push(entry);
|
|
26
|
+
} else {
|
|
27
|
+
merged[index] = {
|
|
28
|
+
...previous,
|
|
29
|
+
rules: [previous.rules[0], ...new Set([...previous.rules.slice(1), ...entry.rules.filter((rule) => rule !== previous.rules[0])])],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return merged;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const migrateExceptions = Effect.fnUntraced(function* (
|
|
37
|
+
cwd: string,
|
|
38
|
+
configPath: string | undefined,
|
|
39
|
+
from: string | undefined,
|
|
40
|
+
): Effect.fn.Return<void, SetupFailure, FileSystem.FileSystem> {
|
|
41
|
+
const config = yield* loadConfig(cwd, configPath);
|
|
42
|
+
const registryPath = from ?? config.registry;
|
|
43
|
+
const raw = yield* readInput(config.root, registryPath);
|
|
44
|
+
if (from !== undefined && raw === undefined) {
|
|
45
|
+
return yield* new SetupFailure({ message: `${registryPath} does not exist. Pass an existing registry file with --from.` });
|
|
46
|
+
}
|
|
47
|
+
const registry = yield* validOrFail(registryPath, yield* Effect.promise(() => decodeRegistry(raw)));
|
|
48
|
+
const entries = merge([
|
|
49
|
+
...config.exceptions,
|
|
50
|
+
...registry.map(
|
|
51
|
+
({ file, reason, rule, subject }): ExceptionDeclaration => ({
|
|
52
|
+
files: [file],
|
|
53
|
+
reason,
|
|
54
|
+
rules: [rule],
|
|
55
|
+
...(subject === undefined ? {} : { subject }),
|
|
56
|
+
}),
|
|
57
|
+
),
|
|
58
|
+
...config.biomeDeclarations.map(({ includes, reason, rule }): ExceptionDeclaration => ({ files: includes, reason, rules: [`biome/${rule}`] })),
|
|
59
|
+
]);
|
|
60
|
+
yield* validOrFail(config.file, yield* Effect.promise(() => decodeExceptions(entries)));
|
|
61
|
+
const text = yield* readInput("", config.file);
|
|
62
|
+
const next = yield* Effect.try({
|
|
63
|
+
catch: (cause) => new SetupFailure({ message: String(cause) }),
|
|
64
|
+
try: () => migrateConfig(text, config.file, entries, config.biomeDeclarations.length > 0),
|
|
65
|
+
});
|
|
66
|
+
const updates: Array<{ readonly path: string; readonly text: string }> = [{ path: config.file, text: next }];
|
|
67
|
+
if (config.biomeDeclarations.length > 0) {
|
|
68
|
+
for (const file of ["biome.json", "biome.jsonc"]) {
|
|
69
|
+
const biome = yield* readInput(config.root, file);
|
|
70
|
+
if (biome !== undefined) {
|
|
71
|
+
const updated = yield* Effect.try({
|
|
72
|
+
catch: (cause) => new SetupFailure({ message: String(cause) }),
|
|
73
|
+
try: () => migrateBiome(biome, file, config.biomeDeclarations),
|
|
74
|
+
});
|
|
75
|
+
updates.push({ path: join(config.root, file), text: updated });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
for (const update of updates) {
|
|
80
|
+
yield* writeText(update.path, update.text).pipe(Effect.mapError((failure) => new SetupFailure({ message: failure.message })));
|
|
81
|
+
}
|
|
82
|
+
if (raw !== undefined) {
|
|
83
|
+
const fs = yield* FileSystem.FileSystem;
|
|
84
|
+
yield* fs.remove(join(config.root, registryPath)).pipe(Effect.mapError((failure) => new SetupFailure({ message: failure.message })));
|
|
85
|
+
}
|
|
86
|
+
yield* Console.log(`quality: moved exceptions to ${config.file}. Run quality fix and review the changes; remove unused declaration imports.`);
|
|
87
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Console, Effect } from "effect";
|
|
2
|
+
import { parseCommand, USAGE } from "#cli/args.ts";
|
|
3
|
+
import { writeBaseline } from "#cli/baseline.ts";
|
|
4
|
+
import { fix } from "#cli/fix.ts";
|
|
5
|
+
import { lint } from "#cli/lint.ts";
|
|
6
|
+
import { migrateBaseline } from "#cli/migrate.ts";
|
|
7
|
+
import { migrateExceptions } from "#cli/migrateExceptions.ts";
|
|
8
|
+
import { pruneBaseline, tightenBaseline } from "#cli/shrink.ts";
|
|
9
|
+
import { SetupFailure } from "#failure.ts";
|
|
10
|
+
import type { Launch } from "#hooks/command.ts";
|
|
11
|
+
import { installHook } from "#hooks/install.ts";
|
|
12
|
+
import { preCommitHook } from "#hooks/preCommit.ts";
|
|
13
|
+
import { uninstallHook } from "#hooks/uninstall.ts";
|
|
14
|
+
|
|
15
|
+
export interface Invocation {
|
|
16
|
+
readonly cwd: string;
|
|
17
|
+
readonly launch: Launch;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const program = Effect.fn("Cli.program")(function* (args: readonly string[], { cwd, launch }: Invocation) {
|
|
21
|
+
const parsed = parseCommand(args);
|
|
22
|
+
if (parsed._tag === "Usage") {
|
|
23
|
+
return yield* new SetupFailure({ message: `${parsed.problem}\n\n${USAGE}` });
|
|
24
|
+
}
|
|
25
|
+
const command = parsed.command;
|
|
26
|
+
switch (command._tag) {
|
|
27
|
+
case "Help":
|
|
28
|
+
return yield* Console.log(USAGE);
|
|
29
|
+
case "Lint":
|
|
30
|
+
return yield* lint(cwd, command.config, command.warnings);
|
|
31
|
+
case "Fix":
|
|
32
|
+
return yield* fix(cwd, command.config);
|
|
33
|
+
case "BaselineWrite":
|
|
34
|
+
return yield* writeBaseline(cwd, command.config, command.rules);
|
|
35
|
+
case "BaselinePrune":
|
|
36
|
+
return yield* pruneBaseline(cwd, command.config, command.against);
|
|
37
|
+
case "BaselineTighten":
|
|
38
|
+
return yield* tightenBaseline(cwd, command.config, command.staged);
|
|
39
|
+
case "ExceptionsMigrate":
|
|
40
|
+
return yield* migrateExceptions(cwd, command.config, command.from);
|
|
41
|
+
case "BaselineMigrate":
|
|
42
|
+
return yield* migrateBaseline(cwd, command.config, command.from);
|
|
43
|
+
case "HooksInstall":
|
|
44
|
+
return yield* installHook({ config: command.config, cwd, force: command.force, launch });
|
|
45
|
+
case "HooksUninstall":
|
|
46
|
+
return yield* uninstallHook(cwd);
|
|
47
|
+
case "HooksPreCommit":
|
|
48
|
+
return yield* preCommitHook(cwd, command.config);
|
|
49
|
+
}
|
|
50
|
+
});
|
package/src/cli/run-main.ts
CHANGED
|
@@ -11,23 +11,24 @@ import type { Git } from "#git/command.ts";
|
|
|
11
11
|
const INTERRUPTED = 130;
|
|
12
12
|
const COULD_NOT_RUN = 2;
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
function explain(cause: Cause.Cause<unknown>): Effect.Effect<void> {
|
|
15
15
|
const error = Cause.squash(cause);
|
|
16
16
|
if (error instanceof SetupFailure) {
|
|
17
17
|
return Console.error(`quality: ${error.message}`);
|
|
18
18
|
}
|
|
19
19
|
return Runtime.getErrorReported(error) ? Console.error(`quality: internal error\n${Cause.pretty(cause)}`) : Effect.void;
|
|
20
|
-
}
|
|
20
|
+
}
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
typeof error === "object" && error !== null && Runtime.errorExitCode in error ? Runtime.getErrorExitCode(error) : COULD_NOT_RUN;
|
|
22
|
+
function exitCodeOf(error: unknown): number {
|
|
23
|
+
return typeof error === "object" && error !== null && Runtime.errorExitCode in error ? Runtime.getErrorExitCode(error) : COULD_NOT_RUN;
|
|
24
|
+
}
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
export function exitCode(exit: Exit.Exit<unknown, unknown>): number {
|
|
26
27
|
if (Exit.isSuccess(exit)) {
|
|
27
28
|
return 0;
|
|
28
29
|
}
|
|
29
30
|
return Cause.hasInterruptsOnly(exit.cause) ? INTERRUPTED : exitCodeOf(Cause.squash(exit.cause));
|
|
30
|
-
}
|
|
31
|
+
}
|
|
31
32
|
|
|
32
33
|
const teardown: Runtime.Teardown = (exit, onExit) => {
|
|
33
34
|
process.stdout.write("", () => process.stderr.write("", () => onExit(exitCode(exit))));
|
|
@@ -45,11 +46,16 @@ export function quietOnClosedPipe(stream: EventEmitter): void {
|
|
|
45
46
|
});
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
const services = NodeChildProcessSpawner.layer.pipe(Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)));
|
|
49
|
+
export const services = NodeChildProcessSpawner.layer.pipe(Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)));
|
|
49
50
|
|
|
50
|
-
export
|
|
51
|
+
export function explained<TValue, TError, TRequirements>(
|
|
52
|
+
program: Effect.Effect<TValue, TError, TRequirements>,
|
|
53
|
+
): Effect.Effect<TValue, TError, TRequirements> {
|
|
54
|
+
return Effect.tapCause(program, (cause) => (Cause.hasInterruptsOnly(cause) ? Effect.void : explain(cause)));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function runMain<TValue, TError>(program: Effect.Effect<TValue, TError, FileSystem.FileSystem | Git>): void {
|
|
51
58
|
quietOnClosedPipe(process.stdout);
|
|
52
59
|
quietOnClosedPipe(process.stderr);
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
};
|
|
60
|
+
NodeRuntime.runMain(Effect.provide(explained(program), services), { disableErrorReporting: true, teardown });
|
|
61
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -1,52 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import process from "node:process";
|
|
3
|
-
import {
|
|
4
|
-
import { parseCommand, USAGE } from "#cli/args.ts";
|
|
5
|
-
import { writeBaseline } from "#cli/baseline.ts";
|
|
6
|
-
import { fix } from "#cli/fix.ts";
|
|
7
|
-
import { lint } from "#cli/lint.ts";
|
|
8
|
-
import { migrateBaseline } from "#cli/migrate.ts";
|
|
3
|
+
import { program } from "#cli/program.ts";
|
|
9
4
|
import { runMain } from "#cli/run-main.ts";
|
|
10
|
-
import { pruneBaseline, tightenBaseline } from "#cli/shrink.ts";
|
|
11
|
-
import { installHook } from "#hooks/install.ts";
|
|
12
|
-
import { preCommitHook } from "#hooks/preCommit.ts";
|
|
13
|
-
import { uninstallHook } from "#hooks/uninstall.ts";
|
|
14
|
-
import { SetupFailure } from "./failure.ts";
|
|
15
5
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const program = Effect.gen(function* () {
|
|
19
|
-
if (parsed._tag === "Usage") {
|
|
20
|
-
return yield* new SetupFailure({ message: `${parsed.problem}\n\n${USAGE}` });
|
|
21
|
-
}
|
|
22
|
-
const command = parsed.command;
|
|
23
|
-
switch (command._tag) {
|
|
24
|
-
case "Help":
|
|
25
|
-
return yield* Console.log(USAGE);
|
|
26
|
-
case "Lint":
|
|
27
|
-
return yield* lint(process.cwd(), command.config, command.warnings);
|
|
28
|
-
case "Fix":
|
|
29
|
-
return yield* fix(process.cwd(), command.config);
|
|
30
|
-
case "BaselineWrite":
|
|
31
|
-
return yield* writeBaseline(process.cwd(), command.config, command.rules);
|
|
32
|
-
case "BaselinePrune":
|
|
33
|
-
return yield* pruneBaseline(process.cwd(), command.config, command.against);
|
|
34
|
-
case "BaselineTighten":
|
|
35
|
-
return yield* tightenBaseline(process.cwd(), command.config, command.staged);
|
|
36
|
-
case "BaselineMigrate":
|
|
37
|
-
return yield* migrateBaseline(process.cwd(), command.config, command.from);
|
|
38
|
-
case "HooksInstall":
|
|
39
|
-
return yield* installHook({
|
|
40
|
-
config: command.config,
|
|
41
|
-
cwd: process.cwd(),
|
|
42
|
-
force: command.force,
|
|
43
|
-
launch: { options: process.execArgv, script: process.argv[1] ?? "" },
|
|
44
|
-
});
|
|
45
|
-
case "HooksUninstall":
|
|
46
|
-
return yield* uninstallHook(process.cwd());
|
|
47
|
-
case "HooksPreCommit":
|
|
48
|
-
return yield* preCommitHook(process.cwd(), command.config);
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
runMain(program);
|
|
6
|
+
runMain(program(process.argv.slice(2), { cwd: process.cwd(), launch: { options: process.execArgv, script: process.argv[1] ?? "" } }));
|
package/src/config/decode.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
2
|
import { type Decoded, decodeWith } from "#decoded.ts";
|
|
3
|
+
import { ExceptionDeclaration } from "#exceptions/declaration.ts";
|
|
3
4
|
import type { Rule } from "#rule.ts";
|
|
4
5
|
|
|
5
6
|
const isRule = (value: unknown): value is Rule =>
|
|
@@ -30,6 +31,7 @@ const PreCommitRemoved = Schema.Never.annotate({
|
|
|
30
31
|
const ConfigInput = Schema.Struct({
|
|
31
32
|
adopt: Schema.optionalKey(AdoptRemoved),
|
|
32
33
|
baseline: Schema.optionalKey(Schema.String),
|
|
34
|
+
exceptions: Schema.optionalKey(Schema.Array(ExceptionDeclaration)),
|
|
33
35
|
extensions: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
34
36
|
local: Schema.optionalKey(Schema.Array(LocalRule)),
|
|
35
37
|
preCommit: Schema.optionalKey(PreCommitRemoved),
|
package/src/config/load.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { basename, dirname, resolve } from "node:path";
|
|
2
2
|
import { pathToFileURL } from "node:url";
|
|
3
|
-
import { Effect, FileSystem } from "effect";
|
|
3
|
+
import { Effect, FileSystem, Schema } from "effect";
|
|
4
4
|
import { validOrFail } from "#decoded.ts";
|
|
5
|
+
import type { ExceptionDeclaration } from "#exceptions/declaration.ts";
|
|
5
6
|
import { SetupFailure } from "#failure.ts";
|
|
6
7
|
import type { InventoryScope } from "#inventory/collect.ts";
|
|
8
|
+
import { ConsumerDeclaration } from "#rules/suppressions/biome/ConsumerDeclaration.ts";
|
|
9
|
+
import type { Declaration } from "#rules/suppressions/biome/declarations.ts";
|
|
7
10
|
import { type ConfigInput, decodeConfig } from "./decode.ts";
|
|
8
11
|
import { CONFIG_FILE } from "./file.ts";
|
|
9
12
|
import { type ResolvedRules, resolveRules } from "./resolve.ts";
|
|
@@ -12,6 +15,8 @@ const DEFAULT_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs"
|
|
|
12
15
|
|
|
13
16
|
export interface ResolvedConfig extends ResolvedRules, InventoryScope {
|
|
14
17
|
readonly baseline: string;
|
|
18
|
+
readonly biomeDeclarations: readonly Declaration[];
|
|
19
|
+
readonly exceptions: readonly ExceptionDeclaration[];
|
|
15
20
|
readonly file: string;
|
|
16
21
|
readonly registry: string;
|
|
17
22
|
readonly root: string;
|
|
@@ -31,9 +36,18 @@ const importDefault = (file: string): Effect.Effect<unknown, SetupFailure> =>
|
|
|
31
36
|
: Effect.fail(new SetupFailure({ message: `${file} has no default export. Export defineConfig({ ... }) as the default.` })),
|
|
32
37
|
);
|
|
33
38
|
|
|
39
|
+
const LegacyOptions = Schema.Struct({ declared: Schema.Array(ConsumerDeclaration) });
|
|
40
|
+
function biomeDeclarations(config: ConfigInput): readonly Declaration[] {
|
|
41
|
+
const setting = config.rules?.["suppressions/biome-overrides"];
|
|
42
|
+
const options = typeof setting === "object" ? setting.options : undefined;
|
|
43
|
+
return Schema.is(LegacyOptions)(options) ? options.declared : [];
|
|
44
|
+
}
|
|
45
|
+
|
|
34
46
|
const resolved = (root: string, file: string, config: ConfigInput, rules: ResolvedRules): ResolvedConfig => ({
|
|
35
47
|
...rules,
|
|
36
48
|
baseline: config.baseline ?? "quality/baseline.jsonl",
|
|
49
|
+
biomeDeclarations: biomeDeclarations(config),
|
|
50
|
+
exceptions: config.exceptions ?? [],
|
|
37
51
|
exclude: [],
|
|
38
52
|
extensions: config.extensions ?? DEFAULT_EXTENSIONS,
|
|
39
53
|
file,
|
package/src/config/resolve.ts
CHANGED
|
@@ -21,6 +21,17 @@ const duplicateIssues = (rules: readonly Rule[]): readonly string[] =>
|
|
|
21
21
|
rules.findIndex((other) => other.id === rule.id) === index ? [] : [`local: rule id "${rule.id}" is already defined`],
|
|
22
22
|
);
|
|
23
23
|
|
|
24
|
+
function activeRule(rule: Rule, check: ActiveRule["check"]): ActiveRule {
|
|
25
|
+
return {
|
|
26
|
+
check,
|
|
27
|
+
description: rule.description,
|
|
28
|
+
family: rule.family === true,
|
|
29
|
+
id: rule.id,
|
|
30
|
+
level: "error",
|
|
31
|
+
registrable: rule.registrable !== false,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
24
35
|
const activate = (rule: Rule, setting: Setting | undefined): Effect.Effect<Decoded<ActiveRule>> =>
|
|
25
36
|
Effect.tryPromise({ catch: messageOf, try: () => rule.configure(optionsOf(setting)) }).pipe(
|
|
26
37
|
Effect.map(
|
|
@@ -29,7 +40,7 @@ const activate = (rule: Rule, setting: Setting | undefined): Effect.Effect<Decod
|
|
|
29
40
|
? { _tag: "Invalid", issues: configured.issues.map((issue) => `rules.${rule.id}.options: ${issue}`) }
|
|
30
41
|
: {
|
|
31
42
|
_tag: "Valid",
|
|
32
|
-
value:
|
|
43
|
+
value: activeRule(rule, configured.check),
|
|
33
44
|
},
|
|
34
45
|
),
|
|
35
46
|
Effect.catch((failure: string) =>
|
package/src/config.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ExceptionDeclaration } from "#exceptions/declaration.ts";
|
|
1
2
|
import type { builtInRules } from "#rules/built-in.ts";
|
|
2
3
|
import type { Rule } from "./rule.ts";
|
|
3
4
|
|
|
@@ -15,6 +16,7 @@ export type BuiltInRules = typeof builtInRules;
|
|
|
15
16
|
|
|
16
17
|
export interface QualityConfig<TLocal extends readonly Rule[] = readonly Rule[]> {
|
|
17
18
|
readonly baseline?: string;
|
|
19
|
+
readonly exceptions?: readonly ExceptionDeclaration[];
|
|
18
20
|
readonly extensions?: readonly string[];
|
|
19
21
|
readonly local?: TLocal;
|
|
20
22
|
readonly registry?: string;
|
package/src/engine/run-rules.ts
CHANGED
|
@@ -11,6 +11,7 @@ export interface ActiveRule {
|
|
|
11
11
|
readonly family: boolean;
|
|
12
12
|
readonly id: string;
|
|
13
13
|
readonly level: ActiveLevel;
|
|
14
|
+
readonly registrable?: boolean;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
const messageOf = (cause: unknown): string => (cause instanceof Error ? cause.message : String(cause));
|
|
@@ -18,24 +19,40 @@ const messageOf = (cause: unknown): string => (cause instanceof Error ? cause.me
|
|
|
18
19
|
// Registry and baseline entries match on the path, so every finding names its file the same way.
|
|
19
20
|
const repositoryPath = (root: string, file: string): string => posix(isAbsolute(file) ? relative(root, file) : file).replace(/^(\.\/)+/u, "");
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
function exceptionHint(rule: ActiveRule, finding: Finding, file: string, config: string): string {
|
|
23
|
+
const id = rule.family && finding.subject !== undefined ? `${rule.id}/${finding.subject}` : rule.id;
|
|
24
|
+
const biome = /^biome\/(?:lint|assist)\/[a-zA-Z]+\/[a-zA-Z]+$/u.test(id);
|
|
25
|
+
if (!biome && rule.registrable === false) {
|
|
26
|
+
return "";
|
|
27
|
+
}
|
|
28
|
+
const shape = JSON.stringify({
|
|
29
|
+
files: [file],
|
|
30
|
+
reason: "Explain why this exception is permanent.",
|
|
31
|
+
rules: [id],
|
|
32
|
+
...(!rule.family && finding.subject !== undefined ? { subject: finding.subject } : {}),
|
|
33
|
+
});
|
|
34
|
+
return ` For a permanent exception, add to exceptions in ${config}: ${shape}.`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const violationOf = (rule: ActiveRule, root: string, finding: Finding, exceptionFile: string): Violation => ({
|
|
22
38
|
...finding,
|
|
23
39
|
file: repositoryPath(root, finding.file),
|
|
24
40
|
level: rule.level,
|
|
41
|
+
message: finding.message + exceptionHint(rule, finding, repositoryPath(root, finding.file), exceptionFile),
|
|
25
42
|
rule: rule.family && finding.subject !== undefined ? `${rule.id}/${finding.subject}` : rule.id,
|
|
26
43
|
});
|
|
27
44
|
|
|
28
|
-
const runRule = (rule: ActiveRule, inputs: RuleInputs): Effect.Effect<readonly Violation[], SetupFailure> =>
|
|
45
|
+
const runRule = (rule: ActiveRule, inputs: RuleInputs, exceptionFile: string): Effect.Effect<readonly Violation[], SetupFailure> =>
|
|
29
46
|
Effect.map(
|
|
30
47
|
Effect.tryPromise({
|
|
31
48
|
catch: (cause) => new SetupFailure({ message: `rule ${rule.id} failed: ${messageOf(cause)}` }),
|
|
32
49
|
try: () => rule.check(inputs),
|
|
33
50
|
}),
|
|
34
|
-
(findings) => findings.map((finding) => violationOf(rule, inputs.root, finding)),
|
|
51
|
+
(findings) => findings.map((finding) => violationOf(rule, inputs.root, finding, exceptionFile)),
|
|
35
52
|
);
|
|
36
53
|
|
|
37
54
|
export const runRules = (rules: readonly ActiveRule[], inputs: RuleInputs): Effect.Effect<readonly Violation[], SetupFailure> =>
|
|
38
55
|
Effect.map(
|
|
39
|
-
Effect.forEach(rules, (rule) => runRule(rule, inputs), { concurrency: "unbounded" }),
|
|
56
|
+
Effect.forEach(rules, (rule) => runRule(rule, inputs, inputs.configFile ?? "quality.config.ts"), { concurrency: "unbounded" }),
|
|
40
57
|
(results) => results.flat(),
|
|
41
58
|
);
|
package/src/engine/session.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { resolve } from "node:path";
|
|
1
|
+
import { relative, resolve } from "node:path";
|
|
2
2
|
import { Effect, type FileSystem } from "effect";
|
|
3
3
|
import { loadConfig, type ResolvedConfig } from "#config/load.ts";
|
|
4
4
|
import { validOrFail } from "#decoded.ts";
|
|
5
5
|
import { decodeRegistry, type RegistryEntry } from "#exceptions/registry.ts";
|
|
6
|
+
import { registryEntries } from "#exceptions/registryEntries.ts";
|
|
6
7
|
import { SetupFailure } from "#failure.ts";
|
|
7
8
|
import { collectInventory, type Inventory } from "#inventory/collect.ts";
|
|
8
9
|
import { type FilesystemFailure, readOptionalText } from "#inventory/filesystem.ts";
|
|
@@ -27,7 +28,14 @@ export const scan = (config: ResolvedConfig): Effect.Effect<Pick<Session, "inven
|
|
|
27
28
|
const inventory = yield* setup(collectInventory(config.root, config));
|
|
28
29
|
const services = yield* Effect.context<FileSystem.FileSystem>();
|
|
29
30
|
const readText = (path: string): Promise<string | undefined> => Effect.runPromiseWith(services)(readOptionalText(resolve(config.root, path)));
|
|
30
|
-
const violations = yield* runRules(config.active, {
|
|
31
|
+
const violations = yield* runRules(config.active, {
|
|
32
|
+
configFile: relative(config.root, config.file),
|
|
33
|
+
exceptions: config.exceptions,
|
|
34
|
+
files: inventory.files,
|
|
35
|
+
readText,
|
|
36
|
+
root: config.root,
|
|
37
|
+
sources: inventory.sources,
|
|
38
|
+
});
|
|
31
39
|
return { inventory, violations };
|
|
32
40
|
});
|
|
33
41
|
|
|
@@ -38,5 +46,5 @@ export const openSession = (cwd: string, configPath: string | undefined): Effect
|
|
|
38
46
|
const registry = yield* validOrFail(config.registry, yield* Effect.promise(() => decodeRegistry(registryRaw)));
|
|
39
47
|
const baseline = yield* readBaseline(config);
|
|
40
48
|
const { inventory, violations } = yield* scan(config);
|
|
41
|
-
return { baseline, config, inventory, registry, violations };
|
|
49
|
+
return { baseline, config, inventory, registry: [...registry, ...registryEntries(config.exceptions)], violations };
|
|
42
50
|
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Finding, RuleInputs } from "#rule.ts";
|
|
2
|
+
|
|
3
|
+
export async function missingBiomeFiles(inputs: RuleInputs): Promise<readonly Finding[]> {
|
|
4
|
+
const paths = (inputs.exceptions ?? [])
|
|
5
|
+
.filter((entry) => entry.rules.some((rule) => rule.startsWith("biome/") && rule !== "biome/files/includes"))
|
|
6
|
+
.flatMap((entry) => entry.files);
|
|
7
|
+
const checked = await Promise.all(
|
|
8
|
+
[...new Set(paths)].map(async (file) =>
|
|
9
|
+
(await inputs.readText(file)) === undefined
|
|
10
|
+
? [{ file, message: `This Biome exception names a missing file. Remove it from exceptions in ${inputs.configFile ?? "quality.config.ts"}.` }]
|
|
11
|
+
: [],
|
|
12
|
+
),
|
|
13
|
+
);
|
|
14
|
+
return checked.flat();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function biomeExceptionHint(files: readonly string[], rule: string, config = "quality.config.ts"): string {
|
|
18
|
+
const entry = { files, reason: "Explain why this exception is permanent.", rules: [`biome/${rule}`] };
|
|
19
|
+
return ` Move this setting into exceptions in ${config}: ${JSON.stringify(entry)}, and remove the setting from the Biome config.`;
|
|
20
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { type Decoded, decodeWith } from "#decoded.ts";
|
|
3
|
+
import { ConsumerDeclaration } from "#rules/suppressions/biome/ConsumerDeclaration.ts";
|
|
4
|
+
|
|
5
|
+
const FILE = /^(?!\.\/)(?!\/)(?!.*(?:^|\/)\.\.(?:\/|$))[^*!?[\]{}\\]+$/u;
|
|
6
|
+
|
|
7
|
+
export const ExceptionDeclaration = Schema.Struct({
|
|
8
|
+
files: Schema.NonEmptyArray(Schema.NonEmptyString),
|
|
9
|
+
reason: Schema.String.check(Schema.isPattern(/\S/u, { expected: "a reason that says why the exception is permanent" })),
|
|
10
|
+
rules: Schema.NonEmptyArray(Schema.NonEmptyString),
|
|
11
|
+
subject: Schema.optionalKey(Schema.NonEmptyString),
|
|
12
|
+
}).check(
|
|
13
|
+
Schema.makeFilter(
|
|
14
|
+
({ files, reason, rules, subject }) =>
|
|
15
|
+
rules.every((rule) => {
|
|
16
|
+
if (rule.startsWith("biome/")) {
|
|
17
|
+
return (
|
|
18
|
+
subject === undefined
|
|
19
|
+
&& (rule === "biome/files/includes" || files.every((file) => FILE.test(file)))
|
|
20
|
+
&& Schema.is(ConsumerDeclaration)({ includes: files, reason, rule: rule.slice(6) })
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
return files.every((file) => FILE.test(file));
|
|
24
|
+
}),
|
|
25
|
+
{
|
|
26
|
+
message:
|
|
27
|
+
"Exceptions name explicit repository files and individual rules. Biome rules use biome/lint/<group>/<rule> or biome/assist/<group>/<action>, without a subject. Only biome/files/includes accepts ** and named directory exclusions.",
|
|
28
|
+
},
|
|
29
|
+
),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
export type ExceptionDeclaration = typeof ExceptionDeclaration.Type;
|
|
33
|
+
|
|
34
|
+
const standard = Schema.toStandardSchemaV1(Schema.Array(ExceptionDeclaration), { parseOptions: { errors: "all", onExcessProperty: "error" } });
|
|
35
|
+
|
|
36
|
+
export function decodeExceptions(value: unknown): Promise<Decoded<readonly ExceptionDeclaration[]>> {
|
|
37
|
+
return decodeWith(standard, value);
|
|
38
|
+
}
|