@vitest-agent/plugin 5.6.2 → 6.0.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/index.d.ts +58 -113
- package/index.js +1 -2
- package/layers/CoverageAnalyzerLive.js +3 -3
- package/package.json +20 -11
- package/plugin.js +13 -36
- package/reporter.js +123 -25
- package/utils/collect-performance-hints.js +205 -0
- package/utils/discover-projects.js +13 -47
- package/utils/discover-strategy.js +7 -15
- package/utils/failure-summary.js +78 -0
- package/utils/find-test-files.js +65 -82
- package/utils/is-test-shaped-package.js +4 -3
- package/utils/resolve-coverage-dir-isolation.js +1 -1
- package/utils/run-diagnostics.js +24 -0
- package/utils/run-script-lock.js +77 -233
- package/utils/runs-in-ci.js +15 -0
- package/utils/stray-output-capture.js +27 -127
- package/utils/vitest-diagnostics.js +173 -0
- package/utils/walker-fs.js +8 -23
- package/version.js +1 -1
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { RuntimeEnv } from "@effected/env";
|
|
2
|
+
import { Option } from "effect";
|
|
3
|
+
|
|
4
|
+
// src/utils/runs-in-ci.ts
|
|
5
|
+
/**
|
|
6
|
+
* Whether the process runs in CI, by `@effected/env`'s `RuntimeEnv` rule: a
|
|
7
|
+
* truthy `GITHUB_ACTIONS`, or a `CI` / `CONTINUOUS_INTEGRATION` that is set,
|
|
8
|
+
* non-empty, and not `false` / `0` (is-in-ci semantics). Reads `process.env`
|
|
9
|
+
* at call time, so the environment in force when the caller runs decides.
|
|
10
|
+
*
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
const runsInCi = () => Option.isSome(RuntimeEnv.fromRecord(process.env).ci);
|
|
14
|
+
|
|
15
|
+
export { runsInCi };
|
|
@@ -1,45 +1,7 @@
|
|
|
1
|
-
import { STRAY_OUTPUT_SOURCE,
|
|
2
|
-
import {
|
|
3
|
-
import { StringDecoder } from "node:string_decoder";
|
|
1
|
+
import { STRAY_OUTPUT_SOURCE, makeStrayOutputRecorder } from "@vitest-agent/sdk";
|
|
2
|
+
import { CliUi } from "@effected/cli/ui";
|
|
4
3
|
|
|
5
4
|
// src/utils/stray-output-capture.ts
|
|
6
|
-
/**
|
|
7
|
-
* Stray-output capture: a pass-through over Vitest's `Logger.outputStream` /
|
|
8
|
-
* `errorStream`.
|
|
9
|
-
*
|
|
10
|
-
* Why those two streams (vitest\@5.0.0, `src/node/pools/workers/`): the forks
|
|
11
|
-
* and vmForks pools fork each worker with `stdio: "pipe"` and pipe its stdout
|
|
12
|
-
* and stderr into `project.vitest.logger.outputStream` / `errorStream`,
|
|
13
|
-
* captured when the worker is constructed, so every raw byte a worker writes
|
|
14
|
-
* (a grandchild process that inherits the worker's fds included, such as an
|
|
15
|
-
* `execFileSync("git", …)` with default stdio) arrives on them. The threads
|
|
16
|
-
* and vmThreads pools pipe a worker thread's JS-level `process.stdout` /
|
|
17
|
-
* `stderr` the same way, but a child process spawned from a thread inherits
|
|
18
|
-
* the main process's real fds 1/2, so its bytes reach the terminal directly
|
|
19
|
-
* and nothing here can see them. Workers are constructed lazily by the pool,
|
|
20
|
-
* at run time, long after `configureVitest`, so installing there is early
|
|
21
|
-
* enough. The `Logger`'s own `console` was built from the ORIGINAL streams in
|
|
22
|
-
* its constructor, so Vitest's logging bypasses the wrapper; its few direct
|
|
23
|
-
* `outputStream.write` calls are escape-only (cursor show, clear screen),
|
|
24
|
-
* which pass straight through unrecorded.
|
|
25
|
-
*
|
|
26
|
-
* Every byte is recorded (bounded: counts, a few sample lines). While a
|
|
27
|
-
* printer is routed (the reporter's live view is up) and the stream's
|
|
28
|
-
* original destination is the process terminal, bytes are line-buffered and
|
|
29
|
-
* each whole line goes to the printer, which prints it above the live frame;
|
|
30
|
-
* otherwise bytes pass through to the original stream unchanged. The
|
|
31
|
-
* original is whatever stream object the Logger held: under MCP's
|
|
32
|
-
* `run_tests` that is the server's null sink, so worker output still never
|
|
33
|
-
* reaches the JSON-RPC stdout, and it is never routed.
|
|
34
|
-
*/
|
|
35
|
-
/**
|
|
36
|
-
* The most text a stream holds back waiting for a newline. Past it, the held
|
|
37
|
-
* text is routed as a line of its own, so a child that never writes a newline
|
|
38
|
-
* cannot grow the buffer without bound.
|
|
39
|
-
*
|
|
40
|
-
* @internal
|
|
41
|
-
*/
|
|
42
|
-
const MAX_HELD_CHARS = 8192;
|
|
43
5
|
const isCapture = (value) => typeof value === "object" && value !== null && "route" in value && "snapshot" in value;
|
|
44
6
|
/**
|
|
45
7
|
* The capture installed on `logger`, if any.
|
|
@@ -51,42 +13,6 @@ const strayOutputCaptureOf = (logger) => {
|
|
|
51
13
|
const value = logger[STRAY_OUTPUT_SOURCE];
|
|
52
14
|
return isCapture(value) ? value : void 0;
|
|
53
15
|
};
|
|
54
|
-
const DELEGATED = [
|
|
55
|
-
"isTTY",
|
|
56
|
-
"columns",
|
|
57
|
-
"rows",
|
|
58
|
-
"getColorDepth",
|
|
59
|
-
"hasColors",
|
|
60
|
-
"getWindowSize"
|
|
61
|
-
];
|
|
62
|
-
var StrayStream = class extends Writable {
|
|
63
|
-
original;
|
|
64
|
-
take;
|
|
65
|
-
constructor(original, take) {
|
|
66
|
-
super({ decodeStrings: false });
|
|
67
|
-
this.original = original;
|
|
68
|
-
this.take = take;
|
|
69
|
-
for (const key of DELEGATED) {
|
|
70
|
-
if (!(key in original)) continue;
|
|
71
|
-
Object.defineProperty(this, key, {
|
|
72
|
-
configurable: true,
|
|
73
|
-
enumerable: true,
|
|
74
|
-
get: () => {
|
|
75
|
-
const value = original[key];
|
|
76
|
-
return typeof value === "function" ? value.bind(original) : value;
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
_write(chunk, encoding, callback) {
|
|
82
|
-
try {
|
|
83
|
-
this.take(chunk, encoding);
|
|
84
|
-
callback();
|
|
85
|
-
} catch (err) {
|
|
86
|
-
callback(err instanceof Error ? err : new Error(String(err)));
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
};
|
|
90
16
|
/**
|
|
91
17
|
* Wrap `logger`'s output and error streams with the stray-output capture, once
|
|
92
18
|
* per Logger (a second call returns the first capture). The capture is
|
|
@@ -103,67 +29,41 @@ const installStrayOutputCapture = (logger, terminal) => {
|
|
|
103
29
|
const existing = strayOutputCaptureOf(logger);
|
|
104
30
|
if (existing !== void 0) return existing;
|
|
105
31
|
const recorder = makeStrayOutputRecorder();
|
|
106
|
-
const originals = {
|
|
107
|
-
stdout: logger.outputStream,
|
|
108
|
-
stderr: logger.errorStream
|
|
109
|
-
};
|
|
110
|
-
const decoders = {
|
|
111
|
-
stdout: new StringDecoder("utf8"),
|
|
112
|
-
stderr: new StringDecoder("utf8")
|
|
113
|
-
};
|
|
114
|
-
const held = {
|
|
115
|
-
stdout: "",
|
|
116
|
-
stderr: ""
|
|
117
|
-
};
|
|
118
32
|
let printer;
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
33
|
+
const print = (stream, line) => printer?.(stream, line) ?? false;
|
|
34
|
+
const wrap = (stream, original) => {
|
|
35
|
+
const routed = CliUi.routeStream(original, print, {
|
|
36
|
+
stream,
|
|
37
|
+
onText: (text) => recorder.write(stream, text)
|
|
38
|
+
});
|
|
39
|
+
routed.route(false);
|
|
40
|
+
return routed;
|
|
124
41
|
};
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const route = printer !== void 0 && original === terminal[stream] ? printer : void 0;
|
|
134
|
-
if (route === void 0) {
|
|
135
|
-
original.write(chunk, encoding);
|
|
136
|
-
return;
|
|
42
|
+
const streams = {
|
|
43
|
+
stdout: {
|
|
44
|
+
original: logger.outputStream,
|
|
45
|
+
routed: wrap("stdout", logger.outputStream)
|
|
46
|
+
},
|
|
47
|
+
stderr: {
|
|
48
|
+
original: logger.errorStream,
|
|
49
|
+
routed: wrap("stderr", logger.errorStream)
|
|
137
50
|
}
|
|
138
|
-
const lines = (held[stream] + text).split("\n");
|
|
139
|
-
const rest = lines.pop() ?? "";
|
|
140
|
-
if (rest.length > 8192) {
|
|
141
|
-
lines.push(rest);
|
|
142
|
-
held[stream] = "";
|
|
143
|
-
} else held[stream] = rest;
|
|
144
|
-
for (const line of lines) if (!route(stream, line)) original.write(`${line}\n`);
|
|
145
|
-
};
|
|
146
|
-
const flushHeldThrough = (stream) => {
|
|
147
|
-
const text = held[stream];
|
|
148
|
-
if (text === "") return;
|
|
149
|
-
held[stream] = "";
|
|
150
|
-
if (printer === void 0 || !printer(stream, text)) originals[stream].write(text);
|
|
151
51
|
};
|
|
152
|
-
logger.outputStream =
|
|
153
|
-
logger.errorStream =
|
|
52
|
+
logger.outputStream = streams.stdout.routed.writable;
|
|
53
|
+
logger.errorStream = streams.stderr.routed.writable;
|
|
154
54
|
const capture = {
|
|
155
55
|
snapshot: recorder.snapshot,
|
|
156
56
|
reset: recorder.reset,
|
|
157
57
|
route: (next) => {
|
|
158
|
-
if (next === void 0) {
|
|
159
|
-
flushHeld("stdout");
|
|
160
|
-
flushHeld("stderr");
|
|
161
|
-
}
|
|
162
58
|
printer = next;
|
|
59
|
+
for (const name of ["stdout", "stderr"]) {
|
|
60
|
+
const { original, routed } = streams[name];
|
|
61
|
+
routed.route(next !== void 0 && original === terminal[name]);
|
|
62
|
+
}
|
|
163
63
|
},
|
|
164
64
|
flush: () => {
|
|
165
|
-
|
|
166
|
-
|
|
65
|
+
streams.stdout.routed.flush();
|
|
66
|
+
streams.stderr.routed.flush();
|
|
167
67
|
}
|
|
168
68
|
};
|
|
169
69
|
Object.defineProperty(logger, STRAY_OUTPUT_SOURCE, {
|
|
@@ -173,4 +73,4 @@ const installStrayOutputCapture = (logger, terminal) => {
|
|
|
173
73
|
return capture;
|
|
174
74
|
};
|
|
175
75
|
|
|
176
|
-
export {
|
|
76
|
+
export { installStrayOutputCapture, strayOutputCaptureOf };
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// src/utils/vitest-diagnostics.ts
|
|
2
|
+
/**
|
|
3
|
+
* Ported from vitest@5.0.5 packages/vitest/src/node/reporters/diagnostics.ts
|
|
4
|
+
* and base.ts (report*Diagnostic). Not exported by Vitest; replace with the
|
|
5
|
+
* public export when available. Re-check on every Vitest bump.
|
|
6
|
+
*
|
|
7
|
+
* The functions below diagnostics.ts's are kept verbatim (names, thresholds,
|
|
8
|
+
* arithmetic). `getIsolateDiagnostic` is the computation extracted from
|
|
9
|
+
* `BaseReporter.reportIsolateDiagnostic` (its watch / flag gating stays with
|
|
10
|
+
* the caller), and `formatTime` is ported from
|
|
11
|
+
* packages/vitest/src/node/reporters/renderers/utils.ts.
|
|
12
|
+
*
|
|
13
|
+
* @packageDocumentation
|
|
14
|
+
*/
|
|
15
|
+
const DOM_ENVIRONMENTS = /* @__PURE__ */ new Set(["jsdom", "happy-dom"]);
|
|
16
|
+
/** Minimum summed environment setup time before the hint is worth printing. */
|
|
17
|
+
const MIN_ENVIRONMENT_TIME = 2e3;
|
|
18
|
+
/** Minimum share of the project's tracked time spent setting up environments. */
|
|
19
|
+
const MIN_ENVIRONMENT_SHARE = .25;
|
|
20
|
+
/**
|
|
21
|
+
* A hint has to be worth acting on: the estimated saving must be noticeable.
|
|
22
|
+
* Run-to-run noise of real suites is commonly a few percent, so anything below
|
|
23
|
+
* ~5% of the wall time cannot even be confirmed by trying the change - except
|
|
24
|
+
* on long runs, where 10 seconds is worth attention regardless of percentage.
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
function isSavingWorthHinting(saving, executionTime) {
|
|
28
|
+
if (saving < 250) return false;
|
|
29
|
+
return saving >= executionTime * .05 || saving >= 1e4;
|
|
30
|
+
}
|
|
31
|
+
/** Minimum summed import time before the hint is worth printing. */
|
|
32
|
+
const MIN_IMPORT_TIME = 2e3;
|
|
33
|
+
/** Minimum share of the project's tracked time spent importing modules. */
|
|
34
|
+
const MIN_IMPORT_SHARE = .25;
|
|
35
|
+
/**
|
|
36
|
+
* Minimum fraction of module fetches that re-evaluate an already evaluated
|
|
37
|
+
* module. Below this the test files import mostly disjoint graphs and
|
|
38
|
+
* reusing workers would not meaningfully reduce the import work.
|
|
39
|
+
*/
|
|
40
|
+
const MIN_IMPORT_DUPLICATION = .2;
|
|
41
|
+
/**
|
|
42
|
+
* Detects projects where test files repeatedly evaluate the same module
|
|
43
|
+
* graph. Typical for barrel-file imports: every test file pulls hundreds of
|
|
44
|
+
* shared modules to use a few of them, and `isolate: true` re-evaluates that
|
|
45
|
+
* graph for every file. The duplication is measured from the server-side
|
|
46
|
+
* fetch counts, so suites with disjoint per-file graphs - where reusing
|
|
47
|
+
* workers would not help - stay quiet.
|
|
48
|
+
* @internal
|
|
49
|
+
*/
|
|
50
|
+
function getImportDiagnostics(projects) {
|
|
51
|
+
const diagnostics = [];
|
|
52
|
+
for (const project of projects) {
|
|
53
|
+
if (project.pool !== "forks" && project.pool !== "threads" || !project.isolate || project.isolateProvided || project.browser || project.fileCount <= project.parallelism || project.importTime < MIN_IMPORT_TIME || project.trackedTime <= 0 || project.importTime / project.trackedTime < MIN_IMPORT_SHARE) continue;
|
|
54
|
+
const parallelism = Math.max(1, project.parallelism);
|
|
55
|
+
let totalFetches = 0;
|
|
56
|
+
let avoidableFetches = 0;
|
|
57
|
+
for (const count of project.fetchCounts) {
|
|
58
|
+
totalFetches += count;
|
|
59
|
+
avoidableFetches += Math.max(0, count - parallelism);
|
|
60
|
+
}
|
|
61
|
+
if (totalFetches === 0) continue;
|
|
62
|
+
const duplication = avoidableFetches / totalFetches;
|
|
63
|
+
if (duplication < MIN_IMPORT_DUPLICATION) continue;
|
|
64
|
+
const estimatedSaving = project.importTime * duplication / parallelism;
|
|
65
|
+
if (!isSavingWorthHinting(estimatedSaving, project.executionTime)) continue;
|
|
66
|
+
diagnostics.push({
|
|
67
|
+
name: project.name,
|
|
68
|
+
importTime: project.importTime,
|
|
69
|
+
share: project.importTime / project.trackedTime,
|
|
70
|
+
totalFetches,
|
|
71
|
+
uniqueModules: project.fetchCounts.length,
|
|
72
|
+
duplication,
|
|
73
|
+
estimatedSaving
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return diagnostics;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Estimates the wall-clock time saved by evaluating shared modules once per
|
|
80
|
+
* worker instead of once per test file. `moduleSelfTimes` holds, per module,
|
|
81
|
+
* the module's own evaluation time in every test file that evaluated it.
|
|
82
|
+
* Reused workers keep ~`parallelism` evaluations of each module (one per
|
|
83
|
+
* lane); the rest of the summed time is avoidable and spread across the
|
|
84
|
+
* lanes.
|
|
85
|
+
* @internal
|
|
86
|
+
*/
|
|
87
|
+
function estimateModuleEvaluationSaving(moduleSelfTimes, parallelism) {
|
|
88
|
+
const lanes = Math.max(1, parallelism);
|
|
89
|
+
let avoidable = 0;
|
|
90
|
+
for (const times of moduleSelfTimes) {
|
|
91
|
+
if (times.length <= lanes) continue;
|
|
92
|
+
let sum = 0;
|
|
93
|
+
for (const time of times) sum += time;
|
|
94
|
+
avoidable += sum * (times.length - lanes) / times.length;
|
|
95
|
+
}
|
|
96
|
+
return avoidable / lanes;
|
|
97
|
+
}
|
|
98
|
+
/** Minimum summed transform time before the hint is worth printing. */
|
|
99
|
+
const MIN_TRANSFORM_TIME = 2e3;
|
|
100
|
+
/** Minimum share of the project's tracked time spent transforming modules. */
|
|
101
|
+
const MIN_TRANSFORM_SHARE = .25;
|
|
102
|
+
/**
|
|
103
|
+
* Detects projects that spend the run transforming modules. Without the fs
|
|
104
|
+
* module cache every `vitest run` starts from scratch and transforms the
|
|
105
|
+
* whole module graph again; `fsModuleCache` persists the results on disk so
|
|
106
|
+
* repeated runs skip them.
|
|
107
|
+
* @internal
|
|
108
|
+
*/
|
|
109
|
+
function getTransformDiagnostics(projects) {
|
|
110
|
+
return projects.filter((project) => {
|
|
111
|
+
if (project.fsModuleCache || project.fsModuleCacheProvided || project.transformTime < MIN_TRANSFORM_TIME || project.trackedTime <= 0 || project.transformTime / project.trackedTime < MIN_TRANSFORM_SHARE) return false;
|
|
112
|
+
return isSavingWorthHinting(project.transformTime, project.executionTime);
|
|
113
|
+
}).map((project) => ({
|
|
114
|
+
name: project.name,
|
|
115
|
+
transformTime: project.transformTime,
|
|
116
|
+
share: project.transformTime / project.trackedTime
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Detects projects where re-creating a DOM environment for every test file
|
|
121
|
+
* dominates the run. With an isolating pool the environment is set up once
|
|
122
|
+
* per file; `vmThreads`/`vmForks` set it up once per worker while still
|
|
123
|
+
* giving every file a fresh VM context.
|
|
124
|
+
* @internal
|
|
125
|
+
*/
|
|
126
|
+
function getEnvironmentDiagnostics(projects) {
|
|
127
|
+
return projects.filter((project) => {
|
|
128
|
+
if (!DOM_ENVIRONMENTS.has(project.environment) || project.pool !== "forks" && project.pool !== "threads" || !project.isolate || project.browser || project.poolProvided || project.environmentCount <= 1 || project.environmentTime < MIN_ENVIRONMENT_TIME || project.trackedTime <= 0 || project.environmentTime / project.trackedTime < MIN_ENVIRONMENT_SHARE) return false;
|
|
129
|
+
const parallelism = Math.max(1, project.parallelism);
|
|
130
|
+
return isSavingWorthHinting(project.environmentTime / parallelism - project.environmentTime / project.environmentCount, project.executionTime);
|
|
131
|
+
}).map((project) => ({
|
|
132
|
+
name: project.name,
|
|
133
|
+
environment: project.environment,
|
|
134
|
+
environmentTime: project.environmentTime,
|
|
135
|
+
environmentCount: project.environmentCount,
|
|
136
|
+
share: project.environmentTime / project.trackedTime,
|
|
137
|
+
suggestIsolate: !project.isolateProvided
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Vitest's duration formatting (renderers/utils.ts `formatTime`).
|
|
142
|
+
* @internal
|
|
143
|
+
*/
|
|
144
|
+
function formatTime(time) {
|
|
145
|
+
if (time > 1e3) return `${(time / 1e3).toFixed(2)}s`;
|
|
146
|
+
return `${Math.round(time)}ms`;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Surfaces the cost of `isolate: true`: with isolation enabled Vitest spawns a
|
|
150
|
+
* fresh worker (and re-creates the test environment) for every test file.
|
|
151
|
+
* Extracted from `BaseReporter.reportIsolateDiagnostic`.
|
|
152
|
+
* @internal
|
|
153
|
+
*/
|
|
154
|
+
function getIsolateDiagnostic(input) {
|
|
155
|
+
const numWorkers = input.numWorkers;
|
|
156
|
+
if (!numWorkers || !input.isolates) return;
|
|
157
|
+
const startupTime = input.startupTime;
|
|
158
|
+
const avgStartup = startupTime / numWorkers;
|
|
159
|
+
const parallelism = Math.max(1, Math.min(input.numFiles, input.maxWorkers));
|
|
160
|
+
if (numWorkers <= parallelism) return;
|
|
161
|
+
const wallStartup = startupTime / parallelism;
|
|
162
|
+
const moduleSavings = input.measuresModules ? estimateModuleEvaluationSaving(input.moduleSelfTimes, parallelism) : 0;
|
|
163
|
+
const estimatedSaving = wallStartup - avgStartup + moduleSavings;
|
|
164
|
+
if (!isSavingWorthHinting(estimatedSaving, input.executionTime)) return;
|
|
165
|
+
return {
|
|
166
|
+
numWorkers,
|
|
167
|
+
avgStartup,
|
|
168
|
+
estimatedSaving,
|
|
169
|
+
measuresModules: input.measuresModules
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export { estimateModuleEvaluationSaving, formatTime, getEnvironmentDiagnostics, getImportDiagnostics, getIsolateDiagnostic, getTransformDiagnostics, isSavingWorthHinting };
|
package/utils/walker-fs.js
CHANGED
|
@@ -1,26 +1,11 @@
|
|
|
1
|
-
import { readdir, stat } from "node:fs/promises";
|
|
2
|
-
|
|
3
1
|
// src/utils/walker-fs.ts
|
|
4
|
-
/**
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const nodeWalkerFs = {
|
|
11
|
-
readDirectory: async (dir) => await readdir(dir, { withFileTypes: true }),
|
|
12
|
-
statEntry: async (path) => {
|
|
13
|
-
try {
|
|
14
|
-
const info = await stat(path);
|
|
15
|
-
return {
|
|
16
|
-
isFile: info.isFile(),
|
|
17
|
-
isDirectory: info.isDirectory(),
|
|
18
|
-
mtimeMs: info.mtimeMs
|
|
19
|
-
};
|
|
20
|
-
} catch {
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
2
|
+
/** `stat` of `path`, links followed, or `null` when it is missing or unreadable. */
|
|
3
|
+
function statOrNull(ops, path) {
|
|
4
|
+
try {
|
|
5
|
+
return ops.fileSystem.stat(path);
|
|
6
|
+
} catch {
|
|
7
|
+
return null;
|
|
23
8
|
}
|
|
24
|
-
}
|
|
9
|
+
}
|
|
25
10
|
|
|
26
|
-
export {
|
|
11
|
+
export { statOrNull };
|