Skip to content

feat: oxlint-tailwindcss/config, the plugin for extends (#218) - #225

Merged
sergioazoc merged 1 commit into
mainfrom
feat/config-helper
Oct 9, 2026
Merged

sergioazoc merged 1 commit into
mainfrom
feat/config-helper

Conversation

@sergioazoc

Copy link
Copy Markdown
Owner

Closes #218.

Summary

A preset published as a package couldn't rely on "oxlint-tailwindcss" in jsPlugins, for two reasons:

  • oxlint resolves a plugin's bare name from where it runs, not from the config that lists it. A pnpm bin shim's NODE_PATH happens to reach a hoisted copy; a plain node …/bin/oxlint (an editor, hoist=false) doesn't.
  • oxlint rejects a relative specifier in an extended config.

oxlint-tailwindcss/config fixes that:

import { defineConfig } from 'oxlint'
import tailwindcss from 'oxlint-tailwindcss/config'

export default defineConfig({
  extends: [tailwindcss()],
  settings: { tailwindcss: { entryPoint: 'src/styles.css' } },
})
  • What tailwindcss() returns: { jsPlugins: [<absolute dist/index.mjs>], rules }, with the recommended rules at their severities. { recommended: false } registers the plugin alone.
  • It takes no settings, and throws if given one. oxlint never reads settings from an extended config, in JSON or TS (verified, and pinned as a canary). The project sets settings.tailwindcss itself.
  • Build: a second entry (src/config.ts, exports["./config"]) with nothing imported at runtime from index. No chunk is shared, and dist/index.cjs is still module.exports = plugin. The recommended map is static and a test holds it equal to each rule's meta.
  • Anchoring fix: a relative entryPoint (and no-borrowed-component-styles' components) now anchors to every config name oxlint discovers (.oxlintrc.json(c), oxlint.config.ts/.mts), not .oxlintrc.json only.
  • Floors: oxlint 1.45.0 (1.43 rejects object extends; 1.44 doesn't exist) on Node 22.18.0 (type stripping). A new CI floors leg runs the smoke's second phase there, and docs-sync holds /setup to it.

Test plan

  • New e2e config-helper, using the real oxlint run without NODE_PATH:
    • /setup's snippet (EN = ES), linted and type-checked, plus CJS and ESM consumers;
    • a shared config with the plugin only as its dependency;
    • recommended: false;
    • a nested oxlint.config.ts anchoring its entry point;
    • a setting failing where it's written;
    • the bare name alongside the helper;
    • the built exports and dist layout;
    • two canaries.
  • Loader tests for the four config names. With the old loader, four tests fail.
  • oxlint-floor-smoke.mjs: 1.43.0 skips the helper phase; 1.45.0 and 1.87.0 pass it.
  • Lint, lint:types, format:check, typecheck, build and pnpm test (3769 + 312); bench npm test. The corpus and synthetic runs are identical to the baseline. interop.mjs passes.

🤖 Generated with Claude Code

https://claude.ai/code/session_019vTh6S5A41M1HLkmk3FaM1

A preset published as a package couldn't rely on listing "oxlint-tailwindcss"
in jsPlugins: oxlint resolves a plugin's bare name from where it runs, not from
the config that lists it — a pnpm bin shim's NODE_PATH happens to reach a
hoisted copy, a plain `node …/bin/oxlint` (an editor, hoist=false) doesn't —
and it rejects a relative specifier in an extended config.

`import tailwindcss from 'oxlint-tailwindcss/config'`; `tailwindcss()` returns
`{ jsPlugins: [<absolute dist/index.mjs>], rules }` — the recommended rules at
their severities, or none with `{ recommended: false }` — for `extends` in an
oxlint.config.ts or in a shared config. It takes no settings and throws if
given one: oxlint never reads `settings` from an extended config (JSON or TS),
so an entryPoint passed through it would vanish; the project sets
settings.tailwindcss itself.

- A second build entry, src/config.ts, behind exports["./config"], importing
  nothing at runtime from index: no shared chunk, dist/index.cjs is still
  `module.exports = plugin`. The recommended rules are the static
  src/recommended.ts, held to each rule's meta.docs.recommended by a test. The
  function is exported by name and default, so the .d.cts matches the CJS
  shape; index.mjs because the bare name resolves there (both register one
  plugin).
- A relative entryPoint (and no-borrowed-component-styles' `components`)
  anchors to every config name oxlint discovers — .oxlintrc.json(c),
  oxlint.config.ts/.mts — not .oxlintrc.json only; never vite.config.*.
- Floors of oxlint.config.ts with object extends: oxlint 1.45.0 (1.43 rejects
  objects; 1.44 was never published) on Node 22.18.0 (type stripping). A new CI
  floors leg runs oxlint-floor-smoke.mjs's second phase there; docs-sync holds
  /setup to it.

Tests: e2e config-helper with the real oxlint run WITHOUT NODE_PATH — /setup's
snippet (EN = ES) linted and type-checked with CJS and ESM consumers, a shared
config with the plugin only as its dependency, recommended: false, a nested
oxlint.config.ts anchoring its entryPoint, a setting failing where it's
written, the bare name alongside, the built package's exports and layout — and
two canaries for the oxlint behaviours the helper is shaped around. Loader
tests for the four config names; an old loader fails four of them.

Docs: /setup (EN/ES) "With oxlint.config.ts, or in a shared config"; the
anchoring text in /setup, /settings, /monorepo and no-borrowed-component-
styles; package README; CLAUDE.md. Bench identical.

Claude-Session: https://claude.ai/code/session_019vTh6S5A41M1HLkmk3FaM1
@sergioazoc
sergioazoc merged commit 4049e38 into main Oct 9, 2026
12 checks passed
@sergioazoc
sergioazoc deleted the feat/config-helper branch October 9, 2026 01:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: expose a self-resolving Oxlint config for downstream presets

1 participant