Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/update-typescript-eslint-8-61.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@naverpay/eslint-config': patch
'@naverpay/eslint-plugin': patch
---

Update typescript-eslint to ^8.61.1
120 changes: 120 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What this repo is

`@naverpay/code-style` is a pnpm + Turborepo monorepo that publishes NaverPay's shared
linting/formatting tooling to npm. Each package under `packages/*` is independently versioned
and published. There are two kinds of packages:

- **Built packages** (`eslint-config`, `eslint-plugin`) — real source compiled by Vite into
dual CJS/ESM bundles under `dist/`. Their `package.json` `exports` point at `./dist/cjs/...`
and `./dist/esm/...`, so **the package must be built before its exports resolve**.
- **Config-only packages** (`prettier-config`, `stylelint-config`, `biome-config`,
`oxlint-config`, `editorconfig`, `markdown-lint`, `code-style-cli`) — ship raw config files
(`.json`/`.js`) referenced directly by `main`/`exports`/`bin`. No build step, no `dist/`.

The repo **dogfoods its own packages**: the root `eslint.config.mjs` consumes
`@naverpay/eslint-config` + `@naverpay/eslint-plugin` (via `workspace:*`), and `.prettierrc` /
`.stylelintrc` extend the published configs.

## Per-package guides

Each package has its own `CLAUDE.md` with precise structure, build/test, and extension notes —
read the relevant one before working inside a package.

| Package | Kind | Guide |
| --- | --- | --- |
| `@naverpay/eslint-config` | built (Vite) | [packages/eslint-config/CLAUDE.md](./packages/eslint-config/CLAUDE.md) |
| `@naverpay/eslint-plugin` | built (Vite) | [packages/eslint-plugin/CLAUDE.md](./packages/eslint-plugin/CLAUDE.md) |
| `@naverpay/code-style-cli` | CLI, no build | [packages/code-style-cli/CLAUDE.md](./packages/code-style-cli/CLAUDE.md) |
| `@naverpay/markdown-lint` | CLI + config | [packages/markdown-lint/CLAUDE.md](./packages/markdown-lint/CLAUDE.md) |
| `@naverpay/stylelint-config` | config (rule modules) | [packages/stylelint-config/CLAUDE.md](./packages/stylelint-config/CLAUDE.md) |
| `@naverpay/prettier-config` | config-only | [packages/prettier-config/CLAUDE.md](./packages/prettier-config/CLAUDE.md) |
| `@naverpay/biome-config` | config-only | [packages/biome-config/CLAUDE.md](./packages/biome-config/CLAUDE.md) |
| `@naverpay/oxlint-config` | config-only | [packages/oxlint-config/CLAUDE.md](./packages/oxlint-config/CLAUDE.md) |
| `@naverpay/editorconfig` | config-only (copied) | [packages/editorconfig/CLAUDE.md](./packages/editorconfig/CLAUDE.md) |

## Commands

Requires Node `>=20.13.1` and pnpm `>=9.1.3` (pinned to `pnpm@9.1.3`). Always use `pnpm`.

```bash
pnpm install # also runs `lefthook install` via postinstall
pnpm build # turbo build — builds eslint-config & eslint-plugin only
pnpm test # turbo test (vitest); test depends on build, so build runs first
pnpm lint # eslint; `prelint` builds the workspace first
pnpm lint:fix
pnpm prettier # check; `prettier:fix` to write
pnpm markdownlint # `markdownlint:fix` to write
```

**Build ordering matters.** Turbo's `build` has `dependsOn: ["^build"]` and `eslint-config`
depends on `eslint-plugin`, so plugin builds first. `test`, `test:watch`, and `prelint` all
depend on `build` — a stale or missing `dist/` will produce confusing failures in lint/test.

### Running a single package's tests

Tests use **vitest** (`"test": "vitest run"` in each buildable package). Scope with Turbo:

```bash
pnpm turbo test --filter=@naverpay/eslint-plugin # one package
pnpm --filter @naverpay/eslint-plugin test # bypass turbo (no auto-build)
cd packages/eslint-plugin && pnpm vitest run lib/rules/sort-exports.test.js # one file
cd packages/eslint-plugin && pnpm vitest watch
```

Note: `eslint-config` still contains a legacy `jest.config.js`, but the active runner is vitest
(see its `test` script). `markdown-lint` has its own `vitest.config.js`.

## Package internals

### eslint-config (`packages/eslint-config`)

Flat-config (ESLint 9) presets. Root `index.js` exposes `configs.{node,react,typescript,strict,packageJson}`;
each lives in its own directory (`node/`, `react/`, `typescript/`, `strict/`, `packageJson/`, `yaml/`)
as `index.js` + `configs.js` (+ `rules/`). A separate `./custom` export
(`custom/index.js`) provides composable rule factories (e.g. `importOrder`,
`typescriptNamingConvention`) that consumers call with options. Built via Vite with two entries
(`./index.js`, `./custom/index.js`). Tests in `tests/`.

### eslint-plugin (`packages/eslint-plugin`)

Custom ESLint rules in `lib/rules/*.js`, registered in `lib/index.js`, each documented in
`docs/<rule>.md` with a colocated `*.test.js`. Current rules: `cognitive-complexity`,
`import-server-only`, `memo-react-components`, `optimize-svg-components`,
`peer-deps-in-dev-deps`, `prevent-default-import`, `sort-exports`, `svg-unique-id`. Built via
Vite from `./lib/index.js`. **When adding a rule:** add to `lib/rules/`, register in
`lib/index.js`, add `docs/<rule>.md` + tests.

### code-style-cli (`packages/code-style-cli`)

`cli.js` (bin: `code-style`) — interactive installer using `@inquirer/prompts`. Detects the
package manager from lockfiles, then installs/updates selected configs and writes their config
files. The catalog of tools (package names + config file scaffolds) lives in `configs.js`; add
new installable tools there.

The Vite builds use `@naverpay/pite`'s `createViteConfig` wrapper (see each package's
`vite.config.mjs`), which emits the dual CJS/ESM `dist/` layout.

## Releasing & changesets

Versioning/publishing is driven by **Changesets** (`baseBranch: main`, `access: public`).

- Any change to a publishable package needs a changeset: `pnpm changeset` (or use the
`naverpay-changeset:changeset` skill). A CI workflow (`changesets-detect-add.yml`) nudges PRs
that are missing one.
- The publish workflow (`release.yml`) runs on push to `main`: it builds, then publishes via
`pnpm release` (`changeset publish --directory dist`). **Canary/RC** releases are triggered by
commenting `canary-publish` / `rc-publish` (or `/canary-publish` / `/rc-publish`) on a PR.

## Conventions enforced automatically

- **Git hooks via lefthook** (`lefthook.yml`): `pre-commit` runs eslint + prettier --check +
markdownlint on staged files (parallel); `commit-msg` runs `@naverpay/commit-helper` to
validate the message.
- `pnpm.overrides` in the root `package.json` replaces `@changesets/assemble-release-plan` with
NaverPay's fork — do not remove.
- The root ESLint config enforces `@naverpay/peer-deps-in-dev-deps` on every `package.json`:
peer dependencies must also appear in devDependencies.
10 changes: 8 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"lefthook": "^1.9.3",
"prettier": "^3.2.5",
"turbo": "^2.3.3",
"typescript": "^5.2.2",
"typescript": "^6.0.0",
"vite": "^6.3.5"
},
"packageManager": "pnpm@9.1.3",
Expand All @@ -45,7 +45,13 @@
},
"pnpm": {
"overrides": {
"@changesets/cli>@changesets/assemble-release-plan": "github:NaverPayDev/changesets-assemble-release-plan"
"@changesets/cli>@changesets/assemble-release-plan": "github:NaverPayDev/changesets-assemble-release-plan",
"typescript": "$typescript"
},
"peerDependencyRules": {
"allowedVersions": {
"typescript": "6"
}
}
}
}
21 changes: 21 additions & 0 deletions packages/biome-config/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# CLAUDE.md — @naverpay/biome-config

NaverPay's Biome config. **Config-only** — the package is `biome.json` (`main`/`exports`). No build,
no tests. Peer dep: `@biomejs/biome@^2.0.6`. Consumed via `"extends": ["@naverpay/biome-config"]`.

## Scope: formatter only

By design this **replaces Prettier, not ESLint** — `linter.enabled: false`. Biome's `assist` is
**on** (so JSX props / object keys may get sorted), but `organizeImports` is deliberately **off**
because it conflicts with ESLint's `import/order`. Don't enable `organizeImports` here while
`@naverpay/eslint-config`'s import ordering is in use.

Formatter settings mirror `@naverpay/prettier-config`: 4-space indent, `lineWidth: 120`, `lf`,
single quotes, `semicolons: "asNeeded"`, `trailingCommas: "all"`, `bracketSpacing: false`,
`arrowParentheses: "always"`; JSON uses `trailingCommas: "none"`. Keep these in sync with the
Prettier/editorconfig packages when changing the house style.

## Gotchas

- The package ships **no `ignore`** — consumers exclude files via `files.includes` with `!` globs.
- The `$schema` version pinned in `biome.json` should track the Biome major in `peerDependencies`.
31 changes: 31 additions & 0 deletions packages/code-style-cli/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# CLAUDE.md — @naverpay/code-style-cli

Interactive installer that wires the other `@naverpay/*` configs into a target project. ESM, bin
`code-style` (`npx @naverpay/code-style-cli`). **No build, no tests** — `cli.js` runs directly.
Only runtime dep: `@inquirer/prompts`.

## How it works

`cli.js` flow: require a `package.json` in cwd → detect the package manager from its lockfile
(`PACKAGE_MANAGERS` in `configs.js`) → `checkbox` prompt to pick tools (already-installed ones are
labeled) → `execSync` the install command → for each picked tool, scaffold its config file
(prompting before overwriting an existing one).

`configs.js` is the catalog and the only place you edit to change behavior:

- `TOOLS` — array of `{value, packages, configFile?, ...}`. `packages` is what gets installed.
- `TOOLS_MAP` — `value → tool` lookup. `PACKAGE_MANAGERS` — lockfile + install command per pm.

A tool scaffolds its config file via exactly one of three modes:

- `configContent` — a static string written verbatim (e.g. `.prettierrc`, `.oxlintrc.json`).
- `getContent()` — computed at run time (e.g. `biome.json` reads the installed `@biomejs/biome`
version from the target's `package.json` to pin the `$schema` URL).
- `copyFrom` — copies an existing file out of `node_modules` (e.g. `.editorconfig`).

## Adding an installable tool

Append an entry to `TOOLS` in `configs.js` with its npm `packages` and, if it needs a config file,
one of the three modes above. Note the catalog also offers `oxfmt` (a formatter config scaffold)
even though there is no `@naverpay/oxfmt` package — `TOOLS` entries are not limited to this repo's
packages.
13 changes: 13 additions & 0 deletions packages/editorconfig/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# CLAUDE.md — @naverpay/editorconfig

NaverPay's shared `.editorconfig`. **Config-only** — the package is the `.editorconfig` file. No
build, no tests, and intentionally **no `main`/`exports`/`bin`**: it is consumed by copying the file
into a project, not by importing it.

Installation is by copy: `@naverpay/code-style-cli` uses its `copyFrom` mode
(`node_modules/@naverpay/editorconfig/.editorconfig`), or a user runs `cp` manually.

Current rules (`.editorconfig`): `*.{js,ts,tsx}` → utf-8, `lf`, 4-space indent, final newline,
`max_line_length = 120`, trim trailing whitespace; `*.{json,yml,yaml}` → same but no
`max_line_length`. Keep indent/width aligned with `@naverpay/prettier-config` and
`@naverpay/biome-config` when changing the house style.
41 changes: 41 additions & 0 deletions packages/eslint-config/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# CLAUDE.md — @naverpay/eslint-config

ESLint **flat-config** (ESLint 9) presets for NaverPay. ESM source, compiled by Vite into dual
CJS/ESM `dist/`. **Built package** — `package.json` `exports` point at `./dist/...`, so the
package must be built before its entry points resolve. Depends on `@naverpay/eslint-plugin`
(`workspace:*`), so the plugin builds first.

## Two entry points

`vite.config.mjs` builds exactly two entries; everything else is imported into them:

- `./index.js` → the default export `{meta, configs}`. `configs` = `{node, react, typescript, strict, packageJson}`.
- `./custom` (`custom/index.js`) → composable **rule factories** consumers call with options:
`importOrder({ruleSeverities, pathGroups})` and `typescriptNamingConvention({...})`. The naming
data lives in `custom/typescript/naming-convention.js`.

## Preset layout

Each preset is a directory exporting a flat-config array; `index.js` wires them into `configs`:

- `node/` — `js.configs.recommended` + `neostandard({noStyle: true})` + `eslint-config-prettier` +
`eslint-plugin-yml`, then `node/rules/` (`style.js`, `variable.js`, `import.js`) + Node/commonjs/jest/vitest globals.
- `typescript/` — `tseslint.config(rules, recommended, stylistic)`.
- `react/` — composes `typescript` + `react/rules/` + `yaml` + browser globals.
- `strict/` — `eslint-plugin-sonarjs` recommended + `eslint-plugin-unicorn` flat/recommended.
- `packageJson/` — `eslint-plugin-package-json` rules.
- `yaml/` — internal only (consumed by `node`/`react`), not exposed in `configs`.

**Adding a preset:** create `<name>/index.js`, import it into the root `index.js` `configs` object,
and add a `tests/<name>.test.js`. You do **not** add a Vite entry (only `./index.js` and `./custom`
are built).

## Tests

`pnpm vitest run` (depends on build). Tests in `tests/*.test.js` use the helper in
`tests/utils/index.js`: `createLinter({ruleId, config})` runs the real `ESLint` programmatically and
returns only the messages for `ruleId`; assert with `lintText(code)` → `toHaveLength(0)` for valid,
`checkErrorRule(result, ruleId)` for invalid. A legacy `jest.config.js` remains but the active
runner is vitest.

Run one file: `pnpm vitest run tests/node.test.js`.
2 changes: 1 addition & 1 deletion packages/eslint-config/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
"eslint-plugin-yml": "^1.16.0",
"globals": "^16.2.0",
"neostandard": "^0.11.9",
"typescript-eslint": "^8.58.0"
"typescript-eslint": "^8.61.1"
},
"devDependencies": {
"builtin-modules": "^4.0.0",
Expand Down
36 changes: 36 additions & 0 deletions packages/eslint-plugin/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# CLAUDE.md — @naverpay/eslint-plugin

Custom ESLint rules for NaverPay. ESM source under `lib/`, compiled by Vite (entry
`./lib/index.js`) into dual CJS/ESM `dist/`. **Built package** — exports resolve to `dist/`, so
build before use. Peer dep: `eslint >=8.57.0`.

## Structure

- `lib/index.js` — the plugin object `{meta, rules}`. Every rule is registered here under its
public name (the registration key, not the filename, is what consumers reference).
- `lib/rules/<name>.js` — one rule each. Current rules: `cognitive-complexity`,
`import-server-only`, `memo-react-components`, `optimize-svg-components`, `peer-deps-in-dev-deps`,
`prevent-default-import`, `sort-exports`, `svg-unique-id`.
- `lib/utils/` — shared AST helpers. `astParser.js` wraps `@naverpay/ast-parser`
(`getReactComponentDeclaration`) and exports node-type enums + `traverseAllNodes`. `svg/` holds
SVG transform/validation utils (used by the svg rules); also `string.js`, `object.js`,
`getIndentationLength.js`.
- `lib/constants/index.js` — shared literals (e.g. the `@autogenerated ... optimize-svg-components`
marker comment that `optimize-svg-components` writes/detects).
- `docs/<name>.md` — one doc page per rule.

## Tests

`pnpm vitest run` (depends on build). Each rule has a colocated `lib/rules/<name>.test.js` using
ESLint's `RuleTester` with `valid`/`invalid` cases. Fixable rules assert the `output`. Parsers are
explicit: `@babel/eslint-parser` for JS and `@typescript-eslint/parser` for TS — a rule that must
handle both runs two `RuleTester` instances. `lib/utils/**` has its own `*.test.js`.

Run one file: `pnpm vitest run lib/rules/sort-exports.test.js`.

## Adding a rule

1. `lib/rules/<name>.js` — export the rule object (`meta` + `create(context)`); set `meta.fixable`
if it autofixes. Reuse `lib/utils/astParser.js` for traversal rather than re-walking the AST.
2. Register it in `lib/index.js` under its public name.
3. Add `docs/<name>.md` and `lib/rules/<name>.test.js`.
2 changes: 1 addition & 1 deletion packages/eslint-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"devDependencies": {
"@babel/eslint-parser": "^7.25.9",
"@babel/preset-react": "^7.24.7",
"@typescript-eslint/parser": "^8.58.0",
"@typescript-eslint/parser": "^8.61.1",
"builtin-modules": "^4.0.0",
"eslint": "^9.17.0",
"vitest": "^2.1.5"
Expand Down
22 changes: 22 additions & 0 deletions packages/markdown-lint/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# CLAUDE.md — @naverpay/markdown-lint

NaverPay's markdownlint ruleset + a thin CLI. CommonJS, `main` is `.markdownlint.jsonc`, bin
`markdownlint`. **No build.** Published `files`: `cli.js`, `.markdownlint.jsonc`.

## Pieces

- `.markdownlint.jsonc` — the ruleset, and the package's `main`. Written as **jsonc**: starts from
`"default": false`, then turns on/configures rules individually with an explanatory comment above
each (`// MDxxx: ...`). This file is the source of truth; consumers `extends` it.
- `cli.js` — wraps `markdownlint-cli2`'s `run()` and always appends `#**/node_modules` to ignore
vendored markdown. Invoked as the `markdownlint` bin.
- `postInstall/` — `createConfigFile()` writes a `.markdownlint.jsonc` (`extends @naverpay/markdown-lint`)
at the git root if absent. **Not currently wired** as an npm `postinstall` hook in `package.json`
— it's a helper, so don't assume it runs on install.

## Tests

`pnpm vitest run` (`vitest.config.js` includes `**/*.test.js`). `index.test.js` loads the real
`.markdownlint.jsonc` with `markdownlint.readConfigSync(..., [parse])` (jsonc-parser) and runs
`markdownlint.sync` over inline good/bad strings, asserting the bad case reports the expected
`ruleNames`. **When you change a rule in `.markdownlint.jsonc`, add/adjust a matching case here.**
26 changes: 26 additions & 0 deletions packages/oxlint-config/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# CLAUDE.md — @naverpay/oxlint-config

NaverPay's [oxlint](https://oxc.rs) config (Rust-based, ESLint-compatible linter). **Config-only** —
ships the `node/` directory (`node/.oxlintrc.json`). No build, no tests. Peer dep: `oxlint@>=1.0.0`.

## Consumption gotcha — path resolution

oxlint resolves `extends` **relative to the config file's location**, not as a node module
specifier. Consumers therefore reference the full path, not the package name:

```json
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"extends": ["./node_modules/@naverpay/oxlint-config/node/.oxlintrc.json"]
}
```

This is why the package exposes a real directory (`files: ["node"]`) instead of `exports`/`main`.

## Ruleset

`node/.oxlintrc.json` sets `env` (node/commonjs/es2023) and base rules (`eqeqeq: smart`,
`no-console`, `no-param-reassign`, `no-unused-vars` with `^_` ignore patterns) plus a set of
`@typescript-eslint/*` rules. An `overrides` block for `**/*.{ts,tsx}` disables the JS-only
`no-unused-vars`/`no-undef`/`no-unused-expressions` and switches to their `@typescript-eslint`
equivalents. Edit this file to change rules; add a changeset.
13 changes: 13 additions & 0 deletions packages/prettier-config/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# CLAUDE.md — @naverpay/prettier-config

NaverPay's Prettier config. **Config-only** — the entire package is `index.json` (the `main`).
No build, no tests. Peer dep: `prettier@^2.8.8 || ^3.0.0`.

Consumed as a string reference: `"@naverpay/prettier-config"` in `.prettierrc`, or the `"prettier"`
key in a project's `package.json`.

Current settings (`index.json`): `singleQuote: true`, `semi: false`, `tabWidth: 4`,
`printWidth: 120`, `bracketSpacing: false`, `arrowParens: "always"`, `trailingComma: "all"`,
`endOfLine: "auto"`. To change the house style, edit `index.json` and add a changeset — these
values must stay aligned with `@naverpay/biome-config` (the Biome formatter that mirrors them) and
`@naverpay/editorconfig` (indent/width).
Loading
Loading