@tsrx/oxc 0.0.0-trusted-publishing-bootstrap → 0.8.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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +141 -0
  3. package/THIRD_PARTY_NOTICES.md +49 -0
  4. package/bin/oxc-tsrx +2 -0
  5. package/bin/oxc-tsrx-fmt +2 -0
  6. package/bin/oxc-tsrx-lint +2 -0
  7. package/bin/oxc-tsrx-lsp +2 -0
  8. package/bin/oxfmt +2 -0
  9. package/bin/oxlint +2 -0
  10. package/dist/bin/oxc-tsrx-fmt.js +13 -0
  11. package/dist/bin/oxc-tsrx-lint.js +13 -0
  12. package/dist/bin/oxc-tsrx-lsp.js +13 -0
  13. package/dist/bin/oxc-tsrx.js +115 -0
  14. package/dist/bin/oxfmt.js +24 -0
  15. package/dist/bin/oxlint.js +33 -0
  16. package/dist/canonical-command.d.ts +50 -0
  17. package/dist/canonical-command.js +196 -0
  18. package/dist/compat.d.ts +149 -0
  19. package/dist/compat.js +1615 -0
  20. package/dist/editor-resolution.js +508 -0
  21. package/dist/format-cli.js +276 -0
  22. package/dist/format-invocation.js +97 -0
  23. package/dist/format.d.ts +1 -0
  24. package/dist/format.js +56 -0
  25. package/dist/index.d.ts +8 -0
  26. package/dist/index.js +16 -0
  27. package/dist/lint-cli.js +487 -0
  28. package/dist/lint-invocation.js +192 -0
  29. package/dist/lint-js-plugins.js +819 -0
  30. package/dist/lint-plugins-dev.d.ts +1 -0
  31. package/dist/lint-plugins-dev.js +2 -0
  32. package/dist/lint-prestart.js +16 -0
  33. package/dist/lint.d.ts +1 -0
  34. package/dist/lint.js +2 -0
  35. package/dist/native-targets.js +76 -0
  36. package/dist/oxlint-lsp-multiplexer.js +622 -0
  37. package/dist/package-binary.js +29 -0
  38. package/dist/parser.d.ts +216 -0
  39. package/dist/parser.js +557 -0
  40. package/dist/process.js +88 -0
  41. package/dist/provider-resolve.d.ts +160 -0
  42. package/dist/provider-resolve.js +471 -0
  43. package/dist/providers-report.js +49 -0
  44. package/dist/runtime.js +323 -0
  45. package/dist/spawn-command.d.ts +20 -0
  46. package/dist/spawn-command.js +87 -0
  47. package/dist/tsrx-core-compat/facade.js +1184 -0
  48. package/dist/tsrx-core-compat/index.d.ts +6 -0
  49. package/dist/tsrx-core-compat/index.js +9 -0
  50. package/dist/tsrx-core-compat/style.js +525 -0
  51. package/dist/tsrx-core-compat/types/estree.d.ts +20 -0
  52. package/dist/tsrx-core-compat/types/index.d.ts +50 -0
  53. package/dist/tsrx-transfer.js +352 -0
  54. package/package.json +144 -5
@@ -0,0 +1,508 @@
1
+ import { posix, win32 } from "node:path";
2
+ //#region src/editor-resolution.ts
3
+ /**
4
+ * What would the editor actually run?
5
+ *
6
+ * `setup` writes one key, `oxc.path.oxlint`, and then hopes. This module removes
7
+ * the hoping. It replays the binary lookup that the official OXC extension
8
+ * (`oxc.oxc-vscode` 1.59.0, `out/main.js`) performs, over a *fixture* tree that
9
+ * the caller injects, so a test can ask "for this workspace topology, which
10
+ * linter would the extension spawn, and does that binary know about `.tsrx`?"
11
+ * without an install, an editor, or a spawned process.
12
+ *
13
+ * Everything that touches the outside world is a parameter. There is no
14
+ * `vscode` import, no `node:fs`, no `child_process`, no network. The only
15
+ * capability this module has is the `stat` function it is handed.
16
+ *
17
+ * The chain being replayed, verbatim from the extension:
18
+ *
19
+ * async searchBinaryPath(e,t){return e?se(t,e):await M(t)??await ie(t)??await ae(t)??await oe(t)}
20
+ *
21
+ * The single most consequential fact in that line is the `?:`. A configured
22
+ * value does not *win* the lookup, it *replaces* it. When `oxc.path.oxlint` is
23
+ * set and does not resolve, the extension stops: no `node_modules/.bin`, no
24
+ * `PATH`, no linter at all. A stale key is strictly worse than no key, which is
25
+ * why every failure here still reports `source: "configured"` rather than
26
+ * falling through.
27
+ *
28
+ * Deliberate divergence from `compat.js`'s `inspectLinterShim`: that function
29
+ * reads `oxlint.cmd` and `oxlint.ps1` on Windows because that is what npm and
30
+ * pnpm write. This one does not, because the extension does not. `j()` builds
31
+ * exactly `<name>` and `<name>.exe`, so on Windows a tree whose only shim is
32
+ * `oxlint.cmd` reads to the extension as a tree with no shim at all.
33
+ */
34
+ /**
35
+ * `T(e)` in the extension. Rejects a configured value outright, before any
36
+ * filesystem access. `..` is checked as a substring, not as a path segment, so
37
+ * a directory named `my..dir` is rejected too. `.\` is the two-character
38
+ * sequence dot-backslash.
39
+ */
40
+ const CONFIGURED_METACHARACTERS = Object.freeze([
41
+ "$",
42
+ "&",
43
+ ";",
44
+ "|",
45
+ "`",
46
+ ">",
47
+ "<",
48
+ "!",
49
+ "%",
50
+ "^"
51
+ ]);
52
+ /** Yarn PnP loaders, in the order `re()` probes them. */
53
+ const PNP_LOADERS = Object.freeze([".pnp.cjs", ".pnp.js"]);
54
+ const TSRX_EXTENSION = ".tsrx";
55
+ function pathFor(platform) {
56
+ return platform === "win32" ? win32 : posix;
57
+ }
58
+ /**
59
+ * Normalise whatever the injected `stat` returned into one shape, or `null` when
60
+ * the path does not exist. A fixture stat returns `{content, realPath}`; a
61
+ * real-filesystem stat may return a `Stats` object or simply `true`, in which
62
+ * case there is no content to read and `realPath` is the path itself. Throwing
63
+ * counts as absent, which is how the extension's `try { await fs.stat } catch`
64
+ * behaves.
65
+ */
66
+ async function statEntry(stat, path) {
67
+ let result;
68
+ try {
69
+ result = await stat(path);
70
+ } catch {
71
+ return null;
72
+ }
73
+ if (!result) return null;
74
+ return {
75
+ path,
76
+ realPath: typeof result.realPath === "string" ? result.realPath : path,
77
+ content: typeof result.content === "string" ? result.content : null
78
+ };
79
+ }
80
+ async function readPackageSource(context, directory) {
81
+ return (await statEntry(context.stat, context.path.join(directory, "package.json")))?.content ?? null;
82
+ }
83
+ /**
84
+ * `A(e,t)` in the extension: given a file inside a package, walk up to that
85
+ * package's `package.json` and resolve its `bin` entry for `name`.
86
+ *
87
+ * Two failure modes are load-bearing and both throw, exactly as the extension
88
+ * does, because every caller wraps this in a `try` and treats a throw as "this
89
+ * step of the chain found nothing": a `package.json` with no matching `bin`
90
+ * entry, and a tree with no `package.json` at all. `JSON.parse` is outside the
91
+ * read guard for the same reason it is in the extension: a malformed manifest
92
+ * aborts the walk rather than being skipped over.
93
+ */
94
+ async function resolvePackageBin(context, file, name) {
95
+ let directory = context.path.dirname(file);
96
+ while (directory !== context.path.dirname(directory)) {
97
+ const source = await readPackageSource(context, directory);
98
+ if (source === null) {
99
+ directory = context.path.dirname(directory);
100
+ continue;
101
+ }
102
+ const manifest = JSON.parse(source);
103
+ const bin = typeof manifest.bin === "string" ? manifest.bin : manifest.bin?.[name];
104
+ if (!bin) throw new Error(`No bin entry for "${name}" found in package.json`);
105
+ return context.path.resolve(directory, bin);
106
+ }
107
+ throw new Error(`Could not find package.json for "${name}"`);
108
+ }
109
+ /**
110
+ * `j(e,t)` in the extension. `t` is a list of `node_modules` directories; each
111
+ * contributes `<dir>/.bin/<name>` and, on Windows only, `<dir>/.bin/<name>.exe`.
112
+ * The extension stats every candidate in parallel and takes the first index that
113
+ * succeeded, which is the same answer this sequential walk gives.
114
+ *
115
+ * Note `loader: "native"`. A `.bin` hit is never treated as a Node script, even
116
+ * when it is one, so on Windows an extensionless shim here is unspawnable for
117
+ * the same reason a configured extensionless path is.
118
+ */
119
+ async function searchBinDirectories(context, moduleDirectories, source) {
120
+ for (const directory of moduleDirectories) {
121
+ const base = context.path.join(directory, ".bin", context.name);
122
+ const candidates = context.platform === "win32" ? [base, `${base}.exe`] : [base];
123
+ for (const candidate of candidates) {
124
+ const entry = await statEntry(context.stat, candidate);
125
+ if (entry) return {
126
+ source,
127
+ path: candidate,
128
+ loader: "native",
129
+ entry
130
+ };
131
+ }
132
+ }
133
+ return null;
134
+ }
135
+ /** `M(e)`: workspace `.bin`, then globbed-package `.bin`, then `require.resolve`. */
136
+ async function searchWorkspace(context) {
137
+ const direct = await searchBinDirectories(context, context.workspaceFolders.map((folder) => context.path.join(folder, "node_modules")), "workspace-node-modules");
138
+ if (direct) return direct;
139
+ const nested = await searchBinDirectories(context, context.packageJsonDirectories.map((directory) => context.path.join(directory, "node_modules")), "package-json-node-modules");
140
+ if (nested) return nested;
141
+ if (typeof context.requireResolve !== "function") return null;
142
+ try {
143
+ const binary = await resolvePackageBin(context, context.requireResolve(context.name, { paths: context.workspaceFolders }), context.name);
144
+ return {
145
+ source: "require-resolve",
146
+ path: binary,
147
+ loader: "node",
148
+ entry: await statEntry(context.stat, binary)
149
+ };
150
+ } catch {
151
+ return null;
152
+ }
153
+ }
154
+ /** `re(e)`: walk up looking for a PnP API, `.pnp.cjs` before `.pnp.js`. */
155
+ function findPnpApi(context, folder) {
156
+ if (!context.pnp) return null;
157
+ let directory = folder;
158
+ for (;;) {
159
+ for (const loader of PNP_LOADERS) {
160
+ const loaderPath = context.path.join(directory, loader);
161
+ const api = typeof context.pnp === "function" ? context.pnp(loaderPath) : context.pnp[loaderPath];
162
+ if (api && typeof api.resolveRequest === "function") return {
163
+ api,
164
+ loaderPath
165
+ };
166
+ }
167
+ const parent = context.path.dirname(directory);
168
+ if (parent === directory) return null;
169
+ directory = parent;
170
+ }
171
+ }
172
+ /**
173
+ * `ie(e)`: Yarn PnP. Guarded by workspace trust, so an untrusted window loses
174
+ * PnP resolution as well as the configured value.
175
+ */
176
+ async function searchYarnPnp(context) {
177
+ if (!context.trusted) return null;
178
+ for (const folder of context.workspaceFolders) {
179
+ const discovered = findPnpApi(context, folder);
180
+ if (!discovered) continue;
181
+ try {
182
+ const request = discovered.api.resolveRequest(context.name, folder + context.path.sep);
183
+ if (!request) continue;
184
+ const binary = await resolvePackageBin(context, request, context.name);
185
+ const entry = await statEntry(context.stat, binary);
186
+ if (!entry) continue;
187
+ return {
188
+ source: "yarn-pnp",
189
+ path: binary,
190
+ loader: "node",
191
+ entry,
192
+ yarnPnpLoaderPath: discovered.loaderPath
193
+ };
194
+ } catch {
195
+ continue;
196
+ }
197
+ }
198
+ return null;
199
+ }
200
+ /**
201
+ * `ae(e)`: the global roots. These are already `node_modules` directories
202
+ * (`npm root -g`, `pnpm root -g`, `~/.bun/install/global/node_modules`), so
203
+ * `.bin` is appended directly rather than `node_modules/.bin`.
204
+ */
205
+ async function searchGlobalRoots(context) {
206
+ const found = await searchBinDirectories(context, context.globalRoots, "global-node-modules");
207
+ if (found) return found;
208
+ if (typeof context.requireResolve !== "function") return null;
209
+ try {
210
+ const binary = await resolvePackageBin(context, context.requireResolve(context.name, { paths: context.globalRoots }), context.name);
211
+ return {
212
+ source: "global-require-resolve",
213
+ path: binary,
214
+ loader: "node",
215
+ entry: await statEntry(context.stat, binary)
216
+ };
217
+ } catch {
218
+ return null;
219
+ }
220
+ }
221
+ /** `oe(e)`: the last resort, `PATH`, with the same `<name>` / `<name>.exe` pair. */
222
+ async function searchPathEntries(context) {
223
+ for (const directory of context.pathEntries) {
224
+ if (!directory) continue;
225
+ const base = context.path.join(directory, context.name);
226
+ const candidates = context.platform === "win32" ? [base, `${base}.exe`] : [base];
227
+ for (const candidate of candidates) {
228
+ const entry = await statEntry(context.stat, candidate);
229
+ if (entry) return {
230
+ source: "path",
231
+ path: candidate,
232
+ loader: "native",
233
+ entry
234
+ };
235
+ }
236
+ }
237
+ return null;
238
+ }
239
+ /**
240
+ * The loader test from `se()`, and the only place the extension infers a loader
241
+ * from the shape of a path. It matters that `<name>/bin/<name>` is checked with
242
+ * the *package* name: the value `setup` writes ends in `@tsrx/oxc/bin/oxlint`,
243
+ * not `oxlint/bin/oxlint`, so it classifies as `native` and takes the shell
244
+ * spawn route on Windows.
245
+ */
246
+ function configuredLoader(name, value, platform) {
247
+ const path = pathFor(platform);
248
+ return value.endsWith(".js") || value.endsWith(".cjs") || value.endsWith(".mjs") || value.endsWith(`${name}${path.sep}bin${path.sep}${name}`) ? "node" : "native";
249
+ }
250
+ /**
251
+ * `T(e)`. Returns the reason a configured value is refused, or `null` when it is
252
+ * acceptable. Split into two reasons because the remedies differ: a traversal
253
+ * means "this value can never work, there is no relative escape from the
254
+ * project", while a metacharacter usually means a path that simply needs
255
+ * quoting out of existence.
256
+ */
257
+ function rejectConfiguredValue(value) {
258
+ if (value.includes("..") || value.includes(".\\")) return "configured-rejected-traversal";
259
+ for (const character of CONFIGURED_METACHARACTERS) if (value.includes(character)) return "configured-rejected-metacharacter";
260
+ return null;
261
+ }
262
+ /**
263
+ * Would the resolved command actually start?
264
+ *
265
+ * The extension spawns a `node` loader through `process.execPath`, but a
266
+ * `native` loader through:
267
+ *
268
+ * {command: isWin32 ? `"${e.path}"` : e.path, args:['--lsp'], options:{shell: isWin32}}
269
+ *
270
+ * `shell: true` on Windows is `cmd.exe`, which can only execute `.exe`, `.com`,
271
+ * `.bat` and `.cmd`. An extensionless file is not one of those, so the spawn
272
+ * fails and the editor goes quiet. The one escape hatch is `oxc.useExecPath`,
273
+ * which forces the `node` route regardless of loader; that setting is not
274
+ * modelled here because it is not something `setup` can write, and every value
275
+ * this oracle judges must stand on its own.
276
+ */
277
+ function isSpawnable(path, loader, platform) {
278
+ if (!path) return false;
279
+ if (platform !== "win32") return true;
280
+ if (loader !== "native") return true;
281
+ return win32.extname(path) !== "";
282
+ }
283
+ function claimsTsrx(manifest) {
284
+ const languages = manifest?.oxc?.provider?.languages;
285
+ if (!Array.isArray(languages)) return false;
286
+ return languages.some((language) => Array.isArray(language?.extensions) && language.extensions.some((extension) => String(extension).toLowerCase() === TSRX_EXTENSION));
287
+ }
288
+ /**
289
+ * Does the binary that will actually run belong to a package that understands
290
+ * `.tsrx`?
291
+ *
292
+ * The question is asked of the *real* path, because the interesting failure is a
293
+ * `node_modules/.bin/oxlint` that exists, stats fine, and is a text shim for
294
+ * Vite+. Resolution succeeds and diagnostics never appear. Only the nearest
295
+ * enclosing `package.json` is consulted: walking further up would eventually
296
+ * reach the consumer's own manifest, which is not the package being run.
297
+ */
298
+ async function isTsrxAware(context, file) {
299
+ if (!file) return false;
300
+ let directory = context.path.dirname(file);
301
+ for (;;) {
302
+ const source = await readPackageSource(context, directory);
303
+ if (source !== null) try {
304
+ return claimsTsrx(JSON.parse(source));
305
+ } catch {
306
+ return false;
307
+ }
308
+ const parent = context.path.dirname(directory);
309
+ if (parent === directory) return false;
310
+ directory = parent;
311
+ }
312
+ }
313
+ function unresolved(source, reason, attempted = null) {
314
+ return {
315
+ source,
316
+ path: null,
317
+ loader: null,
318
+ spawnable: false,
319
+ tsrxAware: false,
320
+ reason,
321
+ realPath: null,
322
+ attempted
323
+ };
324
+ }
325
+ async function describe(context, found) {
326
+ const realPath = found.entry?.realPath ?? found.path;
327
+ const result = {
328
+ source: found.source,
329
+ path: found.path,
330
+ loader: found.loader,
331
+ spawnable: isSpawnable(found.path, found.loader, context.platform),
332
+ tsrxAware: await isTsrxAware(context, realPath),
333
+ reason: "resolved",
334
+ realPath,
335
+ attempted: found.path
336
+ };
337
+ if (found.yarnPnpLoaderPath) result.yarnPnpLoaderPath = found.yarnPnpLoaderPath;
338
+ return result;
339
+ }
340
+ /**
341
+ * `se(e,t)`: the configured value.
342
+ *
343
+ * Order is the whole point and it is not the order anyone assumes. Trust first,
344
+ * then character validation, then, only for a relative value, the join onto
345
+ * `workspaceFolders[0]`. That first folder is whichever folder happens to be
346
+ * first in the window, not the folder holding the `settings.json` the value was
347
+ * written into and not the folder owning the open document, so the same relative
348
+ * value that works in a single-folder window silently addresses the wrong tree
349
+ * in a multi-root one.
350
+ */
351
+ async function resolveConfigured(context, configured) {
352
+ if (!context.trusted) return unresolved("configured", "untrusted-workspace", configured);
353
+ const rejection = rejectConfiguredValue(configured);
354
+ if (rejection) return unresolved("configured", rejection, configured);
355
+ let value = configured;
356
+ if (!context.path.isAbsolute(value)) {
357
+ const first = context.workspaceFolders[0];
358
+ if (!first) return unresolved("configured", "no-workspace-folder", configured);
359
+ value = context.path.normalize(context.path.join(first, value));
360
+ }
361
+ if (context.platform !== "win32" && value.endsWith(".exe")) value = value.slice(0, -4);
362
+ const loader = configuredLoader(context.name, value, context.platform);
363
+ const entry = await statEntry(context.stat, value);
364
+ if (entry) return describe(context, {
365
+ source: "configured",
366
+ path: value,
367
+ loader,
368
+ entry
369
+ });
370
+ if (context.platform === "win32") {
371
+ const withExe = value.endsWith(".exe") ? value : `${value}.exe`;
372
+ const exeEntry = await statEntry(context.stat, withExe);
373
+ if (exeEntry) return describe(context, {
374
+ source: "configured",
375
+ path: withExe,
376
+ loader: "native",
377
+ entry: exeEntry
378
+ });
379
+ }
380
+ return unresolved("configured", "configured-missing", value);
381
+ }
382
+ /**
383
+ * Resolve the linter binary the official extension would run.
384
+ *
385
+ * @param {object} options
386
+ * @param {string} options.name Binary name the extension is looking for, `oxlint`.
387
+ * @param {string|null} [options.configured] The `oxc.path.oxlint` value, if any.
388
+ * An empty string is not a value: the extension's `e ? ... : ...` treats it as
389
+ * unset and auto-detects.
390
+ * @param {string[]} [options.workspaceFolders] Absolute workspace folder paths,
391
+ * in window order. `workspaceFolders[0]` anchors relative configured values.
392
+ * @param {string[]} [options.packageJsonDirectories] Directories containing a
393
+ * `package.json`, as the extension's workspace-wide `package.json` glob would
394
+ * find them (that glob excludes `node_modules`).
395
+ * @param {(request: string, options: {paths: string[]}) => string} [options.requireResolve]
396
+ * Stand-in for `require.resolve`. Throw to mean "not resolvable".
397
+ * @param {object|((loaderPath: string) => object|null)} [options.pnp] Yarn PnP
398
+ * APIs keyed by loader path, or a lookup function. Each API needs
399
+ * `resolveRequest(request, issuer)`.
400
+ * @param {string[]} [options.globalRoots] Global `node_modules` roots.
401
+ * @param {string[]|string} [options.pathEntries] `PATH`, split or raw.
402
+ * @param {NodeJS.Platform} [options.platform]
403
+ * @param {boolean} [options.trusted] Workspace trust. Untrusted disables both the
404
+ * configured value and Yarn PnP.
405
+ * @param {(path: string) => unknown} options.stat The only capability this
406
+ * module has. Return a falsy value or throw for "absent". Return an object to
407
+ * mean "present"; `realPath` names the file that will really execute (a
408
+ * symlink target, or the binary a text shim delegates to) and `content` lets
409
+ * `package.json` be read through the same seam.
410
+ * @returns {Promise<{source: string|null, path: string|null, loader: string|null,
411
+ * spawnable: boolean, tsrxAware: boolean, reason: string, realPath: string|null,
412
+ * attempted: string|null, yarnPnpLoaderPath?: string}>}
413
+ */
414
+ async function resolveEditorLinter({ name, configured = null, workspaceFolders = [], packageJsonDirectories = [], requireResolve = null, pnp = null, globalRoots = [], pathEntries = [], platform = process.platform, trusted = true, stat } = {}) {
415
+ if (typeof name !== "string" || name.length === 0) throw new TypeError("resolveEditorLinter requires a binary name");
416
+ if (typeof stat !== "function") throw new TypeError("resolveEditorLinter requires an injected stat function");
417
+ const path = pathFor(platform);
418
+ const context = {
419
+ name,
420
+ path,
421
+ platform,
422
+ trusted,
423
+ stat,
424
+ workspaceFolders,
425
+ packageJsonDirectories,
426
+ requireResolve,
427
+ pnp,
428
+ globalRoots,
429
+ pathEntries: typeof pathEntries === "string" ? pathEntries.split(path.delimiter) : pathEntries
430
+ };
431
+ if (configured) return resolveConfigured(context, configured);
432
+ const found = await searchWorkspace(context) ?? await searchYarnPnp(context) ?? await searchGlobalRoots(context) ?? await searchPathEntries(context);
433
+ if (!found) return unresolved(null, "not-found");
434
+ return describe(context, found);
435
+ }
436
+ function fixturePathFor(value) {
437
+ return /^[A-Za-z]:[\\/]/u.test(value) || value.includes("\\") ? win32 : posix;
438
+ }
439
+ function fixtureKey(value) {
440
+ return fixturePathFor(value).normalize(value);
441
+ }
442
+ /**
443
+ * Build a `stat` over a plain object, so a test can state a workspace topology
444
+ * as data instead of as a script.
445
+ *
446
+ * Each key is an absolute path. Values may be:
447
+ * - a string, the file's contents;
448
+ * - `{content}`, the same thing spelled out;
449
+ * - `{link}`, a symlink, resolved against the entry's own directory and
450
+ * followed, which is how npm, Yarn and Bun write `node_modules/.bin`;
451
+ * - `{runs}`, an explicit real path for a file that *is* a real file but
452
+ * executes something else, which is how pnpm writes `node_modules/.bin` and
453
+ * the exact shape that makes a `.bin/oxlint` collision invisible to a plain
454
+ * existence check;
455
+ * - `{directory: true}`, an empty directory.
456
+ *
457
+ * Ancestor directories are implied, and stat succeeds on them, because
458
+ * `workspace.fs.stat` does not care what kind of node it found either.
459
+ */
460
+ function createFixtureStat(entries) {
461
+ const files = /* @__PURE__ */ new Map();
462
+ const directories = /* @__PURE__ */ new Set();
463
+ for (const [rawPath, value] of Object.entries(entries)) {
464
+ const key = fixtureKey(rawPath);
465
+ const path = fixturePathFor(key);
466
+ const descriptor = typeof value === "string" ? { content: value } : { ...value };
467
+ if (descriptor.directory) directories.add(key);
468
+ else files.set(key, descriptor);
469
+ let directory = path.dirname(key);
470
+ while (!directories.has(directory)) {
471
+ directories.add(directory);
472
+ const parent = path.dirname(directory);
473
+ if (parent === directory) break;
474
+ directory = parent;
475
+ }
476
+ }
477
+ function follow(key, seen) {
478
+ const descriptor = files.get(key);
479
+ if (!descriptor) return null;
480
+ const path = fixturePathFor(key);
481
+ const next = descriptor.link ? fixtureKey(path.resolve(path.dirname(key), descriptor.link)) : descriptor.runs ? fixtureKey(descriptor.runs) : null;
482
+ if (next === null || seen.has(next)) return {
483
+ realPath: key,
484
+ content: descriptor.content ?? null
485
+ };
486
+ seen.add(next);
487
+ const target = follow(next, seen);
488
+ if (!target) return descriptor.link ? null : {
489
+ realPath: next,
490
+ content: descriptor.content ?? null
491
+ };
492
+ return {
493
+ realPath: target.realPath,
494
+ content: descriptor.link ? target.content : descriptor.content ?? target.content
495
+ };
496
+ }
497
+ return function stat(candidate) {
498
+ const key = fixtureKey(candidate);
499
+ if (files.has(key)) return follow(key, /* @__PURE__ */ new Set([key]));
500
+ if (directories.has(key)) return {
501
+ realPath: key,
502
+ content: null
503
+ };
504
+ return null;
505
+ };
506
+ }
507
+ //#endregion
508
+ export { configuredLoader, createFixtureStat, isSpawnable, rejectConfiguredValue, resolveEditorLinter };