@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.
- package/LICENSE +21 -0
- package/README.md +141 -0
- package/THIRD_PARTY_NOTICES.md +49 -0
- package/bin/oxc-tsrx +2 -0
- package/bin/oxc-tsrx-fmt +2 -0
- package/bin/oxc-tsrx-lint +2 -0
- package/bin/oxc-tsrx-lsp +2 -0
- package/bin/oxfmt +2 -0
- package/bin/oxlint +2 -0
- package/dist/bin/oxc-tsrx-fmt.js +13 -0
- package/dist/bin/oxc-tsrx-lint.js +13 -0
- package/dist/bin/oxc-tsrx-lsp.js +13 -0
- package/dist/bin/oxc-tsrx.js +115 -0
- package/dist/bin/oxfmt.js +24 -0
- package/dist/bin/oxlint.js +33 -0
- package/dist/canonical-command.d.ts +50 -0
- package/dist/canonical-command.js +196 -0
- package/dist/compat.d.ts +149 -0
- package/dist/compat.js +1615 -0
- package/dist/editor-resolution.js +508 -0
- package/dist/format-cli.js +276 -0
- package/dist/format-invocation.js +97 -0
- package/dist/format.d.ts +1 -0
- package/dist/format.js +56 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +16 -0
- package/dist/lint-cli.js +487 -0
- package/dist/lint-invocation.js +192 -0
- package/dist/lint-js-plugins.js +819 -0
- package/dist/lint-plugins-dev.d.ts +1 -0
- package/dist/lint-plugins-dev.js +2 -0
- package/dist/lint-prestart.js +16 -0
- package/dist/lint.d.ts +1 -0
- package/dist/lint.js +2 -0
- package/dist/native-targets.js +76 -0
- package/dist/oxlint-lsp-multiplexer.js +622 -0
- package/dist/package-binary.js +29 -0
- package/dist/parser.d.ts +216 -0
- package/dist/parser.js +557 -0
- package/dist/process.js +88 -0
- package/dist/provider-resolve.d.ts +160 -0
- package/dist/provider-resolve.js +471 -0
- package/dist/providers-report.js +49 -0
- package/dist/runtime.js +323 -0
- package/dist/spawn-command.d.ts +20 -0
- package/dist/spawn-command.js +87 -0
- package/dist/tsrx-core-compat/facade.js +1184 -0
- package/dist/tsrx-core-compat/index.d.ts +6 -0
- package/dist/tsrx-core-compat/index.js +9 -0
- package/dist/tsrx-core-compat/style.js +525 -0
- package/dist/tsrx-core-compat/types/estree.d.ts +20 -0
- package/dist/tsrx-core-compat/types/index.d.ts +50 -0
- package/dist/tsrx-transfer.js +352 -0
- 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 };
|