@c9up/helix 0.1.4 → 0.1.5
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/dist/runtime/suite.d.ts +26 -0
- package/dist/runtime/suite.d.ts.map +1 -1
- package/dist/runtime/suite.js +21 -18
- package/dist/runtime/suite.js.map +1 -1
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +2 -2
- package/src/cli/coverage/aggregate.ts +0 -231
- package/src/cli/coverage/collect.ts +0 -63
- package/src/cli/coverage/diff/base.ts +0 -46
- package/src/cli/coverage/diff/index.ts +0 -160
- package/src/cli/coverage/diff/overlay.ts +0 -62
- package/src/cli/coverage/diff/parse.ts +0 -121
- package/src/cli/coverage/diff/reporters.ts +0 -82
- package/src/cli/coverage/diff/types.ts +0 -46
- package/src/cli/coverage/filter.ts +0 -71
- package/src/cli/coverage/glob.ts +0 -0
- package/src/cli/coverage/index.ts +0 -126
- package/src/cli/coverage/reporters/json.ts +0 -40
- package/src/cli/coverage/reporters/lcov.ts +0 -54
- package/src/cli/coverage/reporters/text.ts +0 -48
- package/src/cli/coverage/thresholds.ts +0 -73
- package/src/cli/coverage/types.ts +0 -93
- package/src/cli/discover.ts +0 -174
- package/src/cli/native.ts +0 -104
- package/src/cli/pool.ts +0 -486
- package/src/cli/reporter.ts +0 -155
- package/src/cli/run.ts +0 -440
- package/src/cli/summary.ts +0 -42
- package/src/cli/watch/loop.ts +0 -159
- package/src/cli/watch/types.ts +0 -22
- package/src/cli/watch/watcher.ts +0 -145
- package/src/container/index.ts +0 -16
- package/src/container/override.ts +0 -86
- package/src/container/spy.ts +0 -25
- package/src/index.ts +0 -42
- package/src/runtime/assertion-error.ts +0 -38
- package/src/runtime/cli-worker.ts +0 -140
- package/src/runtime/equals.ts +0 -400
- package/src/runtime/expect.ts +0 -173
- package/src/runtime/index.ts +0 -50
- package/src/runtime/lifecycle.ts +0 -17
- package/src/runtime/matchers.ts +0 -452
- package/src/runtime/run.ts +0 -573
- package/src/runtime/suite.ts +0 -310
- package/src/runtime/test-context.ts +0 -59
- package/src/runtime/vi/fake-timers.ts +0 -410
- package/src/runtime/vi/index.ts +0 -254
- package/src/runtime/vi/spy.ts +0 -224
- package/src/runtime/vi/spyOn.ts +0 -155
- package/src/runtime/vi/system-time.ts +0 -121
- package/src/runtime/worker.ts +0 -239
- package/src/time/freeze.ts +0 -229
- package/src/time/index.ts +0 -16
|
@@ -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
|
-
}
|
package/src/cli/discover.ts
DELETED
|
@@ -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
|
-
}
|
package/src/cli/native.ts
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Loads the native `ream-test-napi` binary built by `scripts/copy-napi.mjs`
|
|
3
|
-
* and exposes the Rust orchestrator `run(config)` to the TS CLI.
|
|
4
|
-
*
|
|
5
|
-
* Per the orchestrator design (42-N-orchestrator), the Rust NAPI engine is the
|
|
6
|
-
* canonical discovery + worker-pool + reporter + summary path; the TS `runOnce`
|
|
7
|
-
* delegates to it whenever no TS-only layer (coverage / diff-cov / watch / a
|
|
8
|
-
* pluggable reporter instance) is in play. There is NO JS fallback for a failed
|
|
9
|
-
* load — the caller gets a typed error pointing at `build:napi`.
|
|
10
|
-
*
|
|
11
|
-
* Field names are camelCase: napi-rs converts the Rust struct's snake_case
|
|
12
|
-
* fields automatically (`timeout_ms` → `timeoutMs`, etc.).
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { createRequire } from "node:module";
|
|
16
|
-
import { arch, platform } from "node:process";
|
|
17
|
-
import { fileURLToPath } from "node:url";
|
|
18
|
-
|
|
19
|
-
const SUFFIX_MAP: Readonly<Record<string, string>> = {
|
|
20
|
-
"linux-x64": "linux-x64-gnu",
|
|
21
|
-
"linux-arm64": "linux-arm64-gnu",
|
|
22
|
-
"darwin-x64": "darwin-x64",
|
|
23
|
-
"darwin-arm64": "darwin-arm64",
|
|
24
|
-
"win32-x64": "win32-x64-msvc",
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
function platformSuffix(): string {
|
|
28
|
-
const key = `${platform}-${arch}`;
|
|
29
|
-
const suffix = SUFFIX_MAP[key];
|
|
30
|
-
if (typeof suffix !== "string") {
|
|
31
|
-
throw new Error(
|
|
32
|
-
`Unsupported platform/arch '${key}' for @c9up/helix native binary. Supported: ${Object.keys(SUFFIX_MAP).join(", ")}.`,
|
|
33
|
-
);
|
|
34
|
-
}
|
|
35
|
-
return suffix;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Mirror of the Rust `RunConfig` (camelCase). */
|
|
39
|
-
export interface NativeRunConfig {
|
|
40
|
-
readonly root: string;
|
|
41
|
-
readonly files?: readonly string[];
|
|
42
|
-
readonly threads?: number;
|
|
43
|
-
readonly timeoutMs?: number;
|
|
44
|
-
readonly reporter?: string;
|
|
45
|
-
readonly workerEntry: string;
|
|
46
|
-
readonly nodeBin?: string;
|
|
47
|
-
readonly nodeArgs?: readonly string[];
|
|
48
|
-
readonly useColors?: boolean;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Mirror of the Rust `SummaryPayload` (camelCase). */
|
|
52
|
-
export interface NativeSummaryPayload {
|
|
53
|
-
readonly pass: number;
|
|
54
|
-
readonly fail: number;
|
|
55
|
-
readonly skip: number;
|
|
56
|
-
readonly todo: number;
|
|
57
|
-
readonly fileErrors: number;
|
|
58
|
-
readonly durationMs: number;
|
|
59
|
-
readonly exitCode: number;
|
|
60
|
-
/** Full `Summary` serialized as JSON (same shape as TS `Summary`). */
|
|
61
|
-
readonly json: string;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
interface NativeExports {
|
|
65
|
-
readonly run: (config: NativeRunConfig) => Promise<NativeSummaryPayload>;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function isNativeExports(value: unknown): value is NativeExports {
|
|
69
|
-
if (value === null || typeof value !== "object") return false;
|
|
70
|
-
return typeof Reflect.get(value, "run") === "function";
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
let cachedNative: NativeExports | undefined;
|
|
74
|
-
|
|
75
|
-
export function getNative(): NativeExports {
|
|
76
|
-
if (cachedNative !== undefined) return cachedNative;
|
|
77
|
-
|
|
78
|
-
const require = createRequire(import.meta.url);
|
|
79
|
-
const here = fileURLToPath(import.meta.url);
|
|
80
|
-
// `here` is `…/packages/helix/{src,dist}/cli/native.ts|js`. The `.node` lives
|
|
81
|
-
// two levels up at `…/packages/helix/index.<suffix>.node`.
|
|
82
|
-
const suffix = platformSuffix();
|
|
83
|
-
const candidate = `../../index.${suffix}.node`;
|
|
84
|
-
let loaded: unknown;
|
|
85
|
-
try {
|
|
86
|
-
loaded = require(candidate);
|
|
87
|
-
} catch (err) {
|
|
88
|
-
const cause = err instanceof Error ? err.message : String(err);
|
|
89
|
-
const muslHint = suffix.endsWith("-gnu")
|
|
90
|
-
? " If you are on Alpine/musl, note the prebuilt binaries target glibc (musl is not a supported target)."
|
|
91
|
-
: "";
|
|
92
|
-
throw new Error(
|
|
93
|
-
`@c9up/helix native binary 'index.${suffix}.node' not found or failed to load near ${here} — run 'pnpm --filter @c9up/helix build:napi' to build it.${muslHint} Cause: ${cause}`,
|
|
94
|
-
{ cause: err },
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
if (!isNativeExports(loaded)) {
|
|
98
|
-
throw new Error(
|
|
99
|
-
"@c9up/helix native binary loaded but missing the expected 'run' export. Rebuild with 'pnpm --filter @c9up/helix build:napi'.",
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
cachedNative = loaded;
|
|
103
|
-
return cachedNative;
|
|
104
|
-
}
|