@c9up/helix 0.1.4 → 0.1.6

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.
Files changed (65) hide show
  1. package/dist/cli/coverage/diff/overlay.d.ts.map +1 -1
  2. package/dist/cli/coverage/diff/overlay.js +19 -6
  3. package/dist/cli/coverage/diff/overlay.js.map +1 -1
  4. package/dist/cli/pool.js +10 -0
  5. package/dist/cli/pool.js.map +1 -1
  6. package/dist/runtime/equals.d.ts.map +1 -1
  7. package/dist/runtime/equals.js +6 -2
  8. package/dist/runtime/equals.js.map +1 -1
  9. package/dist/runtime/suite.d.ts +26 -0
  10. package/dist/runtime/suite.d.ts.map +1 -1
  11. package/dist/runtime/suite.js +21 -18
  12. package/dist/runtime/suite.js.map +1 -1
  13. package/index.darwin-arm64.node +0 -0
  14. package/index.darwin-x64.node +0 -0
  15. package/index.linux-arm64-gnu.node +0 -0
  16. package/index.linux-x64-gnu.node +0 -0
  17. package/index.win32-x64-msvc.node +0 -0
  18. package/package.json +2 -2
  19. package/src/cli/coverage/aggregate.ts +0 -231
  20. package/src/cli/coverage/collect.ts +0 -63
  21. package/src/cli/coverage/diff/base.ts +0 -46
  22. package/src/cli/coverage/diff/index.ts +0 -160
  23. package/src/cli/coverage/diff/overlay.ts +0 -62
  24. package/src/cli/coverage/diff/parse.ts +0 -121
  25. package/src/cli/coverage/diff/reporters.ts +0 -82
  26. package/src/cli/coverage/diff/types.ts +0 -46
  27. package/src/cli/coverage/filter.ts +0 -71
  28. package/src/cli/coverage/glob.ts +0 -0
  29. package/src/cli/coverage/index.ts +0 -126
  30. package/src/cli/coverage/reporters/json.ts +0 -40
  31. package/src/cli/coverage/reporters/lcov.ts +0 -54
  32. package/src/cli/coverage/reporters/text.ts +0 -48
  33. package/src/cli/coverage/thresholds.ts +0 -73
  34. package/src/cli/coverage/types.ts +0 -93
  35. package/src/cli/discover.ts +0 -174
  36. package/src/cli/native.ts +0 -104
  37. package/src/cli/pool.ts +0 -486
  38. package/src/cli/reporter.ts +0 -155
  39. package/src/cli/run.ts +0 -440
  40. package/src/cli/summary.ts +0 -42
  41. package/src/cli/watch/loop.ts +0 -159
  42. package/src/cli/watch/types.ts +0 -22
  43. package/src/cli/watch/watcher.ts +0 -145
  44. package/src/container/index.ts +0 -16
  45. package/src/container/override.ts +0 -86
  46. package/src/container/spy.ts +0 -25
  47. package/src/index.ts +0 -42
  48. package/src/runtime/assertion-error.ts +0 -38
  49. package/src/runtime/cli-worker.ts +0 -140
  50. package/src/runtime/equals.ts +0 -400
  51. package/src/runtime/expect.ts +0 -173
  52. package/src/runtime/index.ts +0 -50
  53. package/src/runtime/lifecycle.ts +0 -17
  54. package/src/runtime/matchers.ts +0 -452
  55. package/src/runtime/run.ts +0 -573
  56. package/src/runtime/suite.ts +0 -310
  57. package/src/runtime/test-context.ts +0 -59
  58. package/src/runtime/vi/fake-timers.ts +0 -410
  59. package/src/runtime/vi/index.ts +0 -254
  60. package/src/runtime/vi/spy.ts +0 -224
  61. package/src/runtime/vi/spyOn.ts +0 -155
  62. package/src/runtime/vi/system-time.ts +0 -121
  63. package/src/runtime/worker.ts +0 -239
  64. package/src/time/freeze.ts +0 -229
  65. package/src/time/index.ts +0 -16
@@ -1,126 +0,0 @@
1
- /**
2
- * Coverage facade — wires collect → filter → aggregate → reporters →
3
- * thresholds. Called by `run.ts` after the worker pool finishes.
4
- */
5
-
6
- import { mkdir, rm, writeFile } from "node:fs/promises";
7
- import path from "node:path";
8
- import { aggregate } from "./aggregate.js";
9
- import { collect } from "./collect.js";
10
- import { filter } from "./filter.js";
11
- import { jsonSummary } from "./reporters/json.js";
12
- import { lcov } from "./reporters/lcov.js";
13
- import { textSummary } from "./reporters/text.js";
14
- import { enforce } from "./thresholds.js";
15
- import type {
16
- CoverageOptions,
17
- CoverageSummary,
18
- ThresholdViolation,
19
- } from "./types.js";
20
-
21
- export { textSummary } from "./reporters/text.js";
22
- export { enforce, violationSummary } from "./thresholds.js";
23
- export type {
24
- CoverageOptions,
25
- CoverageSummary,
26
- ThresholdViolation,
27
- } from "./types.js";
28
-
29
- export interface CoverageSession {
30
- /** Temp dir passed as `NODE_V8_COVERAGE` to every worker. */
31
- envDir: string;
32
- /** Inject into the child environment. */
33
- env: NodeJS.ProcessEnv;
34
- }
35
-
36
- /**
37
- * Create a unique temp directory for a run. The orchestrator passes its
38
- * path via `NODE_V8_COVERAGE`; every spawned worker writes its own
39
- * `coverage-*.json` on exit. Returns an env bag the pool forwards.
40
- */
41
- export async function openSession(baseDir?: string): Promise<CoverageSession> {
42
- const root = baseDir ?? path.join(process.cwd(), ".helix-coverage");
43
- const unique = `run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
44
- const envDir = path.join(root, unique);
45
- await mkdir(envDir, { recursive: true });
46
- return {
47
- envDir,
48
- env: {
49
- NODE_V8_COVERAGE: envDir,
50
- },
51
- };
52
- }
53
-
54
- export interface FinaliseOptions extends CoverageOptions {
55
- root: string;
56
- session: CoverageSession;
57
- }
58
-
59
- export interface FinaliseResult {
60
- summary: CoverageSummary;
61
- violations: ThresholdViolation[];
62
- textReport: string;
63
- /** Absolute paths of written report files (lcov + json-summary). */
64
- reportFiles: string[];
65
- }
66
-
67
- const KNOWN_REPORTERS = new Set(["text-summary", "lcov", "json-summary"]);
68
-
69
- /**
70
- * After all workers have exited, read the raw v8 output, aggregate, emit
71
- * reporters + enforce thresholds.
72
- *
73
- * The temp dir is removed in `finally` so a write/aggregate failure
74
- * doesn't leak gigabytes of raw V8 JSON under `.helix-coverage/`.
75
- */
76
- export async function finalise(opts: FinaliseOptions): Promise<FinaliseResult> {
77
- try {
78
- const raw = await collect(opts.session.envDir);
79
- const filtered = filter(raw, {
80
- root: opts.root,
81
- include: opts.include,
82
- exclude: opts.exclude,
83
- });
84
- const summary = aggregate(filtered);
85
-
86
- // Treat empty array as "use defaults" — same as undefined.
87
- const requested =
88
- opts.reporters && opts.reporters.length > 0
89
- ? opts.reporters
90
- : ["text-summary", "lcov"];
91
- // Warn on unknown reporter names so typos surface (instead of the
92
- // reporter silently being skipped).
93
- for (const r of requested) {
94
- if (!KNOWN_REPORTERS.has(r)) {
95
- process.stderr.write(
96
- `helix-coverage: unknown reporter "${r}" — known: ${[...KNOWN_REPORTERS].join(", ")}\n`,
97
- );
98
- }
99
- }
100
- const outputDir = opts.outputDir ?? path.join(opts.root, "coverage");
101
- await mkdir(outputDir, { recursive: true });
102
-
103
- const reportFiles: string[] = [];
104
- if (requested.includes("lcov")) {
105
- const file = path.join(outputDir, "lcov.info");
106
- await writeFile(file, lcov(summary, opts.root), "utf8");
107
- reportFiles.push(file);
108
- }
109
- if (requested.includes("json-summary")) {
110
- const file = path.join(outputDir, "coverage-summary.json");
111
- await writeFile(file, jsonSummary(summary, opts.root), "utf8");
112
- reportFiles.push(file);
113
- }
114
-
115
- const textReport = requested.includes("text-summary")
116
- ? textSummary(summary, opts.root)
117
- : "";
118
-
119
- const violations = opts.thresholds ? enforce(summary, opts.thresholds) : [];
120
-
121
- return { summary, violations, textReport, reportFiles };
122
- } finally {
123
- // Always tidy the temp dir — even on aggregate / writeFile failure.
124
- await rm(opts.session.envDir, { recursive: true, force: true });
125
- }
126
- }
@@ -1,40 +0,0 @@
1
- /**
2
- * `coverage-summary.json` — Istanbul-compatible shape consumed by CI
3
- * tooling (lcov-reporter-action, Codecov uploaders that prefer JSON).
4
- *
5
- * File keys are emitted as paths RELATIVE to `root` so the artifact is
6
- * portable across machines. The reserved `total` key holds the
7
- * aggregate; if a real file path collides with it (extraordinarily
8
- * unlikely after relativisation), we keep the totals row and skip the
9
- * collision rather than overwrite — protecting the consumer's invariant.
10
- */
11
-
12
- import path from "node:path";
13
- import type { CoverageSummary } from "../types.js";
14
-
15
- function rel(file: string, root: string): string {
16
- const r = path.relative(root, file).split(path.sep).join("/");
17
- return r.length > 0 && !r.startsWith("..") ? r : file;
18
- }
19
-
20
- export function jsonSummary(summary: CoverageSummary, root: string): string {
21
- // Use a fresh prototype-less object so a pathological file path of
22
- // "__proto__" can't pollute the global Object prototype.
23
- const out: Record<string, unknown> = Object.create(null);
24
- out.total = summary.total;
25
- for (const f of summary.files) {
26
- const key = rel(f.file, root);
27
- if (key === "total") {
28
- // Astronomical edge: a file literally named "total" at the root.
29
- // Prefer correctness of the totals row over reporting that file.
30
- continue;
31
- }
32
- out[key] = {
33
- lines: f.lines,
34
- statements: f.statements,
35
- functions: f.functions,
36
- branches: f.branches,
37
- };
38
- }
39
- return `${JSON.stringify(out, null, 2)}\n`;
40
- }
@@ -1,54 +0,0 @@
1
- /**
2
- * LCOV reporter — writes standard `lcov.info` that Codecov, Coveralls,
3
- * and IDE coverage gutters consume directly.
4
- *
5
- * Format reference: `man geninfo` /
6
- * http://ltp.sourceforge.net/coverage/lcov/geninfo.1.php
7
- *
8
- * Paths in `SF:` are emitted RELATIVE TO `root` so the same artifact
9
- * lines up across machines (developer laptop, CI runner, Codecov server).
10
- */
11
-
12
- import path from "node:path";
13
- import type { CoverageSummary } from "../types.js";
14
-
15
- function rel(file: string, root: string): string {
16
- const r = path.relative(root, file).split(path.sep).join("/");
17
- return r.length > 0 && !r.startsWith("..") ? r : file;
18
- }
19
-
20
- export function lcov(summary: CoverageSummary, root: string): string {
21
- const out: string[] = [];
22
- for (const f of summary.files) {
23
- out.push(`TN:`);
24
- out.push(`SF:${rel(f.file, root)}`);
25
-
26
- let fnf = 0;
27
- let fnh = 0;
28
- for (const fn of f.functionHits) {
29
- // Skip records lcov rejects (line < 1).
30
- if (fn.line < 1) continue;
31
- out.push(`FN:${fn.line},${fn.name}`);
32
- out.push(`FNDA:${fn.count},${fn.name}`);
33
- fnf += 1;
34
- if (fn.count > 0) fnh += 1;
35
- }
36
- out.push(`FNF:${fnf}`);
37
- out.push(`FNH:${fnh}`);
38
-
39
- let lf = 0;
40
- let lh = 0;
41
- for (const l of f.lineHits) {
42
- out.push(`DA:${l.line},${l.count}`);
43
- lf += 1;
44
- if (l.count > 0) lh += 1;
45
- }
46
- out.push(`LF:${lf}`);
47
- out.push(`LH:${lh}`);
48
-
49
- out.push(`BRF:0`);
50
- out.push(`BRH:0`);
51
- out.push(`end_of_record`);
52
- }
53
- return `${out.join("\n")}\n`;
54
- }
@@ -1,48 +0,0 @@
1
- /**
2
- * Text summary reporter — one line per file + totals, written to stdout.
3
- * Deliberately terse (mirrors c8's default output) so it fits alongside
4
- * the test reporter's summary without overwhelming.
5
- */
6
-
7
- import type { CoverageSummary } from "../types.js";
8
-
9
- function fmtPct(n: number): string {
10
- return `${n.toFixed(2).padStart(6)}%`;
11
- }
12
-
13
- function padRight(s: string, w: number): string {
14
- if (s.length === w) return s;
15
- if (s.length > w) {
16
- // Ellipsize from the LEFT — preserves the filename tail which is
17
- // usually more informative than the leading directory.
18
- return `…${s.slice(s.length - w + 1)}`;
19
- }
20
- return s + " ".repeat(w - s.length);
21
- }
22
-
23
- export function textSummary(summary: CoverageSummary, root: string): string {
24
- const lines: string[] = [];
25
- lines.push("");
26
- lines.push("── Coverage ─────────────────────────────────────────────────");
27
- lines.push(
28
- `${padRight("File", 50)} ${"Lines".padStart(8)} ${"Funcs".padStart(8)}`,
29
- );
30
- lines.push("─".repeat(70));
31
- for (const f of summary.files) {
32
- const rel = f.file.startsWith(root)
33
- ? f.file.slice(root.length + 1)
34
- : f.file;
35
- const linesPct =
36
- f.lines.total > 0 ? (f.lines.covered / f.lines.total) * 100 : 100;
37
- const fnPct =
38
- f.functions.total > 0
39
- ? (f.functions.covered / f.functions.total) * 100
40
- : 100;
41
- lines.push(`${padRight(rel, 50)} ${fmtPct(linesPct)} ${fmtPct(fnPct)}`);
42
- }
43
- lines.push("─".repeat(70));
44
- lines.push(
45
- `${padRight("All files", 50)} ${fmtPct(summary.total.lines.pct)} ${fmtPct(summary.total.functions.pct)}`,
46
- );
47
- return `${lines.join("\n")}\n`;
48
- }
@@ -1,73 +0,0 @@
1
- /**
2
- * Threshold enforcement — compare each configured metric (`lines` /
3
- * `functions` / `statements` / `branches`) against the aggregate totals.
4
- * Returns the list of violations; caller decides whether to set
5
- * `exitCode = 1` (today: yes, always).
6
- */
7
-
8
- import type {
9
- CoverageSummary,
10
- Thresholds,
11
- ThresholdViolation,
12
- } from "./types.js";
13
-
14
- const METRICS: Array<keyof Thresholds> = [
15
- "lines",
16
- "functions",
17
- "statements",
18
- "branches",
19
- ];
20
-
21
- /**
22
- * Validate user-supplied thresholds. Throws on NaN / negative / >100 so
23
- * config typos fail loudly. Empty / undefined values are accepted (no
24
- * gate on that metric).
25
- */
26
- function validate(thresholds: Thresholds): void {
27
- for (const metric of METRICS) {
28
- const v = thresholds[metric];
29
- if (v === undefined) continue;
30
- if (typeof v !== "number" || !Number.isFinite(v)) {
31
- throw new Error(
32
- `coverage threshold ${metric}: expected a finite number, got ${v}`,
33
- );
34
- }
35
- if (v < 0 || v > 100) {
36
- throw new Error(`coverage threshold ${metric}: expected 0–100, got ${v}`);
37
- }
38
- }
39
- }
40
-
41
- export function enforce(
42
- summary: CoverageSummary,
43
- thresholds: Thresholds,
44
- ): ThresholdViolation[] {
45
- validate(thresholds);
46
- const violations: ThresholdViolation[] = [];
47
- for (const metric of METRICS) {
48
- const threshold = thresholds[metric];
49
- if (threshold === undefined) continue;
50
- // Compute the actual ratio fresh from totals (without the rounded
51
- // `pct`). A threshold of 100 then catches 99.999% correctly instead
52
- // of being defeated by `pct`'s 2-decimal rounding.
53
- const tot = summary.total[metric];
54
- const actual = tot.total === 0 ? 100 : (tot.covered / tot.total) * 100;
55
- if (actual < threshold) {
56
- violations.push({ metric, actual, threshold });
57
- }
58
- }
59
- return violations;
60
- }
61
-
62
- /**
63
- * Spec format (AC #5): `coverage: lines 84.2 < threshold 88` — one
64
- * decimal place, no percent signs.
65
- */
66
- export function violationSummary(violations: ThresholdViolation[]): string {
67
- return violations
68
- .map(
69
- (v) =>
70
- `coverage: ${v.metric} ${v.actual.toFixed(1)} < threshold ${v.threshold}`,
71
- )
72
- .join("\n");
73
- }
@@ -1,93 +0,0 @@
1
- /**
2
- * Shared types for the coverage pipeline.
3
- *
4
- * Stages: raw V8 JSON → `RawFileCoverage` (per-file after filter) →
5
- * `AggregateCoverage` (merged across workers) → reporters / thresholds.
6
- */
7
-
8
- /** V8 function-range entry as emitted in `coverage-*.json` files. */
9
- export interface V8Range {
10
- startOffset: number;
11
- endOffset: number;
12
- count: number;
13
- }
14
-
15
- export interface V8Function {
16
- functionName: string;
17
- ranges: V8Range[];
18
- isBlockCoverage: boolean;
19
- }
20
-
21
- export interface V8Script {
22
- scriptId: string;
23
- url: string;
24
- functions: V8Function[];
25
- }
26
-
27
- export interface V8CoverageFile {
28
- result: V8Script[];
29
- }
30
-
31
- /**
32
- * Normalised per-file coverage — the unit passed between the collector,
33
- * filter, aggregator, reporter and threshold stages.
34
- */
35
- export interface RawFileCoverage {
36
- /** Absolute file path (decoded from file:// URL). */
37
- file: string;
38
- /** Source text at the time V8 recorded coverage — needed to compute line
39
- * offsets. We fall back to reading from disk if absent. */
40
- source: string;
41
- functions: V8Function[];
42
- }
43
-
44
- export interface FileSummary {
45
- file: string;
46
- lines: { covered: number; total: number };
47
- functions: { covered: number; total: number };
48
- statements: { covered: number; total: number };
49
- branches: { covered: number; total: number };
50
- /** Line numbers (1-based) with hit counts — for lcov DA entries. */
51
- lineHits: Array<{ line: number; count: number }>;
52
- /** Functions, with `(name, line, count)` — for lcov FN/FNDA entries. */
53
- functionHits: Array<{ name: string; line: number; count: number }>;
54
- }
55
-
56
- export interface Totals {
57
- lines: { covered: number; total: number; pct: number };
58
- functions: { covered: number; total: number; pct: number };
59
- statements: { covered: number; total: number; pct: number };
60
- branches: { covered: number; total: number; pct: number };
61
- }
62
-
63
- export interface CoverageSummary {
64
- files: FileSummary[];
65
- total: Totals;
66
- }
67
-
68
- export interface Thresholds {
69
- lines?: number;
70
- functions?: number;
71
- statements?: number;
72
- branches?: number;
73
- }
74
-
75
- export interface ThresholdViolation {
76
- metric: keyof Thresholds;
77
- actual: number;
78
- threshold: number;
79
- }
80
-
81
- export interface CoverageOptions {
82
- enabled: boolean;
83
- include?: string[];
84
- exclude?: string[];
85
- reporters?: string[];
86
- outputDir?: string;
87
- thresholds?: Thresholds;
88
- /**
89
- * Project root coverage paths are relative to (typically the package
90
- * directory containing `package.json`). Defaults to `RunConfig.root`.
91
- */
92
- root?: string;
93
- }
@@ -1,174 +0,0 @@
1
- /**
2
- * File-system discovery of test files.
3
- *
4
- * Design goals:
5
- * - Walks a root directory
6
- * - Honours `.gitignore` (basic pattern subset: basename match, leading-`/`
7
- * root anchor, directory-trailing `/`, simple `*` glob, path-scoped
8
- * `a/*.ts` patterns)
9
- * - Uses `lstat` so symlinks are NOT followed (cycle-safe, matches Rust's
10
- * `ignore::WalkBuilder::follow_links(false)`)
11
- * - Tracks visited absolute paths so any escape via junctions is capped
12
- * - Emits a warning on permission-denied subtrees (so silent tests-gone-missing
13
- * is visible) while still returning the discoverable set
14
- */
15
-
16
- import { existsSync, readFileSync } from "node:fs";
17
- import { lstat, readdir } from "node:fs/promises";
18
- import path from "node:path";
19
-
20
- export interface DiscoveryOptions {
21
- /** Filename suffixes that mark a test file (e.g. `.test.ts`). */
22
- suffixes?: string[];
23
- /** Directory basenames pruned from the walk. */
24
- hardExcludes?: string[];
25
- /** Read `.gitignore` at `root` + every descendant and apply rules. */
26
- honourGitignore?: boolean;
27
- /** Called with a human-readable message when a directory is skipped
28
- * because of an IO error (ENOENT, EACCES). Default: `console.warn`. */
29
- onWarn?: (message: string) => void;
30
- }
31
-
32
- const DEFAULT_SUFFIXES = [
33
- ".test.ts",
34
- ".test.tsx",
35
- ".test.js",
36
- ".test.mjs",
37
- ".test.cjs",
38
- ".spec.ts",
39
- ".spec.tsx",
40
- ".spec.js",
41
- ".spec.mjs",
42
- ".spec.cjs",
43
- ];
44
-
45
- const DEFAULT_HARD_EXCLUDES = [
46
- "node_modules",
47
- "dist",
48
- "build",
49
- "coverage",
50
- ".git",
51
- ".wolf",
52
- "target",
53
- ".next",
54
- ];
55
-
56
- interface GitignorePattern {
57
- raw: string;
58
- /** If true, pattern is anchored to the directory that defined it. */
59
- anchored: boolean;
60
- /** If true, pattern only matches directories (trailing `/`). */
61
- dirOnly: boolean;
62
- /** Regex compiled from the literal pattern, matched against a relative path. */
63
- regex: RegExp;
64
- }
65
-
66
- function compilePattern(line: string): GitignorePattern | undefined {
67
- let p = line.trim();
68
- if (!p || p.startsWith("#")) return undefined;
69
- const anchored = p.startsWith("/");
70
- if (anchored) p = p.slice(1);
71
- const dirOnly = p.endsWith("/");
72
- if (dirOnly) p = p.slice(0, -1);
73
- if (!p) return undefined;
74
- // Translate a tiny glob subset to regex:
75
- // `*` → `[^/]*`
76
- // `**` → `.*`
77
- const escaped = p
78
- .replace(/[.+^${}()|[\]\\]/g, "\\$&")
79
- .replace(/\*\*/g, "__DOUBLESTAR__")
80
- .replace(/\*/g, "[^/]*")
81
- .replace(/__DOUBLESTAR__/g, ".*");
82
- const regex = anchored
83
- ? new RegExp(`^${escaped}(?:/.*)?$`)
84
- : new RegExp(`(?:^|/)${escaped}(?:/.*)?$`);
85
- return { raw: line, anchored, dirOnly, regex };
86
- }
87
-
88
- function readGitignore(dir: string): GitignorePattern[] {
89
- const file = path.join(dir, ".gitignore");
90
- if (!existsSync(file)) return [];
91
- try {
92
- return readFileSync(file, "utf8")
93
- .split("\n")
94
- .map(compilePattern)
95
- .filter((p): p is GitignorePattern => p !== undefined);
96
- } catch {
97
- return [];
98
- }
99
- }
100
-
101
- function matches(
102
- pattern: GitignorePattern,
103
- relPath: string,
104
- isDir: boolean,
105
- ): boolean {
106
- if (pattern.dirOnly && !isDir) return false;
107
- return pattern.regex.test(relPath);
108
- }
109
-
110
- export async function discover(
111
- root: string,
112
- options: DiscoveryOptions = {},
113
- ): Promise<string[]> {
114
- const suffixes = options.suffixes ?? DEFAULT_SUFFIXES;
115
- const hardExcludes = new Set(options.hardExcludes ?? DEFAULT_HARD_EXCLUDES);
116
- const honourGitignore = options.honourGitignore ?? true;
117
- const warn = options.onWarn ?? ((m) => process.stderr.write(`helix: ${m}\n`));
118
-
119
- const results: string[] = [];
120
- const absRoot = path.isAbsolute(root) ? root : path.resolve(root);
121
- const visited = new Set<string>();
122
-
123
- async function walk(
124
- dir: string,
125
- relativeToRoot: string,
126
- inherited: GitignorePattern[],
127
- ): Promise<void> {
128
- const realDir = path.resolve(dir);
129
- if (visited.has(realDir)) return;
130
- visited.add(realDir);
131
-
132
- let entries: string[];
133
- try {
134
- entries = await readdir(dir);
135
- } catch (err) {
136
- warn(`skipping ${dir}: ${(err as NodeJS.ErrnoException).code ?? err}`);
137
- return;
138
- }
139
- const local = honourGitignore
140
- ? [...inherited, ...readGitignore(dir)]
141
- : inherited;
142
-
143
- for (const name of entries) {
144
- if (hardExcludes.has(name)) continue;
145
- const relPath = relativeToRoot ? `${relativeToRoot}/${name}` : name;
146
- const fullPath = path.join(dir, name);
147
- let st: Awaited<ReturnType<typeof lstat>>;
148
- try {
149
- st = await lstat(fullPath);
150
- } catch (err) {
151
- warn(
152
- `skipping ${fullPath}: ${(err as NodeJS.ErrnoException).code ?? err}`,
153
- );
154
- continue;
155
- }
156
- // Symlinks are NOT followed. Users who need symlink-following can
157
- // walk the target manually; matches Rust's `follow_links(false)`.
158
- if (st.isSymbolicLink()) continue;
159
- if (local.some((p) => matches(p, relPath, st.isDirectory()))) continue;
160
- if (st.isDirectory()) {
161
- await walk(fullPath, relPath, local);
162
- continue;
163
- }
164
- if (!st.isFile()) continue;
165
- if (suffixes.some((s) => name.endsWith(s))) {
166
- results.push(fullPath);
167
- }
168
- }
169
- }
170
-
171
- await walk(absRoot, "", []);
172
- results.sort();
173
- return results;
174
- }