Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d9825b2
chore(tools): add vale-ls language server via mise
howmanysmall Oct 7, 2026
4b58d41
docs: update README and rule docs wording
howmanysmall Oct 7, 2026
f10f7a1
chore(editor): add vale prose linter to zed and ignores
howmanysmall Oct 7, 2026
9e2a7c1
chore(zed): remove vale language server settings
howmanysmall Oct 7, 2026
333227b
docs(anti-slop): reword rule rationales to drop first-person voice
howmanysmall Oct 7, 2026
d3c84cb
docs(rules): clarify wording in english rule rationales
howmanysmall Oct 7, 2026
e81ea00
docs(rules): clarify wording in general rule documentation
howmanysmall Oct 7, 2026
712c3aa
ci: install vale in checks workflow
howmanysmall Oct 7, 2026
88bebb3
docs(rules): use backticks for inline code in rule pages
howmanysmall Oct 7, 2026
2360cfb
chore(vale): expand prose linting rules and styles
howmanysmall Oct 7, 2026
b10c472
build(vale): enable Readability package and update token ignores
howmanysmall Oct 7, 2026
d978e4b
chore(vale): tune prose linting rules for markdown
howmanysmall Oct 7, 2026
a3ea09d
docs(changelog): reword release process description
howmanysmall Oct 7, 2026
a4c39bc
chore(vale): disable prose rules for release notes
howmanysmall Oct 7, 2026
a5dfae6
docs: tighten wording across rule rationale pages
howmanysmall Oct 7, 2026
5fa54a6
chore(vale): track project vocabulary and ignore synced styles
howmanysmall Oct 7, 2026
716b69d
chore(vale): accept project vocabulary and tune style rules
howmanysmall Oct 7, 2026
3635985
ci: add Vale prose linting job for markdown and docs
howmanysmall Oct 7, 2026
41bafab
docs: rewrite docs in impersonal voice
howmanysmall Oct 7, 2026
131e5b7
chore(vale): accept simple as a project vocabulary term
howmanysmall Oct 7, 2026
9c7cf4c
chore(vale): skip spelling checks for agent skill docs
howmanysmall Oct 7, 2026
f93ba56
chore(vale): disable prose lint rules for agent skills
howmanysmall Oct 7, 2026
22a02c0
docs: reflow prose and table padding in rule documentation
howmanysmall Oct 7, 2026
869fe5f
build(skills): update computed hashes for dry-refactoring and fallow
howmanysmall Oct 7, 2026
1283ee2
chore(tools): move vale-ls to top of tool list
howmanysmall Oct 7, 2026
9a9eb3f
chore(vale): extend ignore scope to docs and migrations
howmanysmall Oct 7, 2026
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
2 changes: 2 additions & 0 deletions .github/workflows/checks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ jobs:

- name: Setup toolchain
uses: $/.github/actions/setup
with:
tools: bun node pnpm vale

- name: Install dependencies
uses: $/.github/actions/install
Expand Down
27 changes: 27 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ on:
branches:
- main
paths: &ci-paths
- "**/*.markdown"
- "**/*.md"
- "**/*.mdx"
- "*.config.ts"
- ".github/actions/**"
- .github/workflows/bundle-size.yaml
Expand All @@ -14,6 +17,7 @@ on:
- .github/workflows/docs.yaml
- .github/workflows/release.yaml
- ".ox*trc.json"
- .vale.ini
- biome.jsonc
- "documentation/**"
- hk.pkl
Expand Down Expand Up @@ -45,6 +49,29 @@ jobs:
contents: read
uses: $/.github/workflows/checks.yaml

vale:
name: Vale
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Setup toolchain
uses: $/.github/actions/setup
with:
tools: vale

# Styles live in gitignored `.vale/styles`; `Packages` in `.vale.ini` is the source.
- name: Sync styles
run: vale sync

- name: Lint prose
run: vale --no-color .

workflow-security:
if: github.event_name == 'push' || github.event_name == 'pull_request'
name: Workflow Security
Expand Down
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ hk.local.pkl

.eslintcache*
.vitest
.vale/styles/*
!.vale/styles/config/
.vale/styles/config/*
!.vale/styles/config/vocabularies/
.vale/styles/config/vocabularies/*
!.vale/styles/config/vocabularies/Project/
!.vale/styles/config/vocabularies/Project/accept.txt

.agents/skills/animate-expo/
.agents/skills/animate/
Expand Down
71 changes: 69 additions & 2 deletions .vale.ini
Original file line number Diff line number Diff line change
@@ -1,6 +1,73 @@
StylesPath = .vale/styles
MinAlertLevel = warning
MinAlertLevel = suggestion
Vocab = Project

[*.md]
Packages = Google, Microsoft, RedHat, ai-tells, alex, proselint, write-good, Readability

[*.{md,mdx}]
BasedOnStyles = Vale, Google, ai-tells, alex, proselint
Vale.Spelling = NO

Microsoft.Adverbs = YES
Microsoft.Wordiness = YES
Microsoft.ComplexWords = YES

write-good.Weasel = YES

RedHat.SentenceLength = YES
RedHat.PascalCamelCase = YES
RedHat.MergeConflictMarkers = YES
RedHat.SelfReferentialText = YES

Readability.FleschKincaid = YES

[*.tsx]
BasedOnStyles = Vale, ai-tells, proselint
Vale.Spelling = NO
ai-tells.DoubleHyphen = NO

[*.mdx]
TokenIgnores = '(?:(?<!\{)(\{#[^\n}]+\})(?!`))|(?:`[^`\n]*(?:\n[^`\n]*)?`)'
Google.Semicolons = NO

[documentation/src/content/docs/rules/react/ban-react-fc.mdx]
Google.Spacing = NO

[README.md]
Google.FirstPerson = NO
Google.We = NO
write-good.Weasel = NO
RedHat.SelfReferentialText = NO

[{AGENTS,CHANGELOG,CODING_STANDARDS,GLOSSARY,THIRD-PARTY-NOTICES}.md]
BasedOnStyles = Vale
Microsoft.Adverbs = NO
Microsoft.Wordiness = NO
Microsoft.ComplexWords = NO
write-good.Weasel = NO
RedHat.SentenceLength = NO
RedHat.PascalCamelCase = NO
RedHat.SelfReferentialText = NO
Readability.FleschKincaid = NO

[documentation/src/content/releases/**/*.md]
BasedOnStyles = Vale
Microsoft.Adverbs = NO
Microsoft.Wordiness = NO
Microsoft.ComplexWords = NO
write-good.Weasel = NO
RedHat.SentenceLength = NO
RedHat.PascalCamelCase = NO
RedHat.SelfReferentialText = NO
Readability.FleschKincaid = NO

[{.agents/skills,docs,migrations}/**/*.md]
BasedOnStyles = Vale
Vale.Spelling = NO
Microsoft.Adverbs = NO
Microsoft.Wordiness = NO
Readability.FleschKincaid = NO
RedHat.PascalCamelCase = NO
RedHat.SelfReferentialText = NO
RedHat.SentenceLength = NO
write-good.Weasel = NO
4 changes: 4 additions & 0 deletions .vale/styles/config/vocabularies/Project/accept.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# React hooks, not the alex false positive.
(?i)\bhooks?\b
(?i)\bGod\b
(?i)\bsimple\b
13 changes: 11 additions & 2 deletions .zed/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,16 @@
},
"MDX": {
"format_on_save": "on",
"language_servers": ["oxfmt", "rumdl", "wakatime", "codebook", "harper-ls", "discord_presence", "..."],
"language_servers": [
"oxfmt",
"rumdl",
"wakatime",
"codebook",
"harper-ls",
"discord_presence",
"vale",
"..."
],
"prettier": { "allowed": false }
},
"Markdown": {
Expand All @@ -191,7 +200,7 @@
"language_server": { "name": "rumdl" }
},
"hard_tabs": true,
"language_servers": ["!knip", "codebook", "harper-ls", "rumdl", "wakatime"],
"language_servers": ["!knip", "codebook", "harper-ls", "rumdl", "wakatime", "vale"],
"prettier": { "allowed": false },
"show_whitespaces": "none",
"soft_wrap": "editor_width",
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
ni -D @pobammer-ts/small-rules
```

> Requires [Oxlint](https://oxc.rs) **v1.69.0+**, TypeScript **5 – <8**, and Node.js **≥20.19** or **≥22.12**.
> Requires [Oxlint](https://oxc.rs) **v1.69.0+**, TypeScript **5 - <8**, and Node.js **≥20.19** or **≥22.12**.

## Configuration

Expand All @@ -46,7 +46,7 @@ Register the plugin in your `.oxlintrc.json` and enable the rules you want:
}
```

All rules are namespaced under `small-rules/`. Pick the subset that fits your project — there is no bulk opt-in.
Enable rules as `small-rules/<rule-name>`. Pick the subset that fits your project - there is no bulk opt-in yet.

## Rules

Expand Down
6 changes: 3 additions & 3 deletions documentation/src/content/docs/changelog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import { Content as RootChangelog } from "../../../../CHANGELOG.md";

<RootChangelog />

## How Releases Work
## How releases work

`small-rules` uses [bumpp](https://github.com/antfu/bumpp) for versioning. Releases are triggered by pushing a `v*.*.*` tag to the repository.
`small-rules` uses [bumpp](https://github.com/antfu/bumpp) for versioning. Pushing a `v*.*.*` tag to the repository triggers a release.

Each release is published to NPM via Trusted Publishing (OIDC). `communique` writes the user-facing release notes, and `git-cliff` takes over if that generation fails.
Each release publishes to npm through Trusted Publishing (OIDC). `communique` writes the user-facing release notes, and `git-cliff` takes over if that generation fails.

<CardGrid>
<LinkCard
Expand Down
48 changes: 19 additions & 29 deletions documentation/src/content/docs/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ import OxlintConfigTabs from "$components/oxlint-config-tabs.astro";

## Add the plugin

I keep the plugin registration separate from the rules themselves so I can add or remove the rules I actually want. Register it in your Oxlint configuration file:

Register the plugin in your Oxlint configuration file:

<OxlintConfigTabs
Expand All @@ -21,15 +19,15 @@ Register the plugin in your Oxlint configuration file:

## Choose rule severity

I usually start new rules as warnings, then promote the ones I want CI to enforce. `error` fails the lint check, `warn` reports the problem without failing, and `off` disables the rule.
Start new rules as warnings, then promote the ones you want CI to enforce. `error` fails the lint check, `warn` reports the problem without failing, and `off` turns the rule off.

Each rule can be set to one of three severity levels:
Each rule uses one of three severity levels:

| Severity | Behavior |
| --------- | --------------------------------- |
| `"error"` | Fails the lint check |
| `"warn"` | Shows a warning but does not fail |
| `"off"` | Disables the rule |
| Severity | Behavior |
| --------- | -------------------------------- |
| `"error"` | Fails the lint check |
| `"warn"` | Shows a warning but doesn't fail |
| `"off"` | Turns the rule off |

<OxlintConfigTabs
json={`{
Expand All @@ -43,9 +41,7 @@ Each rule can be set to one of three severity levels:

## Pass rule options

Most rules do not need configuration. When one does, pass the severity first and the options object second:

Some rules accept options for customization. Pass them as an array where the first element is the severity and the second is an options object:
Some rules accept options. Pass them as an array where the first element is the severity and the second is an options object:

<OxlintConfigTabs
json={`{
Expand All @@ -71,7 +67,7 @@ Some rules accept options for customization. Pass them as an array where the fir

## Plugin settings

Some behavior cannot be expressed per rule. Oxlint exposes a top-level `settings` object for that, keyed by plugin name:
Some behavior can't live on one rule. Oxlint exposes a top-level `settings` object for that, keyed by plugin name:

<OxlintConfigTabs
json={`{
Expand All @@ -88,30 +84,28 @@ Some behavior cannot be expressed per rule. Oxlint exposes a top-level `settings
| ----------------- | -------- | -------- | -------------------------------------------------------------------------------------------------------------------- |
| `tsgolintVersion` | `string` | inferred | [require-safety-comment-for-type-assertion](/small-rules/rules/anti-slop/require-safety-comment-for-type-assertion/) |

### tsgolintVersion
### The `tsgolintVersion` setting

I use this setting to decide where a suppression comment has to sit to count as justification for a type assertion.
This setting is how the rule picks where a suppression comment has to sit to count as justification for a type assertion.

- Before `7.0.2002`, the linter reports the containing statement, so a `disable-next-line` comment above the statement covers the diagnostic.
- Before `7.0.2002`, the linter reports the containing statement, so a `disable-next-line` comment on the preceding line covers the diagnostic.
- On `7.0.2002` and later, the linter reports the `as` expression itself, so the comment has to cover the assertion line instead.

When you omit the setting, I infer the version in this order:
When you omit the setting, the version comes from this order:

1. The installed version in `node_modules/oxlint-tsgolint`
2. The catalog entry in `pnpm-workspace.yaml`
3. The version range in `package.json`

I ignore invalid values and fall back to the same inference.
Values that aren't a version string fall back to the same lookup.

<Aside type="note">
Only `small-rules/require-safety-comment-for-type-assertion` reads settings today. Nothing else in this plugin needs
it.
Only `small-rules/require-safety-comment-for-type-assertion` reads settings today. The rest of this plugin doesn't
need it.
</Aside>

## Override files

Use oxlint's `overrides` field when a rule makes sense for application code but not for tests or generated files:

Use oxlint's `overrides` field to apply different rules to different file patterns:

<OxlintConfigTabs
Expand All @@ -127,11 +121,9 @@ Use oxlint's `overrides` field to apply different rules to different file patter
}`}
/>

## Disable a rule inline
## Turn a rule off inline

Sometimes a violation is intentional. Disable only that line when possible:

You can disable rules for specific lines using directive comments:
Sometimes a violation is intentional. Turn off only that line when possible:

```ts
// oxlint-disable-next-line small-rules/no-print
Expand All @@ -151,9 +143,7 @@ print("Debug 2");

## A starting configuration

For most roblox-ts projects, I would start with a small set of rules that catch noisy output, unsafe waits, and patterns I do not want spreading through the codebase. Add the rest as you run into a reason to use them.

For most roblox-ts projects, start with this baseline:
For most roblox-ts projects, start with a small set of rules for noisy output and unsafe waits. Add the rest as you run into a reason to use them.

<OxlintConfigTabs
json={`{
Expand Down
4 changes: 2 additions & 2 deletions documentation/src/content/docs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ import { ruleFactCategories, ruleFactCounts } from "$data/rule-facts";
</div>
<div class="note-body">
<p class="home-proof-copy">
I made these rules for the roblox-ts problems I keep seeing in real code, with fixes for the repetitive
cleanup that does not need a review comment.
These rules target the roblox-ts problems that show up in real code, with fixes for the repetitive cleanup
that doesn't need a review comment.
</p>
</div>
<dl class="readouts">
Expand Down
28 changes: 14 additions & 14 deletions documentation/src/content/docs/introduction.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: "Introduction"
description: "What is small-rules and why should you use it?"
description: "What's small-rules and why should you use it?"
---

import { CardGrid, LinkCard } from "@astrojs/starlight/components";
Expand All @@ -14,38 +14,38 @@ import { ruleFactCategories, ruleFactCounts } from "$data/rule-facts";
kicker="Getting started"
/>

I made `@pobammer-ts/small-rules` for TypeScript projects that use roblox-ts and React Luau. It adds rules for the APIs and conventions that a general linter cannot understand, while still using oxlint for the ordinary JavaScript and TypeScript work.
`@pobammer-ts/small-rules` is for TypeScript projects that use roblox-ts and React Luau. It adds rules for the APIs and conventions that a general linter can't understand, while still using oxlint for the ordinary JavaScript and TypeScript work.

Use it with oxlint 1.69.0 or later, TypeScript 5 through 7, and Node.js `^20.19.0` or `>=22.12.0`.

## Why oxlint?

I chose oxlint because it is a Rust-based linter with a native plugin API. That gives me a place to put roblox-ts-specific checks without rebuilding the project around ESLint compatibility. I would not claim every rule is useful to every project, but the ones here come from patterns I actually want called out.
oxlint is a Rust-based linter with a native plugin API. That's a place for roblox-ts-specific checks without rebuilding the project around `ESLint` compatibility. Not every rule is useful to every project, but the ones here come from patterns in real projects.

## Rule Categories
## Rule categories

The rules are organized into {ruleFactCounts.totalCategories} categories:
The rules fall into {ruleFactCounts.totalCategories} categories:

<CardGrid>
{Array.from(ruleFactCategories.values()).map((category) => (
<LinkCard title={category.label} href={`/small-rules/${category.path}/`} description={category.description} />
))}
</CardGrid>

## What I care about
## What matters here

- **Use the native API:** I want these rules to run as oxlint rules, not as an ESLint-shaped layer bolted on afterward.
- **Know the platform:** `Ianitor`, `Color3`, `UDim2`, `task.wait()`, and React Luau components are not ordinary TypeScript patterns. A plugin for roblox-ts needs to understand that.
- **Keep configuration small:** Most rules should work without options. I add configuration when a project genuinely needs a choice, not to make every rule look flexible.
- **Fix the boring parts:** Where a fix is safe, I would rather let `oxlint --fix` handle it than make someone rewrite the same cleanup by hand.
- **Use the native API:** these rules run as oxlint rules, not as a layer shaped like `ESLint` and bolted on afterward.
- **Know the platform:** `Ianitor`, `Color3`, `UDim2`, `task.wait()`, and React Luau components aren't ordinary TypeScript patterns. A plugin for roblox-ts needs to understand that.
- **Small configuration:** most rules should work without options. Add configuration when a project needs a choice, not to make every rule look flexible.
- **Fix the boring parts:** where a fix is safe, let `oxlint --fix` handle it instead of making someone rewrite the same cleanup by hand.

This project overlaps with general Oxlint rules in a few places. The point is not to replace those rules. It is to cover the roblox-ts and React Luau cases they cannot reasonably know about.
This project overlaps with general Oxlint rules in a few places. It covers the roblox-ts and React Luau cases those rules can't reasonably know about.

## Compatibility

`small-rules` requires:

- `oxlint` 1.x (developed and tested against 1.71.0)
- TypeScript `>=5 <8` (for roblox-ts projects)
- `oxlint` 1.x. Developed and tested against 1.71.0
- TypeScript `>=5 <8`, for roblox-ts projects
- Node.js `^20.19.0 || >=22.12.0`
- An oxlint configuration file (`.oxlintrc.json` or `oxlint.config.ts`)
- An oxlint configuration file: `.oxlintrc.json` or `oxlint.config.ts`
Loading
Loading