@shivaedev/quality 0.17.0 → 0.19.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 +54 -0
- package/README.md +151 -508
- package/biome/declarations.json +0 -45
- package/biome/plugins/constant-names.grit +1 -1
- package/biome/plugins/effect-fn-functions.grit +1 -1
- package/biome/preset.json +16 -9
- package/dist/baseline/compare.d.ts.map +1 -1
- package/dist/baseline/format.d.ts.map +1 -1
- package/dist/baseline/legacy.d.ts.map +1 -1
- package/dist/baseline/rewrite.d.ts.map +1 -1
- package/dist/baseline/update.d.ts.map +1 -1
- package/dist/baseline/update.js.map +1 -1
- package/dist/biome/nativeEffectCall.d.ts +4 -0
- package/dist/biome/nativeEffectCall.d.ts.map +1 -0
- package/dist/biome/nativeEffectCall.js +34 -0
- package/dist/biome/nativeEffectCall.js.map +1 -0
- package/dist/biome/recognizeDiagnostics.d.ts +3 -0
- package/dist/biome/recognizeDiagnostics.d.ts.map +1 -0
- package/dist/biome/recognizeDiagnostics.js +58 -0
- package/dist/biome/recognizeDiagnostics.js.map +1 -0
- package/dist/biome/report.d.ts +10 -0
- package/dist/biome/report.d.ts.map +1 -1
- package/dist/biome/report.js +23 -5
- package/dist/biome/report.js.map +1 -1
- package/dist/biome/run.js.map +1 -1
- package/dist/biome/syntax.d.ts +12 -0
- package/dist/biome/syntax.d.ts.map +1 -0
- package/dist/biome/syntax.js +29 -0
- package/dist/biome/syntax.js.map +1 -0
- package/dist/cli/args.d.ts +10 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +25 -3
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/baseline.d.ts +1 -1
- package/dist/cli/baseline.d.ts.map +1 -1
- package/dist/cli/baseline.js.map +1 -1
- package/dist/cli/fix.d.ts +6 -6
- package/dist/cli/fix.d.ts.map +1 -1
- package/dist/cli/fix.js +1 -1
- package/dist/cli/fix.js.map +1 -1
- package/dist/cli/lint.d.ts.map +1 -1
- package/dist/cli/migrate.d.ts.map +1 -1
- package/dist/cli/run-main.d.ts +1 -1
- package/dist/cli/run-main.d.ts.map +1 -1
- package/dist/cli/run-main.js.map +1 -1
- package/dist/cli/shrink.d.ts.map +1 -1
- package/dist/cli.js +14 -0
- package/dist/cli.js.map +1 -1
- package/dist/config/decode.d.ts +3 -3
- package/dist/config/decode.d.ts.map +1 -1
- package/dist/config/decode.js +5 -2
- package/dist/config/decode.js.map +1 -1
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +11 -6
- package/dist/config/load.js.map +1 -1
- package/dist/config/resolve.d.ts.map +1 -1
- package/dist/config/resolve.js +8 -12
- package/dist/config/resolve.js.map +1 -1
- package/dist/config.d.ts +10 -11
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/decoded.d.ts +4 -4
- package/dist/decoded.d.ts.map +1 -1
- package/dist/decoded.js.map +1 -1
- package/dist/engine/baseline-file.d.ts.map +1 -1
- package/dist/engine/evaluate.d.ts.map +1 -1
- package/dist/engine/run-rules.d.ts.map +1 -1
- package/dist/engine/session.d.ts.map +1 -1
- package/dist/engine/session.js.map +1 -1
- package/dist/engine/violation.d.ts +1 -1
- package/dist/engine/violation.d.ts.map +1 -1
- package/dist/engine/violation.js.map +1 -1
- package/dist/exceptions/registry.d.ts.map +1 -1
- package/dist/git/base.d.ts.map +1 -1
- package/dist/git/command.d.ts.map +1 -1
- package/dist/git/history.d.ts.map +1 -1
- package/dist/hooks/command.d.ts +13 -0
- package/dist/hooks/command.d.ts.map +1 -0
- package/dist/hooks/command.js +20 -0
- package/dist/hooks/command.js.map +1 -0
- package/dist/hooks/install.d.ts +11 -0
- package/dist/hooks/install.d.ts.map +1 -0
- package/dist/hooks/install.js +52 -0
- package/dist/hooks/install.js.map +1 -0
- package/dist/hooks/location.d.ts +9 -0
- package/dist/hooks/location.d.ts.map +1 -0
- package/dist/hooks/location.js +22 -0
- package/dist/hooks/location.js.map +1 -0
- package/dist/hooks/preCommit.d.ts +5 -0
- package/dist/hooks/preCommit.d.ts.map +1 -0
- package/dist/hooks/preCommit.js +50 -0
- package/dist/hooks/preCommit.js.map +1 -0
- package/dist/hooks/shim.d.ts +4 -0
- package/dist/hooks/shim.d.ts.map +1 -0
- package/dist/hooks/shim.js +26 -0
- package/dist/hooks/shim.js.map +1 -0
- package/dist/hooks/uninstall.d.ts +4 -0
- package/dist/hooks/uninstall.d.ts.map +1 -0
- package/dist/hooks/uninstall.js +19 -0
- package/dist/hooks/uninstall.js.map +1 -0
- package/dist/imports/browserGlobals.d.ts +3 -0
- package/dist/imports/browserGlobals.d.ts.map +1 -0
- package/dist/imports/browserGlobals.js +125 -0
- package/dist/imports/browserGlobals.js.map +1 -0
- package/dist/imports/browserNames.d.ts +2 -0
- package/dist/imports/browserNames.d.ts.map +1 -0
- package/dist/imports/browserNames.js +78 -0
- package/dist/imports/browserNames.js.map +1 -0
- package/dist/imports/fences/check.d.ts +4 -0
- package/dist/imports/fences/check.d.ts.map +1 -0
- package/dist/imports/fences/check.js +10 -0
- package/dist/imports/fences/check.js.map +1 -0
- package/dist/imports/fences/declarations.d.ts +5 -0
- package/dist/imports/fences/declarations.d.ts.map +1 -0
- package/dist/imports/fences/declarations.js +29 -0
- package/dist/imports/fences/declarations.js.map +1 -0
- package/dist/imports/fences/discover.d.ts +5 -0
- package/dist/imports/fences/discover.d.ts.map +1 -0
- package/dist/imports/fences/discover.js +41 -0
- package/dist/imports/fences/discover.js.map +1 -0
- package/dist/imports/fences/match.js.map +1 -1
- package/dist/imports/graph.js +2 -2
- package/dist/imports/graph.js.map +1 -1
- package/dist/imports/projects.js.map +1 -1
- package/dist/imports/runtime.d.ts +5 -0
- package/dist/imports/runtime.d.ts.map +1 -0
- package/dist/imports/runtime.js +32 -0
- package/dist/imports/runtime.js.map +1 -0
- package/dist/imports/workspace.d.ts +1 -1
- package/dist/imports/workspace.d.ts.map +1 -1
- package/dist/imports/workspace.js +5 -1
- package/dist/imports/workspace.js.map +1 -1
- package/dist/inventory/collect.d.ts.map +1 -1
- package/dist/inventory/collect.js +8 -1
- package/dist/inventory/collect.js.map +1 -1
- package/dist/inventory/filesystem.d.ts.map +1 -1
- package/dist/inventory/ignore-scope.d.ts.map +1 -1
- package/dist/inventory/trackedPaths.d.ts +3 -0
- package/dist/inventory/trackedPaths.d.ts.map +1 -0
- package/dist/inventory/trackedPaths.js +26 -0
- package/dist/inventory/trackedPaths.js.map +1 -0
- package/dist/inventory/walk.d.ts +4 -4
- package/dist/inventory/walk.d.ts.map +1 -1
- package/dist/inventory/walk.js +32 -12
- package/dist/inventory/walk.js.map +1 -1
- package/dist/naming/testName.d.ts.map +1 -1
- package/dist/report/plural.d.ts.map +1 -1
- package/dist/report/render.d.ts.map +1 -1
- package/dist/report/stale.d.ts.map +1 -1
- package/dist/rules/built-in.d.ts +2 -3
- package/dist/rules/built-in.d.ts.map +1 -1
- package/dist/rules/built-in.js +6 -0
- package/dist/rules/built-in.js.map +1 -1
- package/dist/rules/comments/kinds.d.ts.map +1 -1
- package/dist/rules/comments/max-per-file.js +1 -1
- package/dist/rules/comments/max-per-file.js.map +1 -1
- package/dist/rules/comments/pattern-rule.d.ts.map +1 -1
- package/dist/rules/comments/scan.d.ts.map +1 -1
- package/dist/rules/files/folderNames.js +1 -1
- package/dist/rules/files/folderNames.js.map +1 -1
- package/dist/rules/files/otherNames.d.ts.map +1 -1
- package/dist/rules/files/otherNames.js +1 -10
- package/dist/rules/files/otherNames.js.map +1 -1
- package/dist/rules/imports/bundleInputs.d.ts +7 -0
- package/dist/rules/imports/bundleInputs.d.ts.map +1 -0
- package/dist/rules/imports/bundleInputs.js +77 -0
- package/dist/rules/imports/bundleInputs.js.map +1 -0
- package/dist/rules/imports/fences.d.ts +1 -2
- package/dist/rules/imports/fences.d.ts.map +1 -1
- package/dist/rules/imports/fences.js +4 -14
- package/dist/rules/imports/fences.js.map +1 -1
- package/dist/rules/imports/manifest.d.ts +2 -0
- package/dist/rules/imports/manifest.d.ts.map +1 -0
- package/dist/rules/imports/manifest.js +98 -0
- package/dist/rules/imports/manifest.js.map +1 -0
- package/dist/rules/imports/noTestCode.d.ts +2 -0
- package/dist/rules/imports/noTestCode.d.ts.map +1 -0
- package/dist/rules/imports/noTestCode.js +17 -0
- package/dist/rules/imports/noTestCode.js.map +1 -0
- package/dist/rules/imports/runtime.d.ts +2 -0
- package/dist/rules/imports/runtime.d.ts.map +1 -0
- package/dist/rules/imports/runtime.js +41 -0
- package/dist/rules/imports/runtime.js.map +1 -0
- package/dist/rules/max-lines.d.ts +0 -1
- package/dist/rules/max-lines.d.ts.map +1 -1
- package/dist/rules/max-lines.js +3 -10
- package/dist/rules/max-lines.js.map +1 -1
- package/dist/rules/suppressions/biome/configs.d.ts.map +1 -1
- package/dist/rules/suppressions/biome/declarations.d.ts.map +1 -1
- package/dist/rules/suppressions/biome/json.d.ts.map +1 -1
- package/dist/rules/suppressions/biome/resolve.d.ts.map +1 -1
- package/dist/rules/suppressions/biome/weakenings.d.ts.map +1 -1
- package/dist/rules/suppressions/biome-overrides.d.ts.map +1 -1
- package/dist/rules/suppressions/biome-overrides.js +21 -4
- package/dist/rules/suppressions/biome-overrides.js.map +1 -1
- package/dist/rules/suppressions/biomeRecommended.js +2 -2
- package/dist/rules/suppressions/biomeRecommended.js.map +1 -1
- package/dist/rules/suppressions/directives.d.ts.map +1 -1
- package/dist/rules/suppressions/no-inline.js +1 -1
- package/dist/rules/suppressions/no-inline.js.map +1 -1
- package/dist/rules/suppressions/stylesheet-comments.d.ts.map +1 -1
- package/dist/rules/syntax.d.ts.map +1 -1
- package/dist/standard-schema.d.ts +1 -1
- package/dist/standard-schema.d.ts.map +1 -1
- package/dist/standard-schema.js +10 -2
- package/dist/standard-schema.js.map +1 -1
- package/dist/vitest.d.ts +1 -3
- package/dist/vitest.d.ts.map +1 -1
- package/dist/vitest.js +11 -6
- package/dist/vitest.js.map +1 -1
- package/docs/debt-and-hooks.md +90 -0
- package/docs/modules.md +253 -0
- package/docs/naming.md +126 -0
- package/docs/north-star.md +61 -0
- package/docs/roadmap.md +37 -0
- package/docs/rules.md +308 -0
- package/docs/tooling.md +120 -0
- package/package.json +8 -6
- package/src/biome/nativeEffectCall.ts +45 -0
- package/src/biome/recognizeDiagnostics.ts +67 -0
- package/src/biome/report.ts +24 -6
- package/src/biome/syntax.ts +36 -0
- package/src/cli/args.ts +29 -3
- package/src/cli/baseline.ts +3 -3
- package/src/cli/fix.ts +13 -13
- package/src/cli/run-main.ts +1 -1
- package/src/cli.ts +14 -0
- package/src/config/decode.ts +7 -2
- package/src/config/load.ts +15 -6
- package/src/config/resolve.ts +10 -15
- package/src/config.ts +8 -9
- package/src/decoded.ts +3 -3
- package/src/engine/session.ts +3 -3
- package/src/engine/violation.ts +2 -2
- package/src/hooks/command.ts +34 -0
- package/src/hooks/install.ts +69 -0
- package/src/hooks/location.ts +28 -0
- package/src/hooks/preCommit.ts +68 -0
- package/src/hooks/shim.ts +28 -0
- package/src/hooks/uninstall.ts +19 -0
- package/src/imports/browserGlobals.ts +142 -0
- package/src/imports/browserNames.ts +80 -0
- package/src/imports/fences/check.ts +12 -0
- package/src/imports/fences/declarations.ts +38 -0
- package/src/imports/fences/discover.ts +37 -0
- package/src/imports/graph.ts +2 -2
- package/src/imports/projects.ts +2 -2
- package/src/imports/runtime.ts +37 -0
- package/src/imports/workspace.ts +7 -2
- package/src/inventory/collect.ts +8 -1
- package/src/inventory/trackedPaths.ts +28 -0
- package/src/inventory/walk.ts +65 -36
- package/src/rules/built-in.ts +6 -0
- package/src/rules/comments/max-per-file.ts +1 -1
- package/src/rules/files/folderNames.ts +1 -1
- package/src/rules/files/otherNames.ts +1 -10
- package/src/rules/imports/bundleInputs.ts +94 -0
- package/src/rules/imports/fences.ts +4 -16
- package/src/rules/imports/manifest.ts +116 -0
- package/src/rules/imports/noTestCode.ts +18 -0
- package/src/rules/imports/runtime.ts +43 -0
- package/src/rules/max-lines.ts +4 -10
- package/src/rules/suppressions/biome-overrides.ts +31 -4
- package/src/rules/suppressions/biomeRecommended.ts +2 -2
- package/src/rules/suppressions/no-inline.ts +1 -1
- package/src/standard-schema.ts +11 -3
- package/src/vitest.ts +11 -6
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { Console, Effect, Schema } from "effect";
|
|
3
|
+
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
|
|
4
|
+
import { lint } from "#cli/lint.ts";
|
|
5
|
+
import { loadConfig } from "#config/load.ts";
|
|
6
|
+
import { decodeWith, validOrFail } from "#decoded.ts";
|
|
7
|
+
import { GateFailed, SetupFailure } from "#failure.ts";
|
|
8
|
+
import { readOptionalText } from "#inventory/filesystem.ts";
|
|
9
|
+
|
|
10
|
+
const COMMIT_AGAIN =
|
|
11
|
+
"help: fix what the output above reports, then commit again; `quality hooks pre-commit` repeats these checks without committing.";
|
|
12
|
+
|
|
13
|
+
const Manifest = Schema.toStandardSchemaV1(
|
|
14
|
+
Schema.fromJsonString(
|
|
15
|
+
Schema.Struct({
|
|
16
|
+
packageManager: Schema.optionalKey(Schema.String),
|
|
17
|
+
scripts: Schema.optionalKey(Schema.Record(Schema.String, Schema.String)),
|
|
18
|
+
}),
|
|
19
|
+
),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const commitScripts = Effect.fnUntraced(function* (root: string) {
|
|
23
|
+
const file = join(root, "package.json");
|
|
24
|
+
const text = yield* readOptionalText(file).pipe(Effect.mapError((error) => new SetupFailure({ message: error.message })));
|
|
25
|
+
if (text === undefined) {
|
|
26
|
+
return { manager: "npm", scripts: [] };
|
|
27
|
+
}
|
|
28
|
+
const manifest = yield* validOrFail(file, yield* Effect.promise(() => decodeWith(Manifest, text)));
|
|
29
|
+
const manager = manifest.packageManager?.split("@")[0] ?? "npm";
|
|
30
|
+
if (!["npm", "pnpm", "yarn", "bun"].includes(manager)) {
|
|
31
|
+
return yield* new SetupFailure({ message: `${file}: unsupported packageManager "${manager}". Use npm, pnpm, yarn or bun.` });
|
|
32
|
+
}
|
|
33
|
+
return { manager, scripts: ["typecheck", "quality:commit"].filter((script) => manifest.scripts?.[script] !== undefined) };
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const runScript = Effect.fnUntraced(
|
|
37
|
+
function* (root: string, manager: string, script: string) {
|
|
38
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
39
|
+
const child = yield* spawner.spawn(
|
|
40
|
+
ChildProcess.make(manager, ["run", script], { cwd: root, stderr: "inherit", stdin: "inherit", stdout: "inherit" }),
|
|
41
|
+
);
|
|
42
|
+
return Number(yield* child.exitCode);
|
|
43
|
+
},
|
|
44
|
+
Effect.scoped,
|
|
45
|
+
Effect.mapError((error) => new SetupFailure({ message: `cannot run a pre-commit script: ${error.message}` })),
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
export const preCommitHook = Effect.fn("Hooks.preCommitHook")(function* (cwd: string, path: string | undefined) {
|
|
49
|
+
const config = yield* loadConfig(cwd, path);
|
|
50
|
+
const { manager, scripts } = yield* commitScripts(config.root);
|
|
51
|
+
const linted = yield* lint(cwd, path, "summary").pipe(
|
|
52
|
+
Effect.as(true),
|
|
53
|
+
Effect.catchTag("GateFailed", () => Effect.succeed(false)),
|
|
54
|
+
);
|
|
55
|
+
const failed = linted ? [] : ["quality lint"];
|
|
56
|
+
for (const script of scripts) {
|
|
57
|
+
const command = `${manager} run ${script}`;
|
|
58
|
+
yield* Console.log(`quality: running \`${command}\``);
|
|
59
|
+
const code = yield* runScript(config.root, manager, script);
|
|
60
|
+
if (code !== 0) {
|
|
61
|
+
failed.push(`\`${command}\` (exit code ${code})`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (failed.length > 0) {
|
|
65
|
+
yield* Console.error(`quality: pre-commit failed: ${failed.join(", ")}.\n${COMMIT_AGAIN}`);
|
|
66
|
+
return yield* new GateFailed();
|
|
67
|
+
}
|
|
68
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type HookCommand, shellWords } from "./command.ts";
|
|
2
|
+
|
|
3
|
+
const OWNER = "# Installed by @shivaedev/quality: `quality hooks install` writes it, `quality hooks uninstall` removes it.";
|
|
4
|
+
|
|
5
|
+
export function isHookShim(text: string): boolean {
|
|
6
|
+
return text.split("\n").includes(OWNER);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// The hook moves to the checked-out worktree first, so each worktree commits under its own config and its own quality.
|
|
10
|
+
// Git sets GIT_DIR for a hook in a linked worktree, which makes git take the current folder as the worktree root unless GIT_WORK_TREE names it.
|
|
11
|
+
export function hookShim(directory: string, command: HookCommand): string {
|
|
12
|
+
const folder = directory === "" ? "" : `/${shellWords([directory])}`;
|
|
13
|
+
const exec = [shellWords(command.runner), '"$quality"', shellWords(command.args)].filter((part) => part !== "");
|
|
14
|
+
return [
|
|
15
|
+
"#!/bin/sh",
|
|
16
|
+
OWNER,
|
|
17
|
+
'top="$(git rev-parse --show-toplevel)" || exit 2',
|
|
18
|
+
'if [ -n "$GIT_DIR" ] && [ -z "$GIT_WORK_TREE" ]; then export GIT_WORK_TREE="$top"; fi',
|
|
19
|
+
`cd "$top"${folder} || exit 2`,
|
|
20
|
+
`quality=${shellWords([command.launcher])}`,
|
|
21
|
+
'if [ ! -e "$quality" ]; then',
|
|
22
|
+
'\tprintf \'quality: cannot check this commit: %s/%s does not exist.\\nhelp: install the dependencies of this worktree, then commit again.\\n\' "$PWD" "$quality" >&2',
|
|
23
|
+
"\texit 2",
|
|
24
|
+
"fi",
|
|
25
|
+
`exec ${exec.join(" ")}`,
|
|
26
|
+
"",
|
|
27
|
+
].join("\n");
|
|
28
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Console, Effect, FileSystem } from "effect";
|
|
2
|
+
import { SetupFailure } from "#failure.ts";
|
|
3
|
+
import { readOptionalText } from "#inventory/filesystem.ts";
|
|
4
|
+
import { hookLocation } from "./location.ts";
|
|
5
|
+
import { isHookShim } from "./shim.ts";
|
|
6
|
+
|
|
7
|
+
export const uninstallHook = Effect.fn("Hooks.uninstallHook")(function* (cwd: string) {
|
|
8
|
+
const fs = yield* FileSystem.FileSystem;
|
|
9
|
+
const { file } = yield* hookLocation(cwd);
|
|
10
|
+
const existing = yield* Effect.mapError(readOptionalText(file), (failure) => new SetupFailure({ message: failure.message }));
|
|
11
|
+
if (existing === undefined) {
|
|
12
|
+
return yield* Console.log(`quality: there is no pre-commit hook at ${file}.`);
|
|
13
|
+
}
|
|
14
|
+
if (!isHookShim(existing)) {
|
|
15
|
+
return yield* Console.error(`quality: kept ${file}: it is not quality's pre-commit hook.`);
|
|
16
|
+
}
|
|
17
|
+
yield* Effect.mapError(fs.remove(file), (error) => new SetupFailure({ message: `cannot remove ${file}: ${error.message}` }));
|
|
18
|
+
yield* Console.log(`quality: removed the pre-commit hook at ${file}.`);
|
|
19
|
+
});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import type { Finding, SourceFile } from "#rule.ts";
|
|
3
|
+
import { parse } from "#rules/syntax.ts";
|
|
4
|
+
import { browserNames } from "./browserNames.ts";
|
|
5
|
+
|
|
6
|
+
function checkerOf(source: ts.SourceFile): ts.TypeChecker {
|
|
7
|
+
const options: ts.CompilerOptions = { allowJs: true, noLib: true, noResolve: true };
|
|
8
|
+
const host = ts.createCompilerHost(options);
|
|
9
|
+
host.getSourceFile = (path) => (path === source.fileName ? source : undefined);
|
|
10
|
+
return ts.createProgram([source.fileName], options, host).getTypeChecker();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function unbound(checker: ts.TypeChecker, node: ts.Identifier): boolean {
|
|
14
|
+
return (checker.getSymbolAtLocation(node)?.declarations ?? []).length === 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function globalThisRoot(expression: ts.Expression, checker: ts.TypeChecker): boolean {
|
|
18
|
+
let root = expression;
|
|
19
|
+
while (
|
|
20
|
+
ts.isParenthesizedExpression(root)
|
|
21
|
+
|| ts.isAsExpression(root)
|
|
22
|
+
|| ts.isTypeAssertionExpression(root)
|
|
23
|
+
|| ts.isNonNullExpression(root)
|
|
24
|
+
|| ts.isSatisfiesExpression(root)
|
|
25
|
+
) {
|
|
26
|
+
root = root.expression;
|
|
27
|
+
}
|
|
28
|
+
return ts.isIdentifier(root) && root.text === "globalThis" && unbound(checker, root);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function qualifiedName(node: ts.PropertyAccessExpression | ts.ElementAccessExpression, checker: ts.TypeChecker): string | undefined {
|
|
32
|
+
const root = node.expression;
|
|
33
|
+
if (!globalThisRoot(root, checker)) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
37
|
+
return node.name.text;
|
|
38
|
+
}
|
|
39
|
+
return ts.isStringLiteral(node.argumentExpression) ? node.argumentExpression.text : undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function bindingName(node: ts.Identifier): boolean {
|
|
43
|
+
const parent = node.parent;
|
|
44
|
+
return ts.isBindingElement(parent) && (parent.name === node || parent.propertyName === node);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function globalName(node: ts.Node, checker: ts.TypeChecker): string | undefined {
|
|
48
|
+
if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) {
|
|
49
|
+
return qualifiedName(node, checker);
|
|
50
|
+
}
|
|
51
|
+
if (!ts.isIdentifier(node)) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
const parent = node.parent;
|
|
55
|
+
if (bindingName(node) || ts.isLabeledStatement(parent) || ts.isBreakStatement(parent) || ts.isContinueStatement(parent)) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
if ((ts.isPropertyAccessExpression(parent) && parent.name === node) || (ts.isPropertyAssignment(parent) && parent.name === node)) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
if (ts.isShorthandPropertyAssignment(parent)) {
|
|
62
|
+
return checker.getShorthandAssignmentValueSymbol(parent) === undefined ? node.text : undefined;
|
|
63
|
+
}
|
|
64
|
+
return unbound(checker, node) ? node.text : undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function propertyName(node: ts.Node): readonly string[] {
|
|
68
|
+
if (ts.isComputedPropertyName(node) && ts.isStringLiteral(node.expression)) {
|
|
69
|
+
return [node.expression.text];
|
|
70
|
+
}
|
|
71
|
+
return ts.isIdentifier(node) || ts.isStringLiteral(node) ? [node.text] : [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function destructuringOf(
|
|
75
|
+
node: ts.Node,
|
|
76
|
+
): { readonly pattern: ts.ObjectBindingPattern | ts.ObjectLiteralExpression; readonly root: ts.Expression } | undefined {
|
|
77
|
+
if ((ts.isVariableDeclaration(node) || ts.isParameter(node)) && ts.isObjectBindingPattern(node.name) && node.initializer !== undefined) {
|
|
78
|
+
return { pattern: node.name, root: node.initializer };
|
|
79
|
+
}
|
|
80
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isObjectLiteralExpression(node.left)) {
|
|
81
|
+
return { pattern: node.left, root: node.right };
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function destructuredNames(node: ts.Node, checker: ts.TypeChecker): readonly string[] {
|
|
87
|
+
const destructuring = destructuringOf(node);
|
|
88
|
+
if (destructuring === undefined) {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
const { pattern, root } = destructuring;
|
|
92
|
+
if (!globalThisRoot(root, checker)) {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
if (ts.isObjectBindingPattern(pattern)) {
|
|
96
|
+
return pattern.elements.flatMap((element) => (element.dotDotDotToken === undefined ? propertyName(element.propertyName ?? element.name) : []));
|
|
97
|
+
}
|
|
98
|
+
return pattern.properties.flatMap((property) =>
|
|
99
|
+
ts.isShorthandPropertyAssignment(property) || ts.isPropertyAssignment(property) ? propertyName(property.name) : [],
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function superclassesOf(node: ts.HeritageClause): readonly ts.Expression[] {
|
|
104
|
+
if (node.token !== ts.SyntaxKind.ExtendsKeyword || !(ts.isClassDeclaration(node.parent) || ts.isClassExpression(node.parent))) {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
return node.types.map((base) => base.expression);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function browserGlobals(file: SourceFile): readonly Finding[] {
|
|
111
|
+
const parsed = parse(file);
|
|
112
|
+
if (parsed === undefined) {
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
const source = parsed;
|
|
116
|
+
const checker = checkerOf(source);
|
|
117
|
+
const findings: Finding[] = [];
|
|
118
|
+
function visit(node: ts.Node): void {
|
|
119
|
+
if (ts.isHeritageClause(node)) {
|
|
120
|
+
for (const expression of superclassesOf(node)) {
|
|
121
|
+
visit(expression);
|
|
122
|
+
}
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (ts.isTypeNode(node) || ts.isImportDeclaration(node) || ts.isExportSpecifier(node)) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const direct = globalName(node, checker);
|
|
129
|
+
const names = [...destructuredNames(node, checker), ...(direct === undefined ? [] : [direct])];
|
|
130
|
+
for (const name of names.filter((candidate) => browserNames.has(candidate))) {
|
|
131
|
+
findings.push({
|
|
132
|
+
file: file.path,
|
|
133
|
+
line: source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1,
|
|
134
|
+
message: `Shared code uses browser global "${name}". Move browser-dependent code into src/client/ or pass the value into shared code.`,
|
|
135
|
+
subject: name,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
ts.forEachChild(node, visit);
|
|
139
|
+
}
|
|
140
|
+
visit(source);
|
|
141
|
+
return findings;
|
|
142
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import ts from "typescript";
|
|
4
|
+
|
|
5
|
+
const portable = new Set([
|
|
6
|
+
"AbortController",
|
|
7
|
+
"AbortSignal",
|
|
8
|
+
"Blob",
|
|
9
|
+
"BroadcastChannel",
|
|
10
|
+
"ByteLengthQueuingStrategy",
|
|
11
|
+
"CompressionStream",
|
|
12
|
+
"CountQueuingStrategy",
|
|
13
|
+
"Crypto",
|
|
14
|
+
"CryptoKey",
|
|
15
|
+
"CustomEvent",
|
|
16
|
+
"DOMException",
|
|
17
|
+
"DecompressionStream",
|
|
18
|
+
"Event",
|
|
19
|
+
"EventTarget",
|
|
20
|
+
"File",
|
|
21
|
+
"FormData",
|
|
22
|
+
"Headers",
|
|
23
|
+
"MessageChannel",
|
|
24
|
+
"MessageEvent",
|
|
25
|
+
"MessagePort",
|
|
26
|
+
"Performance",
|
|
27
|
+
"PerformanceEntry",
|
|
28
|
+
"PerformanceMark",
|
|
29
|
+
"PerformanceMeasure",
|
|
30
|
+
"PerformanceObserver",
|
|
31
|
+
"PerformanceObserverEntryList",
|
|
32
|
+
"PerformanceResourceTiming",
|
|
33
|
+
"ReadableByteStreamController",
|
|
34
|
+
"ReadableStream",
|
|
35
|
+
"ReadableStreamBYOBReader",
|
|
36
|
+
"ReadableStreamBYOBRequest",
|
|
37
|
+
"ReadableStreamDefaultController",
|
|
38
|
+
"ReadableStreamDefaultReader",
|
|
39
|
+
"Request",
|
|
40
|
+
"Response",
|
|
41
|
+
"SubtleCrypto",
|
|
42
|
+
"TextDecoder",
|
|
43
|
+
"TextDecoderStream",
|
|
44
|
+
"TextEncoder",
|
|
45
|
+
"TextEncoderStream",
|
|
46
|
+
"TransformStream",
|
|
47
|
+
"TransformStreamDefaultController",
|
|
48
|
+
"URL",
|
|
49
|
+
"URLPattern",
|
|
50
|
+
"URLSearchParams",
|
|
51
|
+
"WebSocket",
|
|
52
|
+
"WritableStream",
|
|
53
|
+
"WritableStreamDefaultController",
|
|
54
|
+
"WritableStreamDefaultWriter",
|
|
55
|
+
"atob",
|
|
56
|
+
"btoa",
|
|
57
|
+
"clearInterval",
|
|
58
|
+
"clearTimeout",
|
|
59
|
+
"console",
|
|
60
|
+
"crypto",
|
|
61
|
+
"fetch",
|
|
62
|
+
"performance",
|
|
63
|
+
"queueMicrotask",
|
|
64
|
+
"setInterval",
|
|
65
|
+
"setTimeout",
|
|
66
|
+
"structuredClone",
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
function namesInDom(): readonly string[] {
|
|
70
|
+
const path = join(dirname(ts.getDefaultLibFilePath({})), "lib.dom.d.ts");
|
|
71
|
+
const source = ts.createSourceFile(path, readFileSync(path, "utf8"), ts.ScriptTarget.Latest, true);
|
|
72
|
+
return source.statements.flatMap((statement) => {
|
|
73
|
+
if (ts.isVariableStatement(statement)) {
|
|
74
|
+
return statement.declarationList.declarations.flatMap(({ name }) => (ts.isIdentifier(name) ? [name.text] : []));
|
|
75
|
+
}
|
|
76
|
+
return ts.isFunctionDeclaration(statement) && statement.name !== undefined ? [statement.name.text] : [];
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const browserNames: ReadonlySet<string> = new Set(namesInDom().filter((name) => !portable.has(name)));
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { importGraph } from "#imports/graph.ts";
|
|
2
|
+
import type { Findings, RuleInputs } from "#rule.ts";
|
|
3
|
+
import { discoverFences } from "./discover.ts";
|
|
4
|
+
import type { Fence } from "./model.ts";
|
|
5
|
+
import { compilePolicy } from "./policy.ts";
|
|
6
|
+
|
|
7
|
+
export async function checkFences(inputs: RuleInputs, configured: readonly Fence[]): Promise<Findings> {
|
|
8
|
+
const graph = await importGraph(inputs);
|
|
9
|
+
const fences = [...configured, ...(await discoverFences(inputs, graph.packages))];
|
|
10
|
+
const policy = compilePolicy(fences, { files: inputs.files, packages: graph.packages });
|
|
11
|
+
return policy.flatMap((compiled) => compiled.evaluate(graph));
|
|
12
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { decodeWith } from "#decoded.ts";
|
|
3
|
+
import type { Fence, Selector } from "./model.ts";
|
|
4
|
+
|
|
5
|
+
const SelectorInput: Schema.Codec<Selector> = Schema.suspend(() =>
|
|
6
|
+
Schema.Union([
|
|
7
|
+
Schema.Struct({ kind: Schema.Literal("anything") }),
|
|
8
|
+
Schema.Struct({ kind: Schema.Literal("workspace") }),
|
|
9
|
+
Schema.Struct({ kind: Schema.Literal("anyOf"), members: Schema.Array(SelectorInput) }),
|
|
10
|
+
Schema.Struct({ base: SelectorInput, excluded: Schema.Array(SelectorInput), kind: Schema.Literal("except") }),
|
|
11
|
+
Schema.Struct({ kind: Schema.Literals(["files", "folders"]), paths: Schema.Array(Schema.String) }),
|
|
12
|
+
Schema.Struct({ kind: Schema.Literals(["modules", "packages", "scopes"]), names: Schema.Array(Schema.String) }),
|
|
13
|
+
]),
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const ExampleStep = Schema.Union([Schema.String, Schema.Struct({ external: Schema.String })]);
|
|
17
|
+
|
|
18
|
+
const Chain = Schema.TupleWithRest(Schema.Tuple([Schema.String, ExampleStep]), [ExampleStep]);
|
|
19
|
+
|
|
20
|
+
const FenceInput: Schema.Codec<Fence> = Schema.Struct({
|
|
21
|
+
_tag: Schema.Literal("Fence"),
|
|
22
|
+
examples: Schema.Struct({ illegal: Chain, legal: Chain }),
|
|
23
|
+
from: SelectorInput,
|
|
24
|
+
name: Schema.String,
|
|
25
|
+
prohibition: Schema.Union([
|
|
26
|
+
Schema.Struct({ kind: Schema.Literals(["import", "reach"]), to: SelectorInput }),
|
|
27
|
+
Schema.Struct({ kind: Schema.Literal("vocabulary"), subjects: Schema.Array(Schema.String), unit: SelectorInput }),
|
|
28
|
+
]),
|
|
29
|
+
rationale: Schema.String,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const Declarations = Schema.Array(FenceInput);
|
|
33
|
+
|
|
34
|
+
const standard = Schema.toStandardSchemaV1(Declarations, { parseOptions: { errors: "all", onExcessProperty: "error" } });
|
|
35
|
+
|
|
36
|
+
export function decodeDeclarations(value: unknown) {
|
|
37
|
+
return decodeWith(standard, value);
|
|
38
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { posix, resolve } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import type { WorkspacePackage } from "#imports/workspace.ts";
|
|
5
|
+
import type { RuleInputs } from "#rule.ts";
|
|
6
|
+
import { decodeDeclarations } from "./declarations.ts";
|
|
7
|
+
import type { Fence } from "./model.ts";
|
|
8
|
+
|
|
9
|
+
const BOUNDARY_FILE = "quality.fences.config.ts";
|
|
10
|
+
|
|
11
|
+
async function loadBoundary(inputs: RuleInputs, directory: string): Promise<readonly Fence[]> {
|
|
12
|
+
const file = posix.join(directory, BOUNDARY_FILE);
|
|
13
|
+
try {
|
|
14
|
+
const text = await inputs.readText(file);
|
|
15
|
+
if (text === undefined) {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
const url = pathToFileURL(resolve(inputs.root, file));
|
|
19
|
+
url.searchParams.set("content", createHash("sha256").update(text).digest("hex"));
|
|
20
|
+
const loaded: unknown = await import(url.href);
|
|
21
|
+
if (typeof loaded !== "object" || loaded === null || !("default" in loaded)) {
|
|
22
|
+
throw new Error("export an array of fence() declarations as the default export");
|
|
23
|
+
}
|
|
24
|
+
const decoded = await decodeDeclarations(loaded.default);
|
|
25
|
+
if (decoded._tag === "Invalid") {
|
|
26
|
+
throw new Error(`export an array of fence() declarations:\n${decoded.issues.join("\n")}`);
|
|
27
|
+
}
|
|
28
|
+
return decoded.value;
|
|
29
|
+
} catch (cause) {
|
|
30
|
+
throw new Error(`cannot load boundary at ${file}: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function discoverFences(inputs: RuleInputs, packages: readonly WorkspacePackage[]): Promise<readonly Fence[]> {
|
|
35
|
+
const boundaries = await Promise.all([".", ...packages.map((owner) => owner.directory)].map((directory) => loadBoundary(inputs, directory)));
|
|
36
|
+
return boundaries.flat();
|
|
37
|
+
}
|
package/src/imports/graph.ts
CHANGED
|
@@ -29,7 +29,7 @@ export interface ImportGraph {
|
|
|
29
29
|
const QUERY = /\?.*$/u;
|
|
30
30
|
|
|
31
31
|
const NO_MODULES =
|
|
32
|
-
"the import graph covers no modules: the sources hold no TypeScript or JavaScript module. Point `sources` at the
|
|
32
|
+
"the import graph covers no modules: the sources hold no TypeScript or JavaScript module. Point `sources` at the TypeScript or JavaScript code.";
|
|
33
33
|
|
|
34
34
|
function relativeTarget(from: string, request: ImportRequest): string | undefined {
|
|
35
35
|
const relative = request.kind === "path-reference" || request.specifier.startsWith("./") || request.specifier.startsWith("../");
|
|
@@ -77,7 +77,7 @@ async function build(inputs: RuleInputs): Promise<ImportGraph> {
|
|
|
77
77
|
|
|
78
78
|
const graphs = new WeakMap<readonly SourceFile[], Promise<ImportGraph>>();
|
|
79
79
|
|
|
80
|
-
//
|
|
80
|
+
// Import rules run on one scan, so they share the graph built from its sources.
|
|
81
81
|
export function importGraph(inputs: RuleInputs): Promise<ImportGraph> {
|
|
82
82
|
const known = graphs.get(inputs.sources) ?? build(inputs);
|
|
83
83
|
graphs.set(inputs.sources, known);
|
package/src/imports/projects.ts
CHANGED
|
@@ -73,8 +73,8 @@ function nearestConfig(root: string, file: string): string | undefined {
|
|
|
73
73
|
return undefined;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
function memo<
|
|
77
|
-
const known = new Map<string,
|
|
76
|
+
function memo<TValue>(compute: (key: string) => TValue): (key: string) => TValue {
|
|
77
|
+
const known = new Map<string, TValue>();
|
|
78
78
|
return (key) => {
|
|
79
79
|
const value = known.get(key) ?? compute(key);
|
|
80
80
|
known.set(key, value);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { statSync } from "node:fs";
|
|
2
|
+
import { join, posix } from "node:path";
|
|
3
|
+
import type { RuleInputs } from "#rule.ts";
|
|
4
|
+
import { packageOf, type WorkspacePackage } from "./workspace.ts";
|
|
5
|
+
|
|
6
|
+
export type Runtime = "client" | "server" | "shared";
|
|
7
|
+
|
|
8
|
+
function directoryExists(path: string): boolean {
|
|
9
|
+
try {
|
|
10
|
+
return statSync(path).isDirectory();
|
|
11
|
+
} catch (cause) {
|
|
12
|
+
if (cause instanceof Error && "code" in cause && (cause.code === "ENOENT" || cause.code === "ENOTDIR")) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
throw cause;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function runtimeOf(inputs: RuleInputs, packages: readonly WorkspacePackage[]): (path: string) => Runtime | undefined {
|
|
20
|
+
const owners = [...packages, { directory: ".", name: "." }];
|
|
21
|
+
const adopted = new Set(
|
|
22
|
+
owners
|
|
23
|
+
.filter((owner) => ["client", "server"].some((folder) => directoryExists(join(inputs.root, owner.directory, "src", folder))))
|
|
24
|
+
.map((owner) => owner.directory),
|
|
25
|
+
);
|
|
26
|
+
return (path) => {
|
|
27
|
+
const owner = packageOf(packages, path)?.directory ?? ".";
|
|
28
|
+
if (!adopted.has(owner)) {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
const local = posix.relative(owner, path);
|
|
32
|
+
if (local.startsWith("src/client/")) {
|
|
33
|
+
return "client";
|
|
34
|
+
}
|
|
35
|
+
return local.startsWith("src/server/") ? "server" : "shared";
|
|
36
|
+
};
|
|
37
|
+
}
|
package/src/imports/workspace.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { posix } from "node:path";
|
|
2
|
+
import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem";
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
import { manifestsIn } from "#manifests/sorted.ts";
|
|
2
5
|
import type { RuleInputs } from "#rule.ts";
|
|
3
6
|
import { globMatcher } from "./glob.ts";
|
|
4
7
|
import { pnpmWorkspacePatterns } from "./pnpm-workspace.ts";
|
|
@@ -8,7 +11,7 @@ export interface WorkspacePackage {
|
|
|
8
11
|
readonly name: string;
|
|
9
12
|
}
|
|
10
13
|
|
|
11
|
-
type Reader = Pick<RuleInputs, "files" | "readText"
|
|
14
|
+
type Reader = Pick<RuleInputs, "files" | "readText"> & Partial<Pick<RuleInputs, "root">>;
|
|
12
15
|
|
|
13
16
|
const MANIFEST = "package.json";
|
|
14
17
|
|
|
@@ -60,7 +63,9 @@ async function memberOf(inputs: Reader): Promise<(directory: string) => boolean>
|
|
|
60
63
|
|
|
61
64
|
export async function workspacePackages(inputs: Reader): Promise<readonly WorkspacePackage[]> {
|
|
62
65
|
const member = await memberOf(inputs);
|
|
63
|
-
const
|
|
66
|
+
const manifests =
|
|
67
|
+
inputs.root === undefined ? inputs.files : await Effect.runPromise(manifestsIn(inputs.root).pipe(Effect.provide(NodeFileSystem.layer)));
|
|
68
|
+
const directories = manifests
|
|
64
69
|
.filter((file) => posix.basename(file) === MANIFEST && file !== MANIFEST)
|
|
65
70
|
.map((manifest) => posix.dirname(manifest))
|
|
66
71
|
.filter(member);
|
package/src/inventory/collect.ts
CHANGED
|
@@ -46,7 +46,14 @@ export const collectInventory = (root: string, scope: InventoryScope): Effect.Ef
|
|
|
46
46
|
const excluded = ignore().add([...scope.exclude]);
|
|
47
47
|
const zones = yield* Effect.forEach(scope.sources, (zone) => zoneFiles(root, zone), { concurrency: READ_CONCURRENCY });
|
|
48
48
|
const relativePaths = zones.flat().map((absolute) => posix(relative(root, absolute)));
|
|
49
|
-
const files = [...new Set(relativePaths)]
|
|
49
|
+
const files = [...new Set(relativePaths)]
|
|
50
|
+
.filter((path) => !excluded.ignores(path))
|
|
51
|
+
.sort((left, right) => {
|
|
52
|
+
if (left === right) {
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
return left < right ? -1 : 1;
|
|
56
|
+
});
|
|
50
57
|
const sourcePaths = files.filter((path) => scope.extensions.some((extension) => path.endsWith(extension)));
|
|
51
58
|
const sources = yield* Effect.forEach(sourcePaths, (path) => readSource(root, path), { concurrency: READ_CONCURRENCY });
|
|
52
59
|
return { files, sources };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
|
|
5
|
+
const run = promisify(execFile);
|
|
6
|
+
|
|
7
|
+
async function listedPaths(directory: string, options: readonly string[]): Promise<readonly string[]> {
|
|
8
|
+
try {
|
|
9
|
+
const { stdout } = await run("git", ["ls-files", "-z", ...options, "--", "."], { cwd: directory, maxBuffer: 2 ** 30 });
|
|
10
|
+
return stdout
|
|
11
|
+
.split("\0")
|
|
12
|
+
.filter(Boolean)
|
|
13
|
+
.map((path) => join(directory, path));
|
|
14
|
+
} catch (error) {
|
|
15
|
+
if (error instanceof Error && "stderr" in error && String(error.stderr).includes("not a git repository")) {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function trackedPaths(directory: string): Promise<readonly string[]> {
|
|
23
|
+
return listedPaths(directory, []);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function ignoredTrackedPaths(directory: string): Promise<readonly string[]> {
|
|
27
|
+
return listedPaths(directory, ["--cached", "--ignored", "--exclude-standard"]);
|
|
28
|
+
}
|