Zypsy's shared configuration for JavaScript and TypeScript projects: linting with oxlint, formatting with oxfmt and a TypeScript base for Next.js apps.
| Import | What |
|---|---|
@zypsy/configs/oxlint |
oxlint config, strict and type-aware |
@zypsy/configs/oxlint-plugin |
the zypsy oxlint plugin, loaded by the oxlint config |
@zypsy/configs/oxfmt |
oxfmt config: Tailwind class sorting and import sorting |
@zypsy/configs/tsconfig/next.json |
compilerOptions for a Next.js app |
- Node 22.18 or later.
oxlint1.88 or later andoxlint-tsgolint7 for the oxlint config,oxfmt0.73 or later for the oxfmt config, TypeScript 7 for the tsconfig. They are peer dependencies, so the versions are yours to pin.
pnpm add -D @zypsy/configs oxlint oxlint-tsgolint oxfmtoxlint.config.ts:
import zypsy from "@zypsy/configs/oxlint";
import { defineConfig } from "oxlint";
export default defineConfig({
extends: [zypsy],
});Add what is specific to your project after extends, for example:
export default defineConfig({
extends: [zypsy],
ignorePatterns: ["src/generated/**"],
settings: { react: { version: "19.3.0" } },
overrides: [{ files: ["src/env.js"], env: { node: true } }],
});What the config does:
- Type-aware. The typescript-eslint
strict-type-checkedandstylistic-type-checkedrules run throughoxlint-tsgolint: floating promises, unsafeany, unnecessary assertions, deprecated APIs. - React and the React Compiler rules: rules of hooks, exhaustive dependencies, purity, immutability.
settings.react.versionis set to19.3.0; set yours if it differs. - Imports: no cycles, no duplicate imports, no
import { type A }that leaves a side-effect import behind (writeimport type { A }), no default exports (except insrc/app, stories and config files), no file extensions. - House style in
src/**:- Arrow functions only, no
functiondeclarations or expressions. - No explicit return types on arrow functions: inference is enough.
.at(0)and.at(-1)instead ofarray[0]andarray[array.length - 1], which pairs withnoUncheckedIndexedAccess.- No variable or parameter named
e: writeeventorerror.
- Arrow functions only, no
- Rules still in oxlint's nursery stay off until they are stable:
no-unnecessary-condition,prefer-optional-chain,import/export,no-undef,react/require-render-return.
oxlint has no native no-restricted-syntax, so the zypsy plugin provides it as zypsy/restricted-syntax, over oxlint's selector engine. The oxlint config already loads the plugin and uses it for the house style above. Rule options replace the shared ones rather than merge, so to ban more syntax, spread the shared selectors, exported as restrictedSyntax, in an override for the same files:
import zypsy, { restrictedSyntax } from "@zypsy/configs/oxlint";
import { defineConfig } from "oxlint";
export default defineConfig({
extends: [zypsy],
overrides: [
{
files: ["src/**/*.{ts,tsx}"],
rules: {
"zypsy/restricted-syntax": [
"error",
...restrictedSyntax,
{
selector: "TSEnumDeclaration",
message: "Use a union of string literals.",
},
],
},
},
],
});A disable comment names the rule like any other: // oxlint-disable-next-line zypsy/restricted-syntax.
oxfmt has no extends, so spread the shared config in oxfmt.config.ts:
import zypsy from "@zypsy/configs/oxfmt";
import { defineConfig } from "oxfmt";
export default defineConfig({
...zypsy,
ignorePatterns: [...(zypsy.ignorePatterns ?? []), "supabase/templates"],
});It formats at 80 columns, sorts Tailwind classes and sorts imports: builtin, external, internal (@/), then relative, with no blank lines between groups.
{
"extends": "@zypsy/configs/tsconfig/next.json",
"compilerOptions": { "paths": { "@/*": ["./src/*"] } },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules", ".next"]
}Strict, with noUncheckedIndexedAccess, verbatimModuleSyntax, checkJs and the Next.js plugin. paths, include and exclude stay in your tsconfig.json, because TypeScript resolves them relative to the file that declares them.
{
"lint": "oxlint src",
"lint:fix": "oxlint src --fix",
"format": "oxfmt",
"format:check": "oxfmt --check"
}The sources are TypeScript in src, built by vp pack into dist. pnpm prepush builds, lints, formats, type-checks and runs the smoke test, which lints a fixture through the built config and checks that the house rules fire.
Pull request titles are conventional commits, checked in CI, because the squash merge makes the title the commit message: fix: … for a patch, feat: … for a minor, feat!: … (or a BREAKING CHANGE: footer) for a breaking change, a minor while the version is below 1.0. chore:, docs:, ci: and the like release nothing.
Releases are automated with release-please. Every merge to main updates a release pull request with the next version and the changelog. Merging it tags v<version>, creates the GitHub release and publishes the package to npm with provenance. Don't edit the version or CHANGELOG.md by hand.