@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,819 @@
1
+ import { resolvePackageBinary } from "./package-binary.js";
2
+ import { runCaptured } from "./process.js";
3
+ import { resolveNativeCommand } from "./runtime.js";
4
+ import { createRequire } from "node:module";
5
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
6
+ import { dirname, isAbsolute, join, parse, relative, resolve, sep } from "node:path";
7
+ import { pathToFileURL } from "node:url";
8
+ import { tmpdir } from "node:os";
9
+ import { existsSync, realpathSync } from "node:fs";
10
+ //#region src/lint-js-plugins.ts
11
+ const OXLINT_JS_PLUGIN_LANE_MINIMUM = "1.74.0";
12
+ const OXLINT_JS_PLUGIN_LANE_BELOW = "2.0.0";
13
+ const CONFIG_FILE_NAMES = [".oxlintrc.json", ".oxlintrc.jsonc"];
14
+ const BUILTIN_CATEGORIES = [
15
+ "correctness",
16
+ "nursery",
17
+ "pedantic",
18
+ "perf",
19
+ "restriction",
20
+ "style",
21
+ "suspicious"
22
+ ];
23
+ function versionParts(version) {
24
+ return String(version).split(/[-+]/u, 1)[0].split(".").map((part) => Number.parseInt(part, 10));
25
+ }
26
+ function compareVersions(left, right) {
27
+ const a = versionParts(left);
28
+ const b = versionParts(right);
29
+ for (let index = 0; index < 3; index += 1) {
30
+ const first = Number.isInteger(a[index]) ? a[index] : 0;
31
+ const second = Number.isInteger(b[index]) ? b[index] : 0;
32
+ if (first !== second) return first < second ? -1 : 1;
33
+ }
34
+ return 0;
35
+ }
36
+ function laneSupportsOxlintVersion(version) {
37
+ if (typeof version !== "string" || !/^\d+\.\d+\.\d+/u.test(version)) return false;
38
+ return compareVersions(version, "1.74.0") >= 0 && compareVersions(version, "2.0.0") < 0;
39
+ }
40
+ function oxlintVersionRefusal(version) {
41
+ return `oxlint (oxc-tsrx): JS plugins on .tsrx require oxlint >=${OXLINT_JS_PLUGIN_LANE_MINIMUM} <${OXLINT_JS_PLUGIN_LANE_BELOW}; found ${version}. Refusing rather than silently skipping your rules.`;
42
+ }
43
+ /** The pinned Oxlint's own version, read through its public `./package.json` export. */
44
+ function installedOxlintVersion(fromUrl = import.meta.url) {
45
+ const manifest = createRequire(fromUrl)("oxlint-current/package.json");
46
+ return typeof manifest.version === "string" ? manifest.version : "unknown";
47
+ }
48
+ /**
49
+ * The one line this lane prints before the report.
50
+ *
51
+ * The extra parse is real and the user is told about it every time, on stderr,
52
+ * with the exact key that turns it off. `--silent` suppresses it along with
53
+ * everything else the command would have printed.
54
+ */
55
+ function jsPluginDisclosure(fileCount) {
56
+ return `oxlint (oxc-tsrx): running JS plugins on ${fileCount} .tsrx file(s) by linting the TSX projection; this parses each of those files once more. Disable with "settings": { "oxcTsrx": { "jsPluginsOnTsrx": false } }.`;
57
+ }
58
+ /**
59
+ * The line that reports plugin diagnostics this lane could not place.
60
+ *
61
+ * A diagnostic whose labels land on text the projection inserted has no
62
+ * position in the file the developer wrote, so it is dropped rather than
63
+ * reported somewhere they can see no such code. Dropping it quietly is the same
64
+ * silence this lane exists to remove, one level down: the rule looks like it
65
+ * simply found nothing. So the count reaches stderr, and
66
+ * `oxcTsrx.jsPluginProjection.unmapped` carries it in `--format=json` too.
67
+ */
68
+ function jsPluginUnmappedNote(count) {
69
+ return `oxlint (oxc-tsrx): ${count} JS plugin diagnostic(s) on .tsrx had no position in the source you wrote (they landed on text the TSX projection inserted) and were dropped. See oxcTsrx.jsPluginProjection.unmapped in --format=json.`;
70
+ }
71
+ /**
72
+ * Read a `.oxlintrc.json` or `.oxlintrc.jsonc`.
73
+ *
74
+ * Comments and trailing commas are stripped rather than parsed, because this
75
+ * file only ever re-emits plain JSON. Anything it does not understand is copied
76
+ * through untouched, so Oxlint keeps deciding what the configuration means.
77
+ */
78
+ function parseOxlintConfigText(text) {
79
+ let stripped = "";
80
+ let index = 0;
81
+ while (index < text.length) {
82
+ const character = text[index];
83
+ if (character === "\"") {
84
+ const start = index;
85
+ index += 1;
86
+ while (index < text.length) {
87
+ if (text[index] === "\\") {
88
+ index += 2;
89
+ continue;
90
+ }
91
+ if (text[index] === "\"") {
92
+ index += 1;
93
+ break;
94
+ }
95
+ index += 1;
96
+ }
97
+ stripped += text.slice(start, index);
98
+ continue;
99
+ }
100
+ if (character === "/" && text[index + 1] === "/") {
101
+ while (index < text.length && text[index] !== "\n") index += 1;
102
+ continue;
103
+ }
104
+ if (character === "/" && text[index + 1] === "*") {
105
+ const end = text.indexOf("*/", index + 2);
106
+ index = end === -1 ? text.length : end + 2;
107
+ continue;
108
+ }
109
+ stripped += character;
110
+ index += 1;
111
+ }
112
+ return JSON.parse(stripped.replace(/,(\s*[}\]])/gu, "$1"));
113
+ }
114
+ async function readOxlintConfig(path) {
115
+ try {
116
+ const parsed = parseOxlintConfigText(await readFile(path, "utf8"));
117
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
118
+ } catch {
119
+ return null;
120
+ }
121
+ }
122
+ /** The nearest Oxlint config at or above `directory`, the way Oxlint looks for one. */
123
+ function findOxlintConfig(directory) {
124
+ let current = resolve(directory);
125
+ const root = parse(current).root;
126
+ for (;;) {
127
+ for (const name of CONFIG_FILE_NAMES) {
128
+ const candidate = join(current, name);
129
+ if (existsSync(candidate)) return candidate;
130
+ }
131
+ if (current === root) return null;
132
+ current = dirname(current);
133
+ }
134
+ }
135
+ /**
136
+ * Every entry of one config's `jsPlugins`, normalized.
137
+ *
138
+ * Oxlint accepts both a bare specifier and `{ name, specifier }`, where `name`
139
+ * is the alias the plugin's rules are configured under. Vite+ writes the second
140
+ * form. The alias, when there is one, saves this lane from having to import the
141
+ * module to learn the plugin's namespace.
142
+ */
143
+ function declaredJsPlugins(config) {
144
+ const declared = config?.jsPlugins;
145
+ if (!Array.isArray(declared)) return [];
146
+ const entries = [];
147
+ for (const entry of declared) {
148
+ if (typeof entry === "string" && entry.length > 0) {
149
+ entries.push({
150
+ specifier: entry,
151
+ name: null
152
+ });
153
+ continue;
154
+ }
155
+ if (entry !== null && typeof entry === "object" && typeof entry.specifier === "string") entries.push({
156
+ specifier: entry.specifier,
157
+ name: typeof entry.name === "string" && entry.name.length > 0 ? entry.name : null
158
+ });
159
+ }
160
+ return entries;
161
+ }
162
+ /**
163
+ * Every JavaScript plugin one configuration brings in, and whether the project
164
+ * turned this lane off.
165
+ *
166
+ * `extends` is followed because a project that keeps its shared rules in one
167
+ * file and its per-package config in another still expects its plugins to run.
168
+ * Missing this would not be a visible error; it would be the rule quietly not
169
+ * running, which is exactly the failure this lane exists to remove. Each plugin
170
+ * specifier travels with the directory of the config that declared it, because
171
+ * that is what it resolves against.
172
+ */
173
+ async function collectLaneFacts(path, directoryOverride = null, seen = /* @__PURE__ */ new Set(), depth = 0) {
174
+ const facts = {
175
+ config: null,
176
+ jsPlugins: [],
177
+ optedOut: void 0
178
+ };
179
+ if (depth > 8 || seen.has(path)) return facts;
180
+ seen.add(path);
181
+ const config = await readOxlintConfig(path);
182
+ if (config === null) return facts;
183
+ facts.config = config;
184
+ const directory = directoryOverride ?? dirname(path);
185
+ if (Array.isArray(config.extends)) for (const specifier of config.extends) {
186
+ if (typeof specifier !== "string") continue;
187
+ const resolved = resolveSpecifier(specifier, directory);
188
+ if (!isAbsolute(resolved) || !existsSync(resolved)) continue;
189
+ const inherited = await collectLaneFacts(resolved, null, seen, depth + 1);
190
+ facts.jsPlugins.push(...inherited.jsPlugins);
191
+ if (inherited.optedOut !== void 0) facts.optedOut = inherited.optedOut;
192
+ }
193
+ for (const entry of declaredJsPlugins(config)) facts.jsPlugins.push({
194
+ ...entry,
195
+ directory
196
+ });
197
+ if (Array.isArray(config.overrides)) for (const override of config.overrides) for (const entry of declaredJsPlugins(override)) facts.jsPlugins.push({
198
+ ...entry,
199
+ directory
200
+ });
201
+ const own = config.settings?.oxcTsrx?.jsPluginsOnTsrx;
202
+ if (typeof own === "boolean") facts.optedOut = own === false;
203
+ return facts;
204
+ }
205
+ /** Resolve one plugin or extends specifier against the directory its config lives in. */
206
+ function resolveSpecifier(specifier, configDirectory) {
207
+ if (isAbsolute(specifier)) return specifier;
208
+ if (specifier.startsWith(".")) return resolve(configDirectory, specifier);
209
+ try {
210
+ return createRequire(join(configDirectory, "package.json")).resolve(specifier);
211
+ } catch {
212
+ return specifier;
213
+ }
214
+ }
215
+ /**
216
+ * The plugin namespaces this project's `jsPlugins` contribute, or `null` when
217
+ * they cannot all be determined.
218
+ *
219
+ * A rule's diagnostic code is `<plugin meta.name>(<rule>)`, so this is what
220
+ * separates a diagnostic the user's own JavaScript produced from a built-in one
221
+ * that a `rules` entry re-enabled behind the categories this lane turns off.
222
+ * `null` means "do not filter by namespace", which is strictly more permissive
223
+ * and can only ever leave a duplicate in, never drop a user's rule.
224
+ */
225
+ async function pluginNamespaces(declared) {
226
+ const namespaces = /* @__PURE__ */ new Set();
227
+ for (const { specifier, name: alias, directory } of declared) {
228
+ if (alias !== null) {
229
+ namespaces.add(alias);
230
+ continue;
231
+ }
232
+ const resolved = resolveSpecifier(specifier, directory);
233
+ try {
234
+ const module = await (isAbsolute(resolved) ? import(pathToFileURL(resolved).href) : import(resolved));
235
+ const name = module.default?.meta?.name ?? module.meta?.name;
236
+ if (typeof name !== "string" || name.length === 0) return null;
237
+ namespaces.add(name);
238
+ } catch {
239
+ return null;
240
+ }
241
+ }
242
+ return namespaces;
243
+ }
244
+ /**
245
+ * A glob and the same glob with `.tsx` appended.
246
+ *
247
+ * The mirror names each projection `<authored name>.tsx`, so a project that
248
+ * wrote `overrides: [{ files: ["**\/*.tsrx"] }]` would match nothing there. This
249
+ * was measured rather than assumed: `**\/*.tsrx` does not match `demo.tsrx.tsx`,
250
+ * and `**\/*.tsrx.tsx` does.
251
+ */
252
+ function projectedGlobs(globs) {
253
+ if (!Array.isArray(globs)) return globs;
254
+ const expanded = [];
255
+ for (const glob of globs) {
256
+ expanded.push(glob);
257
+ if (typeof glob === "string" && !expanded.includes(`${glob}.tsx`)) expanded.push(`${glob}.tsx`);
258
+ }
259
+ return expanded;
260
+ }
261
+ /** One `jsPlugins` entry with its specifier resolved, in either form Oxlint accepts. */
262
+ function absoluteJsPlugin(entry, configDirectory) {
263
+ if (typeof entry === "string") return resolveSpecifier(entry, configDirectory);
264
+ if (entry !== null && typeof entry === "object" && typeof entry.specifier === "string") return {
265
+ ...entry,
266
+ specifier: resolveSpecifier(entry.specifier, configDirectory)
267
+ };
268
+ return entry;
269
+ }
270
+ /**
271
+ * The user's configuration as the projection run should see it.
272
+ *
273
+ * Everything Oxlint understands survives, because Oxlint is the thing resolving
274
+ * it. Four edits, each for one reason:
275
+ *
276
+ * * every built-in category off, so the native lane stays the only reporter of
277
+ * built-in rules and nothing is printed twice;
278
+ * * `jsPlugins` and `extends` made absolute, because the config is read from a
279
+ * different directory than the one it was written in;
280
+ * * `ignorePatterns` dropped, because the native lane has already applied them
281
+ * and they were written against `.tsrx` names the mirror does not use;
282
+ * * every `overrides` glob given a `.tsx` twin, so an override aimed at
283
+ * `.tsrx` still selects that file's projection.
284
+ */
285
+ function projectionConfig(config, configDirectory) {
286
+ const projected = { ...config };
287
+ delete projected.$schema;
288
+ delete projected.ignorePatterns;
289
+ projected.categories = { ...config.categories ?? {} };
290
+ for (const category of BUILTIN_CATEGORIES) projected.categories[category] = "off";
291
+ if (Array.isArray(config.jsPlugins)) projected.jsPlugins = config.jsPlugins.map((entry) => absoluteJsPlugin(entry, configDirectory));
292
+ if (Array.isArray(config.extends)) projected.extends = config.extends.map((specifier) => typeof specifier === "string" ? resolveSpecifier(specifier, configDirectory) : specifier);
293
+ if (Array.isArray(config.overrides)) projected.overrides = config.overrides.map((override) => {
294
+ if (override === null || typeof override !== "object") return override;
295
+ const mapped = { ...override };
296
+ mapped.files = projectedGlobs(override.files);
297
+ if (override.excludeFiles !== void 0) mapped.excludeFiles = projectedGlobs(override.excludeFiles);
298
+ if (Array.isArray(override.jsPlugins)) mapped.jsPlugins = override.jsPlugins.map((entry) => absoluteJsPlugin(entry, configDirectory));
299
+ return mapped;
300
+ });
301
+ return projected;
302
+ }
303
+ /** The user's configuration with `jsPlugins` removed, for the native lane. */
304
+ function nativeLaneConfig(config) {
305
+ const stripped = { ...config };
306
+ delete stripped.jsPlugins;
307
+ if (Array.isArray(config.overrides)) stripped.overrides = config.overrides.map((override) => {
308
+ if (override === null || typeof override !== "object") return override;
309
+ const mapped = { ...override };
310
+ delete mapped.jsPlugins;
311
+ return mapped;
312
+ });
313
+ return stripped;
314
+ }
315
+ /** Where one authored path lives inside the mirror, relative to the mirror root. */
316
+ function mirrorRelativePath(cwd, path) {
317
+ const relativePath = relative(cwd, path);
318
+ if (relativePath !== "" && !relativePath.startsWith("..") && !isAbsolute(relativePath)) return `${relativePath}.tsx`;
319
+ const flattened = path.replace(/^[A-Za-z]:/u, "").split(/[\\/]/u).filter((segment) => segment.length > 0 && segment !== "..").join(sep);
320
+ return `${join("__outside_cwd__", flattened)}.tsx`;
321
+ }
322
+ async function writeMirrorFile(root, relativePath, contents) {
323
+ const absolute = join(root, relativePath);
324
+ await mkdir(dirname(absolute), { recursive: true });
325
+ await writeFile(absolute, contents);
326
+ return absolute;
327
+ }
328
+ /**
329
+ * Decide whether the JavaScript plugin lane runs for this invocation, and set it
330
+ * up if it does.
331
+ *
332
+ * Returns `null` when there is nothing to do, or one of:
333
+ *
334
+ * * `{ status: "opted-out" }` — the project turned the lane off, so the native
335
+ * lane keeps `jsPlugins` and answers with its own refusal;
336
+ * * `{ status: "version-refused", message }` — the installed Oxlint is outside
337
+ * the supported range, so the command must stop rather than skip rules;
338
+ * * `{ status: "active", ... }` — ready to run.
339
+ */
340
+ async function preparePluginLane({ cwd, files, viteConfig, explicitConfig }) {
341
+ if (files.length === 0) return null;
342
+ const nativeSource = viteConfig ? {
343
+ path: viteConfig.path,
344
+ base: viteConfig.base,
345
+ explicit: true,
346
+ directory: viteConfig.base
347
+ } : explicitConfig ? {
348
+ path: resolve(cwd, explicitConfig),
349
+ base: dirname(resolve(cwd, explicitConfig)),
350
+ explicit: true
351
+ } : (() => {
352
+ const discovered = findOxlintConfig(cwd);
353
+ return discovered === null ? null : {
354
+ path: discovered,
355
+ base: dirname(discovered),
356
+ explicit: false
357
+ };
358
+ })();
359
+ const configs = /* @__PURE__ */ new Map();
360
+ const laneFiles = [];
361
+ let sawOptOut = false;
362
+ for (const file of files) {
363
+ const path = nativeSource?.explicit ? nativeSource.path : findOxlintConfig(dirname(file));
364
+ if (path === null || path === void 0) continue;
365
+ let entry = configs.get(path);
366
+ if (entry === void 0) {
367
+ const directoryOverride = path === nativeSource?.path ? nativeSource.directory ?? null : null;
368
+ const facts = await collectLaneFacts(path, directoryOverride);
369
+ entry = {
370
+ path,
371
+ config: facts.config,
372
+ directory: directoryOverride ?? dirname(path),
373
+ jsPlugins: facts.jsPlugins,
374
+ stripsNative: declaredJsPlugins(facts.config).length > 0,
375
+ optedOut: facts.optedOut === true,
376
+ files: []
377
+ };
378
+ configs.set(path, entry);
379
+ }
380
+ if (entry.jsPlugins.length === 0) continue;
381
+ if (entry.optedOut) {
382
+ sawOptOut = true;
383
+ continue;
384
+ }
385
+ entry.files.push(file);
386
+ laneFiles.push(file);
387
+ }
388
+ const nativeConfigEntry = nativeSource === null ? null : configs.get(nativeSource.path);
389
+ const nativeNeedsStrip = Boolean(nativeConfigEntry && nativeConfigEntry.stripsNative && !nativeConfigEntry.optedOut);
390
+ if (laneFiles.length === 0) return sawOptOut ? { status: "opted-out" } : null;
391
+ const version = installedOxlintVersion();
392
+ if (!laneSupportsOxlintVersion(version)) return {
393
+ status: "version-refused",
394
+ message: oxlintVersionRefusal(version)
395
+ };
396
+ const active = [...configs.values()].filter((entry) => entry.files.length > 0);
397
+ const temporary = [];
398
+ let nativeConfig = null;
399
+ if (nativeNeedsStrip) {
400
+ const directory = await mkdtemp(join(tmpdir(), "oxc-tsrx-native-config-"));
401
+ temporary.push(directory);
402
+ const path = join(directory, ".oxlintrc.json");
403
+ await writeFile(path, `${JSON.stringify(nativeLaneConfig(nativeConfigEntry.config))}\n`);
404
+ nativeConfig = {
405
+ path,
406
+ base: nativeSource.base,
407
+ typeAware: viteConfig?.typeAware === true,
408
+ typeCheck: viteConfig?.typeCheck === true
409
+ };
410
+ }
411
+ return {
412
+ status: "active",
413
+ files: laneFiles,
414
+ fileCount: laneFiles.length,
415
+ nativeConfig,
416
+ notice: jsPluginDisclosure(laneFiles.length),
417
+ async run() {
418
+ return runPluginLane({
419
+ cwd,
420
+ configs: active,
421
+ nativeConfig,
422
+ explicit: Boolean(nativeSource?.explicit),
423
+ temporary
424
+ });
425
+ },
426
+ async cleanup() {
427
+ await Promise.all(temporary.map((directory) => rm(directory, {
428
+ recursive: true,
429
+ force: true
430
+ })));
431
+ }
432
+ };
433
+ }
434
+ async function emitProjections(cwd, files, nativeConfig) {
435
+ const args = ["--emit-plugin-projection"];
436
+ if (nativeConfig) args.push("--config", nativeConfig.path, "--config-base", nativeConfig.base);
437
+ const command = resolveNativeCommand("lint", [...args, ...files]);
438
+ const result = await runCaptured(command.executable, command.args, { cwd });
439
+ if (result.status !== 0) throw new Error(`the native TSRX projection needed for JS plugins failed:\n${result.stderr || result.stdout}`);
440
+ let parsed;
441
+ try {
442
+ parsed = JSON.parse(result.stdout);
443
+ } catch {
444
+ throw new Error(`the native TSRX projection needed for JS plugins returned non-JSON output:\n${result.stdout}`);
445
+ }
446
+ return Array.isArray(parsed.projections) ? parsed.projections : [];
447
+ }
448
+ async function mapDiagnostics(cwd, byFile) {
449
+ const command = resolveNativeCommand("lint", ["--map-plugin-diagnostics"]);
450
+ const request = JSON.stringify({ files: [...byFile].map(([path, diagnostics]) => ({
451
+ path,
452
+ diagnostics
453
+ })) });
454
+ const result = await runCaptured(command.executable, command.args, {
455
+ cwd,
456
+ input: request
457
+ });
458
+ if (result.status !== 0) throw new Error(`mapping JS plugin diagnostics back to authored .tsrx positions failed:\n${result.stderr || result.stdout}`);
459
+ let parsed;
460
+ try {
461
+ parsed = JSON.parse(result.stdout);
462
+ } catch {
463
+ throw new Error(`mapping JS plugin diagnostics back to authored .tsrx positions returned non-JSON output:\n${result.stdout}`);
464
+ }
465
+ return Array.isArray(parsed.files) ? parsed.files : [];
466
+ }
467
+ function diagnosticNamespace(diagnostic) {
468
+ const code = typeof diagnostic.code === "string" ? diagnostic.code : "";
469
+ const open = code.indexOf("(");
470
+ return open === -1 ? code : code.slice(0, open);
471
+ }
472
+ /**
473
+ * The failures Oxlint's plugin host itself reported, in the user's own terms.
474
+ *
475
+ * A rule that throws does not come back as a diagnostic on a file: Oxlint reports
476
+ * it with an empty `filename`, no `code`, and no labels, which is exactly the
477
+ * shape every other filter in this file drops. Dropping it too would mean a
478
+ * broken rule looks like a rule that found nothing, which is the failure this
479
+ * whole lane exists to remove. `paths` rewrites the mirror path Oxlint saw back
480
+ * to the file the developer opened.
481
+ */
482
+ /**
483
+ * Mirror paths to the authored paths they stand for, each mirror path recorded
484
+ * under both the name it was written with and the one a `realpath` resolves it
485
+ * to. Oxlint reports the resolved name, and on macOS a temporary directory is
486
+ * always a symlink away from it.
487
+ */
488
+ function authoredPathMap(pairs) {
489
+ const map = /* @__PURE__ */ new Map();
490
+ for (const [mirrorPath, authored] of pairs) {
491
+ map.set(mirrorPath, authored);
492
+ try {
493
+ map.set(realpathSync(mirrorPath), authored);
494
+ } catch {}
495
+ }
496
+ return map;
497
+ }
498
+ function pluginHostFailures(report, paths = /* @__PURE__ */ new Map()) {
499
+ const failures = [];
500
+ for (const diagnostic of report?.diagnostics ?? []) {
501
+ if (typeof diagnostic?.message !== "string" || diagnostic.message === "") continue;
502
+ if (diagnostic.filename !== void 0 && diagnostic.filename !== "") continue;
503
+ if ((diagnostic.labels ?? []).length > 0) continue;
504
+ if (typeof diagnostic.code === "string" && diagnostic.code !== "") continue;
505
+ let message = diagnostic.message.split(/\n\s+at /u, 1)[0].trim();
506
+ const rewrites = [...paths].sort(([left], [right]) => right.length - left.length);
507
+ for (const [from, to] of rewrites) message = message.split(from).join(to);
508
+ failures.push(message);
509
+ }
510
+ return failures;
511
+ }
512
+ async function runPluginLane({ cwd, configs, nativeConfig, explicit, temporary }) {
513
+ const projections = await emitProjections(cwd, configs.flatMap((entry) => entry.files), nativeConfig);
514
+ if (projections.length === 0) return {
515
+ diagnostics: [],
516
+ files: 0,
517
+ extraParses: 0,
518
+ unmapped: 0,
519
+ failures: []
520
+ };
521
+ const mirror = await mkdtemp(join(tmpdir(), "oxc-tsrx-js-plugins-"));
522
+ temporary.push(mirror);
523
+ const authoredByMirrorPath = /* @__PURE__ */ new Map();
524
+ const mirrored = [];
525
+ for (const projection of projections) {
526
+ if (typeof projection?.path !== "string" || typeof projection.projected !== "string") continue;
527
+ const relativePath = mirrorRelativePath(cwd, projection.path);
528
+ await writeMirrorFile(mirror, relativePath, projection.projected);
529
+ authoredByMirrorPath.set(relativePath, projection.path);
530
+ mirrored.push(relativePath);
531
+ }
532
+ if (mirrored.length === 0) return {
533
+ diagnostics: [],
534
+ files: 0,
535
+ extraParses: 0,
536
+ unmapped: 0,
537
+ failures: []
538
+ };
539
+ const namespaces = /* @__PURE__ */ new Set();
540
+ let namespacesKnown = true;
541
+ for (const entry of configs) {
542
+ const projected = projectionConfig(entry.config, entry.directory);
543
+ const relativeConfig = explicit ? ".oxlintrc.json" : (() => {
544
+ const candidate = relative(cwd, entry.path);
545
+ return candidate !== "" && !candidate.startsWith("..") && !isAbsolute(candidate) ? candidate : ".oxlintrc.json";
546
+ })();
547
+ await writeMirrorFile(mirror, relativeConfig, `${JSON.stringify(projected, null, 2)}\n`);
548
+ entry.mirrorConfig = relativeConfig;
549
+ const found = await pluginNamespaces(entry.jsPlugins);
550
+ if (found === null) namespacesKnown = false;
551
+ else for (const name of found) namespaces.add(name);
552
+ }
553
+ const oxlintArgs = [resolvePackageBinary("oxlint-current", "oxlint", import.meta.url), "--format=json"];
554
+ if (explicit) oxlintArgs.push("--config", configs[0].mirrorConfig);
555
+ const result = await runCaptured(process.execPath, [...oxlintArgs, ...mirrored], {
556
+ cwd: mirror,
557
+ env: process.env
558
+ });
559
+ if (result.status > 1) throw new Error(`running your JS plugins over the .tsrx projection failed:\n${result.stderr || result.stdout}`);
560
+ let report;
561
+ try {
562
+ report = JSON.parse(result.stdout);
563
+ } catch {
564
+ throw new Error(`running your JS plugins over the .tsrx projection returned non-JSON output:\n${result.stdout}${result.stderr}`);
565
+ }
566
+ const byFile = /* @__PURE__ */ new Map();
567
+ for (const relativePath of mirrored) byFile.set(authoredByMirrorPath.get(relativePath), []);
568
+ for (const diagnostic of report.diagnostics ?? []) {
569
+ const authored = authoredByMirrorPath.get(diagnostic.filename);
570
+ if (authored === void 0) continue;
571
+ const namespace = diagnosticNamespace(diagnostic);
572
+ if (namespace === "") continue;
573
+ if (namespacesKnown && !namespaces.has(namespace)) continue;
574
+ byFile.get(authored).push(diagnostic);
575
+ }
576
+ const nonEmpty = new Map([...byFile].filter(([, diagnostics]) => diagnostics.length > 0));
577
+ const diagnostics = [];
578
+ let unmapped = 0;
579
+ if (nonEmpty.size > 0) for (const file of await mapDiagnostics(cwd, nonEmpty)) {
580
+ if (Number.isSafeInteger(file.unmapped) && file.unmapped > 0) unmapped += file.unmapped;
581
+ for (const diagnostic of file.diagnostics ?? []) diagnostics.push({
582
+ ...diagnostic,
583
+ filename: file.path
584
+ });
585
+ }
586
+ return {
587
+ diagnostics,
588
+ files: mirrored.length,
589
+ extraParses: mirrored.length,
590
+ unmapped,
591
+ failures: pluginHostFailures(report, authoredPathMap([...authoredByMirrorPath].map(([relativePath, authored]) => [join(mirror, relativePath), authored])))
592
+ };
593
+ }
594
+ /** The flag that turns this module into the editor's lane host. */
595
+ const LANE_HOST_FLAG = "--oxc-tsrx-js-plugin-lane-host";
596
+ /**
597
+ * The one line the editor session prints when the lane starts.
598
+ *
599
+ * An editor has no report to put a notice in front of, so this goes to the
600
+ * server's stderr, which every LSP client surfaces as its output log. It names
601
+ * the extra parse and the exact key that turns it off, the same way the command
602
+ * line's own notice does.
603
+ */
604
+ function jsPluginEditorDisclosure() {
605
+ return "oxc-tsrx-lsp: running this project's Oxlint JS plugins on .tsrx by linting each file's TSX projection; this parses every linted .tsrx file once more. Disable with \"settings\": { \"oxcTsrx\": { \"jsPluginsOnTsrx\": false } }.";
606
+ }
607
+ /** One long-lived mirror, config set, and Oxlint invocation for an editor session. */
608
+ var EditorPluginLane = class {
609
+ cwd;
610
+ mirror;
611
+ configs;
612
+ constructor(cwd) {
613
+ this.cwd = resolve(cwd);
614
+ this.mirror = null;
615
+ this.configs = /* @__PURE__ */ new Map();
616
+ }
617
+ async mirrorRoot() {
618
+ if (this.mirror === null) this.mirror = await mkdtemp(join(tmpdir(), "oxc-tsrx-js-plugins-lsp-"));
619
+ return this.mirror;
620
+ }
621
+ /**
622
+ * The configuration governing one directory, resolved and mirrored once.
623
+ *
624
+ * A configuration file that changes during the session is not re-read here:
625
+ * the language server watches `.oxlintrc.json`, rebuilds its workspace tool on
626
+ * a change, and that drops this whole process along with the stale cache.
627
+ */
628
+ async entryFor(directory) {
629
+ const path = findOxlintConfig(directory);
630
+ if (path === null) return { active: false };
631
+ const cached = this.configs.get(path);
632
+ if (cached !== void 0) return cached;
633
+ const facts = await collectLaneFacts(path);
634
+ let entry = { active: false };
635
+ if (facts.config !== null && facts.jsPlugins.length > 0 && facts.optedOut !== true) {
636
+ const mirror = await this.mirrorRoot();
637
+ const candidate = relative(this.cwd, path);
638
+ await writeMirrorFile(mirror, candidate !== "" && !candidate.startsWith("..") && !isAbsolute(candidate) ? candidate : ".oxlintrc.json", `${JSON.stringify(projectionConfig(facts.config, dirname(path)), null, 2)}\n`);
639
+ entry = {
640
+ active: true,
641
+ namespaces: await pluginNamespaces(facts.jsPlugins)
642
+ };
643
+ }
644
+ this.configs.set(path, entry);
645
+ return entry;
646
+ }
647
+ /**
648
+ * Run this project's JavaScript plugins over one projection.
649
+ *
650
+ * Returns Oxlint's own diagnostics with their label spans still measured in
651
+ * projection bytes. Mapping them to authored bytes is the caller's job,
652
+ * because the caller is the process that owns the span map.
653
+ */
654
+ async lint(path, projection) {
655
+ const entry = await this.entryFor(dirname(path));
656
+ if (!entry.active) return [];
657
+ const mirror = await this.mirrorRoot();
658
+ const relativePath = mirrorRelativePath(this.cwd, path);
659
+ await writeMirrorFile(mirror, relativePath, projection);
660
+ const oxlintBinary = resolvePackageBinary("oxlint-current", "oxlint", import.meta.url);
661
+ const result = await runCaptured(process.execPath, [
662
+ oxlintBinary,
663
+ "--format=json",
664
+ relativePath
665
+ ], {
666
+ cwd: mirror,
667
+ env: process.env
668
+ });
669
+ if (result.status > 1) throw new Error(`running your JS plugins over the .tsrx projection failed:\n${result.stderr || result.stdout}`);
670
+ let report;
671
+ try {
672
+ report = JSON.parse(result.stdout);
673
+ } catch {
674
+ throw new Error(`running your JS plugins over the .tsrx projection returned non-JSON output:\n${result.stdout}${result.stderr}`);
675
+ }
676
+ const failures = pluginHostFailures(report, authoredPathMap([[join(mirror, relativePath), path]]));
677
+ if (failures.length > 0) throw new Error(failures.join("\n"));
678
+ const diagnostics = [];
679
+ for (const diagnostic of report.diagnostics ?? []) {
680
+ if (diagnostic.filename !== relativePath) continue;
681
+ const namespace = diagnosticNamespace(diagnostic);
682
+ if (namespace === "") continue;
683
+ if (entry.namespaces !== null && !entry.namespaces.has(namespace)) continue;
684
+ const labels = [];
685
+ for (const label of diagnostic.labels ?? []) {
686
+ const offset = label?.span?.offset;
687
+ if (!Number.isSafeInteger(offset) || offset < 0) continue;
688
+ const length = label.span.length;
689
+ labels.push({
690
+ offset,
691
+ length: Number.isSafeInteger(length) && length > 0 ? length : 0
692
+ });
693
+ }
694
+ if (labels.length === 0) continue;
695
+ diagnostics.push({
696
+ code: typeof diagnostic.code === "string" ? diagnostic.code : null,
697
+ message: typeof diagnostic.message === "string" ? diagnostic.message : "",
698
+ severity: diagnostic.severity === "error" ? "error" : "warning",
699
+ help: typeof diagnostic.help === "string" ? diagnostic.help : null,
700
+ labels
701
+ });
702
+ }
703
+ return diagnostics;
704
+ }
705
+ async cleanup() {
706
+ if (this.mirror !== null) await rm(this.mirror, {
707
+ recursive: true,
708
+ force: true
709
+ });
710
+ }
711
+ };
712
+ /**
713
+ * Serve the editor's plugin lane over newline-delimited JSON on stdio.
714
+ *
715
+ * The first line out is the handshake: `{"ready":true,...}`, or
716
+ * `{"ready":false,"error":...}` when the installed Oxlint is outside the range
717
+ * this lane was established against. Refusing out loud is the point — an editor
718
+ * that quietly stopped running a developer's rule is the failure this whole lane
719
+ * exists to remove, and a squiggle that silently disappears is worse than one
720
+ * that never appeared.
721
+ *
722
+ * Every request is `{id, path, projection}` and every answer is either
723
+ * `{id, diagnostics}` or `{id, error}`. Requests are served one at a time and in
724
+ * order, so a burst of keystrokes cannot interleave two Oxlint runs over the
725
+ * same mirror file.
726
+ */
727
+ async function runJsPluginLaneHost({ cwd = process.cwd(), input = process.stdin, output = process.stdout, errorOutput = process.stderr } = {}) {
728
+ const version = installedOxlintVersion();
729
+ if (!laneSupportsOxlintVersion(version)) {
730
+ output.write(`${JSON.stringify({
731
+ ready: false,
732
+ error: oxlintVersionRefusal(version)
733
+ })}\n`);
734
+ return 0;
735
+ }
736
+ const lane = new EditorPluginLane(cwd);
737
+ errorOutput.write(`${jsPluginEditorDisclosure()}\n`);
738
+ output.write(`${JSON.stringify({
739
+ ready: true,
740
+ oxlint: version
741
+ })}\n`);
742
+ let pending = Promise.resolve();
743
+ let buffer = "";
744
+ await new Promise((finished) => {
745
+ const drain = () => {
746
+ pending.then(() => finished(), () => finished());
747
+ };
748
+ input.setEncoding("utf8");
749
+ input.on("data", (chunk) => {
750
+ buffer += chunk;
751
+ for (;;) {
752
+ const newline = buffer.indexOf("\n");
753
+ if (newline === -1) break;
754
+ const line = buffer.slice(0, newline).trim();
755
+ buffer = buffer.slice(newline + 1);
756
+ if (line === "") continue;
757
+ let request;
758
+ try {
759
+ request = JSON.parse(line);
760
+ } catch {
761
+ continue;
762
+ }
763
+ pending = pending.then(async () => {
764
+ let answer;
765
+ try {
766
+ answer = {
767
+ id: request.id,
768
+ diagnostics: await lane.lint(String(request.path), String(request.projection))
769
+ };
770
+ } catch (error) {
771
+ answer = {
772
+ id: request.id,
773
+ error: error instanceof Error ? error.message : String(error)
774
+ };
775
+ }
776
+ output.write(`${JSON.stringify(answer)}\n`);
777
+ });
778
+ }
779
+ });
780
+ input.once("end", drain);
781
+ input.once("close", drain);
782
+ input.once("error", drain);
783
+ });
784
+ await lane.cleanup();
785
+ return 0;
786
+ }
787
+ /**
788
+ * Whether this module is the process entry point.
789
+ *
790
+ * `process.argv[1]` keeps the path the caller named while `import.meta.url`
791
+ * reports the real one, and the editor reaches this file through a package
792
+ * symlink often enough that comparing them raw would silently never match.
793
+ */
794
+ function invokedAsLaneHost() {
795
+ if (!process.argv.includes("--oxc-tsrx-js-plugin-lane-host")) return false;
796
+ const entry = process.argv[1];
797
+ if (typeof entry !== "string" || entry === "") return false;
798
+ for (const candidate of [entry, (() => {
799
+ try {
800
+ return realpathSync(entry);
801
+ } catch {
802
+ return entry;
803
+ }
804
+ })()]) try {
805
+ if (pathToFileURL(candidate).href === import.meta.url) return true;
806
+ } catch {}
807
+ return false;
808
+ }
809
+ if (invokedAsLaneHost()) {
810
+ const index = process.argv.indexOf("--cwd");
811
+ runJsPluginLaneHost({ cwd: index === -1 ? process.cwd() : process.argv[index + 1] ?? process.cwd() }).then((status) => {
812
+ process.exitCode = status;
813
+ }, (error) => {
814
+ process.stderr.write(`oxc-tsrx-lsp: the JS plugin lane host stopped: ${error instanceof Error ? error.message : String(error)}\n`);
815
+ process.exitCode = 1;
816
+ });
817
+ }
818
+ //#endregion
819
+ export { LANE_HOST_FLAG, OXLINT_JS_PLUGIN_LANE_BELOW, OXLINT_JS_PLUGIN_LANE_MINIMUM, installedOxlintVersion, jsPluginDisclosure, jsPluginEditorDisclosure, jsPluginUnmappedNote, laneSupportsOxlintVersion, mirrorRelativePath, nativeLaneConfig, oxlintVersionRefusal, parseOxlintConfigText, preparePluginLane, projectionConfig };