@tsrx/oxc 0.9.0 → 0.11.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/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <a href="https://compiled.run/oxc-tsrx">
2
+ <a href="https://oxc.tsrx.dev">
3
3
  <img alt="OXC for TSRX" width="600" src="https://raw.githubusercontent.com/tsrx-org/oxc/HEAD/.github/assets/readme-hero.png">
4
4
  </a>
5
5
  </p>
@@ -26,7 +26,7 @@ written in Rust. This package teaches them to read `.tsrx` too.
26
26
 
27
27
  _OXC for TSRX is the official OXC integration maintained by the TSRX project._
28
28
 
29
- [**Docs**](https://compiled.run/oxc-tsrx) &nbsp;·&nbsp; [**Getting started**](https://compiled.run/oxc-tsrx/guide/getting-started) &nbsp;·&nbsp; [**Playground**](https://compiled.run/oxc-tsrx/playground)
29
+ [**Docs**](https://oxc.tsrx.dev) &nbsp;·&nbsp; [**Getting started**](https://oxc.tsrx.dev/guide/getting-started) &nbsp;·&nbsp; [**Playground**](https://oxc.tsrx.dev/playground)
30
30
 
31
31
  ## Install
32
32
 
@@ -37,11 +37,11 @@ npm install --save-dev @tsrx/oxc
37
37
  That is the whole setup, for the command line and for your editor. You get
38
38
  `oxlint` and `oxfmt`, the real [OXC](https://oxc.rs) commands, now able to read
39
39
  `.tsrx`, with no config file, no ignore file, and no install script. [Vite+ needs
40
- one more command](https://compiled.run/oxc-tsrx/guide/getting-started#try-it-with-vite).
40
+ one more command](https://oxc.tsrx.dev/guide/getting-started#try-it-with-vite).
41
41
 
42
42
  You do not need Rust installed: the install downloads a ready-built program for
43
43
  your machine, one of eight published for macOS, Linux, and Windows. See
44
- [Platform support](https://compiled.run/oxc-tsrx/reference/platform-support).
44
+ [Platform support](https://oxc.tsrx.dev/reference/platform-support).
45
45
 
46
46
  ## Usage
47
47
 
@@ -64,7 +64,7 @@ These are the TSRX blocks the linter and formatter understand: `@{` blocks,
64
64
  `@if` / `@else if` / `@else`, `@for` / `@empty`, `@switch` / `@case` /
65
65
  `@default`, `@try` / `@pending` / `@catch`, tags whose name is an expression,
66
66
  `<{expression}>`, and plain `<style>` blocks
67
- ([TSRX syntax](https://compiled.run/oxc-tsrx/guide/tsrx-syntax) shows each one). Anything
67
+ ([TSRX syntax](https://oxc.tsrx.dev/guide/tsrx-syntax) shows each one). Anything
68
68
  outside that list **fails closed**: the command stops and says what it found and
69
69
  where, rather than guessing and maybe producing wrong output.
70
70
 
@@ -82,7 +82,7 @@ underlines the same problems the terminal reports. One catch: the TSRX
82
82
  toolchain's own extension owns `.tsrx`, and the OXC extension lists no
83
83
  activation event for it, so it does not start on its own. Open any JavaScript,
84
84
  TypeScript, or JSON file in the project once, and `.tsrx` works for the rest of
85
- the session. See the [editor guide](https://compiled.run/oxc-tsrx/integrations/editor).
85
+ the session. See the [editor guide](https://oxc.tsrx.dev/integrations/editor).
86
86
 
87
87
  ## Using it from your own code
88
88
 
@@ -94,7 +94,7 @@ import { format } from "@tsrx/oxc/format";
94
94
 
95
95
  The parser hands back the structure of your file and the formatter hands back
96
96
  tidied text, so you can build your own tools on the same reader the commands
97
- use. [Parsing](https://compiled.run/oxc-tsrx/guide/parsing) has the shape it returns.
97
+ use. [Parsing](https://oxc.tsrx.dev/guide/parsing) has the shape it returns.
98
98
  None of these compiles `.tsrx` either; that stays your framework's TSRX
99
99
  plugin's job.
100
100
 
@@ -121,16 +121,16 @@ editor both say when they have done that, and
121
121
  so it refuses `jsPlugins` and names `oxlint` as the command that can.
122
122
  `@tsrx/oxc/lint/plugins-dev` is for *writing* a plugin, since it re-exports the
123
123
  `RuleTester` from `oxlint`. See [Custom JavaScript
124
- plugins](https://compiled.run/oxc-tsrx/integrations/custom-js-plugins).
124
+ plugins](https://oxc.tsrx.dev/integrations/custom-js-plugins).
125
125
 
126
126
  ## Documentation
127
127
 
128
- - [Getting started](https://compiled.run/oxc-tsrx/guide/getting-started): install, first file, first run.
129
- - [Configuration](https://compiled.run/oxc-tsrx/integrations/configuration): every supported setting.
130
- - [CLI reference](https://compiled.run/oxc-tsrx/reference/cli): commands, flags, and exit codes.
131
- - [Platform support](https://compiled.run/oxc-tsrx/reference/platform-support): which of the eight published platforms are tested on every change.
132
- - [Limitations](https://compiled.run/oxc-tsrx/reference/limitations): what is not claimed yet.
133
- - [Provider protocol](https://compiled.run/oxc-tsrx/architecture/provider-protocol): the `oxc.provider` block, what reads it today, and how a plain install reaches released hosts.
128
+ - [Getting started](https://oxc.tsrx.dev/guide/getting-started): install, first file, first run.
129
+ - [Configuration](https://oxc.tsrx.dev/integrations/configuration): every supported setting.
130
+ - [CLI reference](https://oxc.tsrx.dev/reference/cli): commands, flags, and exit codes.
131
+ - [Platform support](https://oxc.tsrx.dev/reference/platform-support): which of the eight published platforms are tested on every change.
132
+ - [Limitations](https://oxc.tsrx.dev/reference/limitations): what is not claimed yet.
133
+ - [Provider protocol](https://oxc.tsrx.dev/architecture/provider-protocol): the `oxc.provider` block, what reads it today, and how a plain install reaches released hosts.
134
134
 
135
135
  `@tsrx/oxc` is the only package to depend on. The eight `@tsrx/oxc-*`
136
136
  packages are platform binaries in `optionalDependencies`, and you never name one
@@ -20,7 +20,10 @@ setup, status, and remove are the temporary compatibility bridge for consumers
20
20
  whose resolver is not provider-aware yet. The bridge creates the exact
21
21
  oxc-parser, oxlint, and oxfmt package-name facades those consumers need, and one
22
22
  fourth slot for the editor. Transitive official packages in those exact slots are
23
- preserved and restored; direct or unrecognized packages are never replaced.
23
+ preserved and restored; direct or unrecognized packages are never replaced. An
24
+ official oxlint or oxfmt you declare yourself keeps its slot and its command
25
+ name (reported as a collision, with what that means); setup still writes the
26
+ editor slot so the official OXC extension serves .tsrx through this package.
24
27
 
25
28
  The editor slot is the one thing setup writes outside node_modules. When
26
29
  node_modules/.bin/oxlint belongs to another tool, the official OXC extension
@@ -8,13 +8,16 @@ try {
8
8
  const { decideCanonicalCommand, deferralNotice, runOfficialCommand } = await import("../canonical-command.js");
9
9
  const args = process.argv.slice(2);
10
10
  const decision = await decideCanonicalCommand("oxlint");
11
- if (decision.owner === "project") {
11
+ if (args.some((argument) => argument.split("=", 1)[0] === "--lsp")) {
12
+ const { runOxlintLspMultiplexer } = await import("../oxlint-lsp-multiplexer.js");
13
+ process.exitCode = await runOxlintLspMultiplexer(args, decision.owner === "project" ? { canonical: {
14
+ binPath: decision.binPath,
15
+ version: decision.officialVersion
16
+ } } : {});
17
+ } else if (decision.owner === "project") {
12
18
  const notice = deferralNotice(decision, args);
13
19
  if (notice !== null) console.error(notice);
14
20
  await runOfficialCommand(decision);
15
- } else if (args.some((argument) => argument.split("=", 1)[0] === "--lsp")) {
16
- const { runOxlintLspMultiplexer } = await import("../oxlint-lsp-multiplexer.js");
17
- process.exitCode = await runOxlintLspMultiplexer(args);
18
21
  } else {
19
22
  const { canRunCanonicalOxlint, importDeclaredPackageBinary, planCanonicalOxlintComposition } = await import("../lint-invocation.js");
20
23
  if (canRunCanonicalOxlint(args)) await importDeclaredPackageBinary("oxlint-current", "oxlint", import.meta.url);
package/dist/compat.d.ts CHANGED
@@ -8,8 +8,22 @@ export type OxcTsrxCompatibilityState =
8
8
  export interface OxcTsrxCompatibilitySlot {
9
9
  readonly name: "oxc-parser" | "oxlint" | "oxfmt";
10
10
  readonly capability: "parser" | "lint" | "format";
11
+ /** The command name the slot's package publishes, or null for the parser slot. */
12
+ readonly binary: "oxlint" | "oxfmt" | null;
11
13
  readonly path: string;
12
14
  readonly state: OxcTsrxCompatibilityState;
15
+ /**
16
+ * Why a `collision` slot is left alone. `direct-dependency` is the project's
17
+ * own official package; `setup` works around it and the report explains it.
18
+ * The other kinds are trees the bridge no longer owns and `setup` refuses.
19
+ */
20
+ readonly collision?:
21
+ | "direct-dependency"
22
+ | "foreign-package"
23
+ | "reinstalled-over-facade"
24
+ | "facade-without-backup";
25
+ /** What occupies a `collision` slot, as its own package.json names it. */
26
+ readonly occupant?: { readonly name: string | null; readonly version: string | null };
13
27
  readonly replacedPackage?: {
14
28
  readonly name: string;
15
29
  readonly version: string;
package/dist/compat.js CHANGED
@@ -561,14 +561,22 @@ async function inspectSlot(modules, slot, providerVersion, projectManifest) {
561
561
  };
562
562
  const manifest = await readJson(join(destination, "package.json")).catch(() => null);
563
563
  const metadata = compatibilityMetadata(manifest);
564
+ const occupant = {
565
+ name: typeof manifest?.name === "string" ? manifest.name : null,
566
+ version: typeof manifest?.version === "string" ? manifest.version : null
567
+ };
568
+ const collision = (kind) => ({
569
+ slot,
570
+ destination,
571
+ state: "collision",
572
+ metadata: null,
573
+ collision: kind,
574
+ occupant
575
+ });
564
576
  if (!metadata || metadata.capability !== slot.capability) {
565
- if (manifest?.name === slot.name && typeof manifest.version === "string" && !directlySelected(projectManifest, slot.name)) {
566
- if (await exists(backupPath(modules, slot))) return {
567
- slot,
568
- destination,
569
- state: "collision",
570
- metadata: null
571
- };
577
+ if (manifest?.name === slot.name && typeof manifest.version === "string") {
578
+ if (directlySelected(projectManifest, slot.name)) return collision("direct-dependency");
579
+ if (await exists(backupPath(modules, slot))) return collision("reinstalled-over-facade");
572
580
  return {
573
581
  slot,
574
582
  destination,
@@ -580,19 +588,9 @@ async function inspectSlot(modules, slot, providerVersion, projectManifest) {
580
588
  }
581
589
  };
582
590
  }
583
- return {
584
- slot,
585
- destination,
586
- state: "collision",
587
- metadata: null
588
- };
591
+ return collision("foreign-package");
589
592
  }
590
- if (metadata.replacedPackage && !await exists(backupPath(modules, slot))) return {
591
- slot,
592
- destination,
593
- state: "collision",
594
- metadata: null
595
- };
593
+ if (metadata.replacedPackage && !await exists(backupPath(modules, slot))) return collision("facade-without-backup");
596
594
  return {
597
595
  slot,
598
596
  destination,
@@ -1345,12 +1343,17 @@ async function compatibilityStatus(options = {}) {
1345
1343
  packageManager: await detectPackageManager(projectRoot, options.userAgent),
1346
1344
  providerVersion: provider.manifest.version,
1347
1345
  selectedFrom: provider.selectedFrom,
1348
- slots: slots.map(({ slot, destination, state, replacedPackage }) => ({
1346
+ slots: slots.map(({ slot, destination, state, replacedPackage, collision, occupant }) => ({
1349
1347
  name: slot.name,
1350
1348
  capability: slot.capability,
1349
+ binary: slot.binary,
1351
1350
  path: destination,
1352
1351
  state,
1353
- ...replacedPackage ? { replacedPackage } : {}
1352
+ ...replacedPackage ? { replacedPackage } : {},
1353
+ ...collision ? {
1354
+ collision,
1355
+ occupant
1356
+ } : {}
1354
1357
  })),
1355
1358
  editorSlot: await inspectEditorSlot(projectRoot, provider.root, modules, options),
1356
1359
  languageSupport: await inspectLanguageSupport(projectRoot, modules)
@@ -1358,8 +1361,8 @@ async function compatibilityStatus(options = {}) {
1358
1361
  }
1359
1362
  async function setupCompatibility(options = {}) {
1360
1363
  const status = await compatibilityStatus(options);
1361
- const collisions = status.slots.filter((slot) => slot.state === "collision");
1362
- if (collisions.length > 0) throw new Error(`refusing to replace unowned package slot(s): ${collisions.map((slot) => slot.name).join(", ")}. Installing on top of the existing node_modules does not free the slot, so run rm -rf node_modules, install again, and run ${PROVIDER} setup again`);
1364
+ const refused = status.slots.filter((slot) => slot.state === "collision" && slot.collision !== "direct-dependency");
1365
+ if (refused.length > 0) throw new Error(`refusing to replace unowned package slot(s): ${refused.map((slot) => slot.name).join(", ")}. Installing on top of the existing node_modules does not free the slot, so run rm -rf node_modules, install again, and run ${PROVIDER} setup again`);
1363
1366
  const modules = join(status.projectRoot, "node_modules");
1364
1367
  if (!await exists(modules)) throw new Error(`node_modules is missing under ${status.projectRoot}; install dependencies first`);
1365
1368
  let tsconfigWrite = null;
@@ -1407,7 +1410,7 @@ async function setupCompatibility(options = {}) {
1407
1410
  languageSupport,
1408
1411
  ...tsconfigWrite ? { tsconfigWrite } : {},
1409
1412
  changed,
1410
- unchanged: [...status.slots.filter((slot) => slot.state === "active").map((slot) => slot.name), ...editorWritten ? [] : [status.editorSlot.name]]
1413
+ unchanged: [...status.slots.filter((slot) => slot.state === "active" || slot.state === "collision").map((slot) => slot.name), ...editorWritten ? [] : [status.editorSlot.name]]
1411
1414
  };
1412
1415
  }
1413
1416
  async function removeCompatibility(options = {}) {
@@ -1466,6 +1469,20 @@ async function removeCompatibility(options = {}) {
1466
1469
  removed
1467
1470
  };
1468
1471
  }
1472
+ /**
1473
+ * What a package-slot collision means for the reader, by kind. Only the direct
1474
+ * dependency gets prose in the report, because it is the one a reader can act on
1475
+ * two different ways and the one that used to be a silent failure: the command
1476
+ * names stay with their package, so the drop-in `oxlint`/`oxfmt` skip `.tsrx`
1477
+ * with nothing on the command line saying so under pnpm.
1478
+ */
1479
+ const SLOT_COLLISION_EXPLANATION = Object.freeze({ "direct-dependency": (slot) => {
1480
+ const version = slot.occupant?.version ? ` ${slot.occupant.version}` : "";
1481
+ if (!slot.binary) return `${slot.name}${version} is declared in your package.json, so that slot is yours and was left alone. Import @tsrx/oxc/parser for .tsrx.`;
1482
+ const leaf = slot.name === "oxlint" ? "oxc-tsrx-lint" : "oxc-tsrx-fmt";
1483
+ const editor = slot.name === "oxlint" ? ` The editor is wired separately: with "${EDITOR_SLOT.key}" written, the official OXC extension serves .tsrx through this package and keeps ordinary files on your own oxlint.` : " The editor is unaffected: .tsrx formatting rides on the oxlint --lsp connection.";
1484
+ return `${slot.name}${version} is declared in your package.json, so the ${slot.binary} command belongs to it: on the command line it runs exactly as it did before ${PROVIDER} was installed, and it does not read .tsrx files. Run ${leaf} for .tsrx, or remove the direct ${slot.name} dependency and let this package serve both, at the ${slot.name} version this package bundles rather than the one you pinned.${editor}`;
1485
+ } });
1469
1486
  const EDITOR_SLOT_EXPLANATION = Object.freeze({
1470
1487
  active: (slot, projectRoot) => `${toPosix(relative(projectRoot, slot.path))} carries "${slot.key}": "${slot.value}". This is the one file setup writes outside node_modules; it merges that single key and never edits package.json or tsconfig.json.`,
1471
1488
  stale: (slot, projectRoot) => `${toPosix(relative(projectRoot, slot.path))} carries a "${slot.key}" this package wrote that no longer resolves here; setup refreshes it to "${slot.value}".`,
@@ -1585,6 +1602,12 @@ function formatCompatibilityReport(result) {
1585
1602
  const gutter = ` ${`${name}:`.padEnd(nameWidth + 1)} `;
1586
1603
  lines.push(`${gutter}${paint(label, SLOT_STATE_STYLE[state] ?? "cyan", color)}`);
1587
1604
  }
1605
+ for (const slot of result.slots) {
1606
+ const explain = slot.state === "collision" ? SLOT_COLLISION_EXPLANATION[slot.collision] : null;
1607
+ if (!explain) continue;
1608
+ lines.push("");
1609
+ for (const line of wrapReportText(explain(slot), " ", " ", width)) lines.push(paint(line, "dim", color));
1610
+ }
1588
1611
  if (editor) {
1589
1612
  const explain = EDITOR_SLOT_EXPLANATION[editor.state];
1590
1613
  if (explain) {
@@ -1,6 +1,6 @@
1
- import { resolvePackageBinary } from "./package-binary.js";
2
1
  import { runCaptured, runPassthrough } from "./process.js";
3
- import { argumentValue, canonicalToolEnvironment, discoverTsrxFiles, isViteConfigPath, prepareVitePlusConfig, removeExplicitTsrx, replaceConfigArgument, resolveNativeCommand } from "./runtime.js";
2
+ import { resolvePackageBinary } from "./package-binary.js";
3
+ import { argumentValue, canonicalToolEnvironment, discoverTsrxFiles, isViteConfigPath, pathArguments, prepareVitePlusConfig, removeExplicitTsrx, replaceConfigArgument, resolveNativeCommand } from "./runtime.js";
4
4
  import { VALUE_OPTIONS, parseOxfmtInvocation, parseOxfmtOption } from "./format-invocation.js";
5
5
  import { isAbsolute, relative } from "node:path";
6
6
  import { readFileSync } from "node:fs";
@@ -231,7 +231,7 @@ async function runCli(args, options = {}) {
231
231
  }
232
232
  }
233
233
  const positions = invocation.positionals;
234
- const files = await discoverTsrxFiles(positions, cwd);
234
+ const files = await discoverTsrxFiles(positions, cwd, "format");
235
235
  if (files.length > 0 || hasTsrxPositional(positions)) {
236
236
  const unknown = unknownCanonicalOption(args);
237
237
  if (unknown !== null) {
@@ -242,13 +242,15 @@ async function runCli(args, options = {}) {
242
242
  const explicitConfig = argumentValue(args, /* @__PURE__ */ new Set(["-c", "--config"]));
243
243
  const bridgeViteConfig = explicitConfig === null || isViteConfigPath(explicitConfig);
244
244
  const viteConfig = files.length > 0 && bridgeViteConfig ? await prepareVitePlusConfig("fmt", cwd, isViteConfigPath(explicitConfig) ? explicitConfig : null) : null;
245
+ let nativePaths = null;
245
246
  try {
246
247
  const stripped = removeExplicitTsrx(args, VALUE_OPTIONS);
247
248
  const shouldRunUpstream = !stripped.hadPositionals || stripped.remainingPositionals > 0;
248
249
  const upstream = resolvePackageBinary("oxfmt-current", "oxfmt", import.meta.url);
249
250
  const useMaterializedUpstreamConfig = Boolean(viteConfig && !viteConfig.requiresAuthoredBase);
250
251
  const upstreamArgs = useMaterializedUpstreamConfig ? replaceConfigArgument(stripped.args, viteConfig.path) : stripped.args;
251
- const nativeArgs = files.length > 0 ? nativeArguments(args, withCwdRelativePaths(files, cwd), viteConfig) : null;
252
+ nativePaths = files.length > 0 ? await pathArguments(withCwdRelativePaths(files, cwd)) : null;
253
+ const nativeArgs = nativePaths ? nativeArguments(args, nativePaths.args, viteConfig) : null;
252
254
  const nativeCommand = nativeArgs ? resolveNativeCommand("format", nativeArgs) : null;
253
255
  const [upstreamResult, nativeResult] = await Promise.all([shouldRunUpstream ? runCaptured(process.execPath, [upstream, ...upstreamArgs], {
254
256
  cwd,
@@ -269,6 +271,7 @@ async function runCli(args, options = {}) {
269
271
  process.stderr.write(attributeNativeErrors(mergeFormatStderr(upstreamResult, nativeResult, stdout)));
270
272
  return Math.max(upstreamResult.status, nativeResult.status);
271
273
  } finally {
274
+ await nativePaths?.cleanup();
272
275
  await viteConfig?.cleanup();
273
276
  }
274
277
  }
package/dist/lint-cli.js CHANGED
@@ -1,6 +1,6 @@
1
- import { resolvePackageBinary } from "./package-binary.js";
2
1
  import { runCaptured, runPassthrough } from "./process.js";
3
- import { argumentValue, canonicalToolEnvironment, discoverTsrxFiles, ensureSupportedOutput, isViteConfigPath, prepareVitePlusConfig, removeExplicitTsrx, replaceConfigArgument, resolveNativeCommand } from "./runtime.js";
2
+ import { resolvePackageBinary } from "./package-binary.js";
3
+ import { argumentValue, canonicalToolEnvironment, discoverTsrxFiles, ensureSupportedOutput, isViteConfigPath, pathArguments, prepareVitePlusConfig, removeExplicitTsrx, replaceConfigArgument, resolveNativeCommand } from "./runtime.js";
4
4
  import { DELEGATE_ONLY, VALUE_OPTIONS, parseOxlintInvocation, parseOxlintOption, withOxlintOutputFormat } from "./lint-invocation.js";
5
5
  import { jsPluginUnmappedNote, preparePluginLane } from "./lint-js-plugins.js";
6
6
  import { readFile } from "node:fs/promises";
@@ -379,6 +379,7 @@ async function runCli(args, options = {}) {
379
379
  }
380
380
  const pluginLaneActive = pluginLane?.status === "active";
381
381
  if (pluginLaneActive && !args.includes("--silent")) process.stderr.write(`${pluginLane.notice}\n`);
382
+ let nativePaths = null;
382
383
  try {
383
384
  const stripped = removeExplicitTsrx(args, VALUE_OPTIONS);
384
385
  const shouldRunUpstream = !stripped.hadPositionals || stripped.remainingPositionals > 0;
@@ -396,7 +397,8 @@ async function runCli(args, options = {}) {
396
397
  let upstreamArgs = withOxlintOutputFormat(stripped.args, "json");
397
398
  if (useMaterializedUpstreamConfig) upstreamArgs = replaceConfigArgument(upstreamArgs, viteConfig.path);
398
399
  const nativeResolvedConfig = pluginLane?.nativeConfig ?? viteConfig;
399
- const nativeArgs = files.length > 0 ? nativeArguments(args, files, nativeResolvedConfig) : null;
400
+ nativePaths = files.length > 0 ? await pathArguments(files) : null;
401
+ const nativeArgs = nativePaths ? nativeArguments(args, nativePaths.args, nativeResolvedConfig) : null;
400
402
  const nativeCommand = nativeArgs ? resolveNativeCommand("lint", nativeArgs) : null;
401
403
  let upstreamPromise;
402
404
  if (!shouldRunUpstream) upstreamPromise = Promise.resolve({
@@ -480,6 +482,7 @@ async function runCli(args, options = {}) {
480
482
  return Math.max(upstreamResult.status, nativeResult.status, denyWarnings && warnings > 0 ? 1 : 0, exceedsMaximum ? 1 : 0, pluginErrors ? 1 : 0);
481
483
  } finally {
482
484
  await pluginLane?.cleanup?.();
485
+ await nativePaths?.cleanup();
483
486
  await viteConfig?.cleanup();
484
487
  }
485
488
  }
@@ -1,6 +1,6 @@
1
- import { resolvePackageBinary } from "./package-binary.js";
2
1
  import { runCaptured } from "./process.js";
3
- import { resolveNativeCommand } from "./runtime.js";
2
+ import { resolvePackageBinary } from "./package-binary.js";
3
+ import { pathArguments, resolveNativeCommand } from "./runtime.js";
4
4
  import { createRequire } from "node:module";
5
5
  import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
6
6
  import { dirname, isAbsolute, join, parse, relative, resolve, sep } from "node:path";
@@ -434,8 +434,14 @@ async function preparePluginLane({ cwd, files, viteConfig, explicitConfig }) {
434
434
  async function emitProjections(cwd, files, nativeConfig) {
435
435
  const args = ["--emit-plugin-projection"];
436
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 });
437
+ const paths = await pathArguments(files);
438
+ let result;
439
+ try {
440
+ const command = resolveNativeCommand("lint", [...args, ...paths.args]);
441
+ result = await runCaptured(command.executable, command.args, { cwd });
442
+ } finally {
443
+ await paths.cleanup();
444
+ }
439
445
  if (result.status !== 0) throw new Error(`the native TSRX projection needed for JS plugins failed:\n${result.stderr || result.stdout}`);
440
446
  let parsed;
441
447
  try {
@@ -1,5 +1,5 @@
1
- import { resolvePackageBinary } from "./package-binary.js";
2
1
  import { runCaptured } from "./process.js";
2
+ import { resolvePackageBinary } from "./package-binary.js";
3
3
  //#region src/lint-prestart.ts
4
4
  function startCanonicalOxlint(args, cwd = process.cwd(), env = process.env) {
5
5
  const binary = resolvePackageBinary("oxlint-current", "oxlint", import.meta.url);
@@ -566,6 +566,11 @@ async function runOxlintLspMultiplexer(args, options = {}) {
566
566
  const spawnProcess = options.spawn ?? spawn;
567
567
  const clientError = options.clientError ?? process.stderr;
568
568
  const cwd = options.cwd ?? process.cwd();
569
+ const canonicalBinary = options.canonical?.binPath ?? resolveCanonicalOxlintBinary();
570
+ if (options.canonical?.binPath) {
571
+ const version = options.canonical.version ? ` ${options.canonical.version}` : "";
572
+ clientError.write(`oxlint (oxc-tsrx): ordinary documents are served by the official oxlint${version} this project declares (${options.canonical.binPath}); .tsrx documents by the native server\n`);
573
+ }
569
574
  const childOptions = {
570
575
  cwd,
571
576
  env: {
@@ -584,7 +589,7 @@ async function runOxlintLspMultiplexer(args, options = {}) {
584
589
  cwd: index.root
585
590
  } : childOptions;
586
591
  const providers = providerLspSessions(index, spawnProcess, providerOptions);
587
- const canonical = spawnProcess(process.execPath, [resolveCanonicalOxlintBinary(), ...args], childOptions);
592
+ const canonical = spawnProcess(process.execPath, [canonicalBinary, ...args], childOptions);
588
593
  const multiplexer = createOxlintLspMultiplexer({
589
594
  providerRoot: providerOptions === childOptions ? null : index.root,
590
595
  clientInput: options.clientInput ?? process.stdin,
package/dist/parser.js CHANGED
@@ -3,7 +3,7 @@ import { isTsrxBinaryProgram, parseTrustedTsrxProgram, parseTsrxProgram } from "
3
3
  import { createRequire } from "node:module";
4
4
  //#region src/parser.ts
5
5
  const require = createRequire(import.meta.url);
6
- const PACKAGE_VERSION = "0.9.0";
6
+ const PACKAGE_VERSION = "0.11.0";
7
7
  const parserManifest = Object.freeze({
8
8
  name: "@tsrx/oxc",
9
9
  version: PACKAGE_VERSION,
package/dist/runtime.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { nativePackageName, nativeTargetForHost } from "./native-targets.js";
2
- import { resolvePackageBinary } from "./package-binary.js";
3
2
  import { runCaptured, runPassthrough } from "./process.js";
3
+ import { resolvePackageBinary } from "./package-binary.js";
4
4
  import { createRequire } from "node:module";
5
5
  import { mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
6
6
  import { dirname, isAbsolute, join, parse, resolve, sep } from "node:path";
@@ -254,7 +254,7 @@ function slash(path) {
254
254
  function hasMagic(path) {
255
255
  return /[*?[\]{}()!]/u.test(path);
256
256
  }
257
- async function classifyPattern(raw, cwd, positives, patterns) {
257
+ async function classifyPattern(raw, cwd, positives, patterns, directories) {
258
258
  const negative = raw.startsWith("!");
259
259
  const value = negative ? raw.slice(1) : raw;
260
260
  const absolute = isAbsolute(value) ? value : resolve(cwd, value);
@@ -266,31 +266,93 @@ async function classifyPattern(raw, cwd, positives, patterns) {
266
266
  return;
267
267
  }
268
268
  if (metadata.isDirectory()) {
269
- patterns.push(`${negative ? "!" : ""}${slash(join(absolute, "**/*.tsrx"))}`);
269
+ if (negative) patterns.push(`!${slash(join(absolute, "**/*.tsrx"))}`);
270
+ else directories.push(absolute);
270
271
  return;
271
272
  }
272
273
  } catch {}
273
274
  patterns.push(`${negative ? "!" : ""}${slash(value)}`);
274
275
  }
275
- async function classifyPatterns(inputs, cwd, positives, patterns) {
276
+ async function classifyPatterns(inputs, cwd, positives, patterns, directories) {
276
277
  const classified = await Promise.all(inputs.map(async (input) => {
277
278
  const entryPositives = /* @__PURE__ */ new Set();
278
279
  const entryPatterns = [];
279
- await classifyPattern(input, cwd, entryPositives, entryPatterns);
280
+ const entryDirectories = [];
281
+ await classifyPattern(input, cwd, entryPositives, entryPatterns, entryDirectories);
280
282
  return {
281
283
  entryPositives,
282
- entryPatterns
284
+ entryPatterns,
285
+ entryDirectories
283
286
  };
284
287
  }));
285
- for (const { entryPositives, entryPatterns } of classified) {
288
+ for (const { entryPositives, entryPatterns, entryDirectories } of classified) {
286
289
  for (const positive of entryPositives) positives.add(positive);
287
290
  for (const pattern of entryPatterns) patterns.push(pattern);
291
+ for (const directory of entryDirectories) directories.push(directory);
292
+ }
293
+ }
294
+ /**
295
+ * A file list as arguments the native leaf accepts. A short list travels on the
296
+ * command line as before. A long one is written to a temporary file and named
297
+ * with `--paths-file`, because a host's argument limit is reached by a real
298
+ * repository (E2BIG past about a megabyte on macOS and Linux, a 32 KiB command
299
+ * line on Windows, which a few hundred paths fill). The caller disposes of it.
300
+ */
301
+ async function pathArguments(files, budget = 24e3) {
302
+ if (files.reduce((total, file) => total + Buffer.byteLength(file) + 1, 0) <= budget) return {
303
+ args: [...files],
304
+ cleanup: async () => {}
305
+ };
306
+ const directory = await mkdtemp(join(tmpdir(), "oxc-tsrx-paths-"));
307
+ const path = join(directory, "paths.txt");
308
+ await writeFile(path, `${files.join("\n")}\n`);
309
+ return {
310
+ args: ["--paths-file", path],
311
+ cleanup: () => rm(directory, {
312
+ recursive: true,
313
+ force: true
314
+ })
315
+ };
316
+ }
317
+ /**
318
+ * Directories are walked by the native leaf, on the same `ignore` crate
319
+ * canonical Oxlint walks with, so `.gitignore` and `.ignore` files are honoured
320
+ * and `node_modules` and `.git` are never entered. Without that, a monorepo
321
+ * that keeps gitignored copies of itself handed every one of them to the leaf.
322
+ * Returns null when no native package is installed, and the caller falls back
323
+ * to the plain glob so the run can still reach the error that names the missing
324
+ * package.
325
+ */
326
+ async function walkWithNativeLeaf(directories, cwd, kind) {
327
+ let command;
328
+ try {
329
+ command = resolveNativeCommand(kind, ["--discover"]);
330
+ } catch {
331
+ return null;
332
+ }
333
+ const paths = await pathArguments(directories);
334
+ try {
335
+ const result = await runCaptured(command.executable, [...command.args, ...paths.args], { cwd });
336
+ if (result.status !== 0) throw new Error(`.tsrx discovery failed: ${(result.stderr || result.stdout).trim()}`);
337
+ const report = JSON.parse(result.stdout);
338
+ if (!Array.isArray(report?.files)) throw new Error(".tsrx discovery returned no file list");
339
+ return report.files.map((file) => resolve(file));
340
+ } finally {
341
+ await paths.cleanup();
288
342
  }
289
343
  }
290
- async function discoverTsrxFiles(positionals, cwd = process.cwd()) {
344
+ /**
345
+ * `kind` names the leaf that walks directories, `lint` or `format`: both
346
+ * answer `--discover` identically, and each command resolves its own binary.
347
+ */
348
+ async function discoverTsrxFiles(positionals, cwd = process.cwd(), kind = "lint") {
291
349
  const positives = /* @__PURE__ */ new Set();
292
350
  const patterns = [];
293
- await classifyPatterns(positionals.length === 0 ? ["."] : positionals, cwd, positives, patterns);
351
+ const directories = [];
352
+ await classifyPatterns(positionals.length === 0 ? ["."] : positionals, cwd, positives, patterns, directories);
353
+ const walked = directories.length > 0 && !patterns.some((pattern) => pattern.startsWith("!")) ? await walkWithNativeLeaf(directories, cwd, kind) : null;
354
+ if (walked === null) for (const directory of directories) patterns.push(slash(join(directory, "**/*.tsrx")));
355
+ else for (const file of walked) positives.add(file);
294
356
  if (patterns.length > 0) {
295
357
  const { glob } = await import("tinyglobby");
296
358
  const matches = await glob(patterns, {
@@ -320,4 +382,4 @@ function ensureSupportedOutput(format, files) {
320
382
  if (files.length > 0 && format !== "default" && format !== "json") throw new Error(`OXC for TSRX currently combines default and json lint output; ${format} is unavailable for mixed .tsrx runs`);
321
383
  }
322
384
  //#endregion
323
- export { argumentValue, canonicalToolEnvironment, discoverTsrxFiles, ensureSupportedOutput, isViteConfigPath, platformPackage, prepareVitePlusConfig, removeExplicitTsrx, replaceConfigArgument, resolveNativeBinary, resolveNativeCommand, resolvePackageBinary, runCaptured, runPassthrough };
385
+ export { argumentValue, canonicalToolEnvironment, discoverTsrxFiles, ensureSupportedOutput, isViteConfigPath, pathArguments, platformPackage, prepareVitePlusConfig, removeExplicitTsrx, replaceConfigArgument, resolveNativeBinary, resolveNativeCommand, resolvePackageBinary, runCaptured, runPassthrough };
@@ -949,7 +949,7 @@ function materializeCompatibilityProgram(program, source, filename, loose, posit
949
949
  if (value.pending != null && value.pendingKeyword == null) value.pendingKeyword = keywordSpan(source, "@pending", value.block?.end ?? value.start, value.pending?.end ?? value.end, positionAt);
950
950
  if (value.handler != null && value.handlerKeyword == null) value.handlerKeyword = keywordSpan(source, "@catch", value.pending?.end ?? value.block?.end ?? value.start, value.handler?.end ?? value.end, positionAt);
951
951
  }
952
- if (value.type === "JSXStyleElement" && typeof value.css === "string") {
952
+ if (value.type === "JSXStyleElement" && typeof value.css === "string" && value.openingElement?.selfClosing !== true) {
953
953
  const style = parse_style(value.css, {
954
954
  filename,
955
955
  line: value.openingElement?.loc?.start?.line ?? value.loc?.start?.line ?? 1,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tsrx/oxc",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "The official OXC integration for TSRX — Rust-native Oxlint and Oxfmt for .tsrx, JavaScript, TypeScript, and JSX",
5
5
  "keywords": [
6
6
  "formatter",
@@ -14,7 +14,7 @@
14
14
  "tsrx",
15
15
  "typescript"
16
16
  ],
17
- "homepage": "https://compiled.run/oxc-tsrx",
17
+ "homepage": "https://oxc.tsrx.dev",
18
18
  "bugs": {
19
19
  "url": "https://github.com/tsrx-org/oxc/issues"
20
20
  },
@@ -128,14 +128,14 @@
128
128
  "tinyglobby": "0.2.17"
129
129
  },
130
130
  "optionalDependencies": {
131
- "@tsrx/oxc-darwin-arm64": "0.9.0",
132
- "@tsrx/oxc-darwin-x64": "0.9.0",
133
- "@tsrx/oxc-linux-arm64-gnu": "0.9.0",
134
- "@tsrx/oxc-linux-arm64-musl": "0.9.0",
135
- "@tsrx/oxc-linux-x64-gnu": "0.9.0",
136
- "@tsrx/oxc-linux-x64-musl": "0.9.0",
137
- "@tsrx/oxc-win32-arm64-msvc": "0.9.0",
138
- "@tsrx/oxc-win32-x64-msvc": "0.9.0"
131
+ "@tsrx/oxc-darwin-arm64": "0.11.0",
132
+ "@tsrx/oxc-darwin-x64": "0.11.0",
133
+ "@tsrx/oxc-linux-arm64-gnu": "0.11.0",
134
+ "@tsrx/oxc-linux-arm64-musl": "0.11.0",
135
+ "@tsrx/oxc-linux-x64-gnu": "0.11.0",
136
+ "@tsrx/oxc-linux-x64-musl": "0.11.0",
137
+ "@tsrx/oxc-win32-arm64-msvc": "0.11.0",
138
+ "@tsrx/oxc-win32-x64-msvc": "0.11.0"
139
139
  },
140
140
  "engines": {
141
141
  "node": "^20.19.0 || >=22.12.0"