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
5 changes: 5 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ jobs:
- name: Check type-aware snapshot
run: nr check:type-aware

# Same reason again. A stale word list silently narrows what
# `naming: { allowedWords: true }` lets through.
- name: Check Roblox allowed-words snapshot
run: nr check:roblox-words

- name: Build
run: nr build

Expand Down
16 changes: 10 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ files that have to sit next to the code they augment, and
`src/cli/constants-generated.ts`). Nothing in `src/generated/` is hand-edited,
and both root lint configs ignore it.

`pnpm gen` runs eight generators in `scripts/`: `typegen.ts`
`pnpm gen` runs nine generators in `scripts/`: `typegen.ts`
(`src/typegen.d.ts` - ESLint rule types and config names), `typegen-oxlint.ts`
(`src/oxlint/typegen.d.ts`, plus `src/generated/oxlint-native.ts` and
`src/generated/oxlint-capabilities.ts` - the native and jsPlugin capability sets
Expand All @@ -105,7 +105,10 @@ severities, used by the redundancy check), `versiongen.ts`
(`src/cli/constants-generated.ts`), `stylistic-gen.ts`
(`src/generated/stylistic.ts` - `@stylistic` rule names), `type-aware-gen.ts`
(`src/generated/type-aware.ts` - the `requiresTypeChecking` snapshot the
type-aware split reads) and `gen-package-extensions.ts` (the `packageExtensions`
type-aware split reads), `roblox-allowed-words-gen.ts`
(`src/generated/roblox-allowed-words.ts` - the Roblox names
`naming: { allowedWords: true }` hands to `flawless/naming-convention`, scraped
from `@rbxts/types`) and `gen-package-extensions.ts` (the `packageExtensions`
block of `pnpm-workspace.yaml`). Run it after modifying configs.

`typegen-oxlint.ts` boots the real ESLint factory to learn which rules the
Expand All @@ -115,10 +118,11 @@ the committed output changes depending on who ran it.

`typegen.ts` and `type-aware-gen.ts` share one list of config modules
(`scripts/config-factories.ts`); add new modules there or they escape both
generators. Both `gen-package-extensions.ts` and `type-aware-gen.ts` take
`--check` (`nr check:extensions`, `nr check:type-aware`), which CI runs _before_
the build - `pnpm gen` would otherwise repair a stale file in place and the
drift would never be reported.
generators. `gen-package-extensions.ts`, `type-aware-gen.ts` and
`roblox-allowed-words-gen.ts` all take `--check` (`nr check:extensions`,
`nr check:type-aware`, `nr check:roblox-words`), which CI runs _before_ the
build - `pnpm gen` would otherwise repair a stale file in place and the drift
would never be reported.

### CLI Tools

Expand Down
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -771,6 +771,49 @@ selectors are simply added. A less specific entry cannot override a more
specific default. The `overridesTypeAware` escape hatch still replaces the whole
rule if you need full control.

##### Roblox names

`strictCamelCase` and `StrictPascalCase` reject two capitals in a row, so a
`CFrame` has to be stored in `targetCframe` and the identifier stops matching
the type it holds. `allowedWords` lets listed words keep their own casing inside
a name, while the rest of the name is still checked. It is off by default:

```ts
// eslint.config.ts
import isentinel from "@isentinel/eslint-config";

export default isentinel({
naming: {
// Use ROBLOX_ALLOWED_WORDS, generated from `@rbxts/types`
allowedWords: true,
},
});
```

```ts
const targetCFrame = new CFrame(); // ok
const listLayoutUIPadding = 0; // ok
const target_CFrame = 0; // still an error
```

Pass an array to use exactly those words instead. Spread `ROBLOX_ALLOWED_WORDS`
to extend the list rather than replace it:

```ts
import isentinel, { ROBLOX_ALLOWED_WORDS } from "@isentinel/eslint-config";

export default isentinel({
naming: {
allowedWords: [...ROBLOX_ALLOWED_WORDS, "HTTP", "URL"],
},
});
```

The list reaches every selector, including your own `selectors` entries. A
selector opts out with its own `allowedWords: []`. Only the two strict formats
honour it, so it can never loosen `snake_case` or `UPPER_CASE`, and a word only
matches at a hump boundary, so it cannot split an existing hump.

#### Oxlint

The config can run alongside (or be replaced by)
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,10 @@
"build:inspector": "pnpm build && pnpm dlx @eslint/config-inspector build --config eslint-inspector.config.ts --out-dir ./.eslint-config-inspector",
"check:extensions": "node scripts/check-package-extensions.ts && node scripts/gen-package-extensions.ts --check",
"check:published": "node scripts/check-published-extensions.ts",
"check:roblox-words": "node scripts/roblox-allowed-words-gen.ts --check",
"check:type-aware": "node scripts/type-aware-gen.ts --check",
"dev": "npx @eslint/config-inspector --config eslint-inspector.config.ts",
"gen": "node scripts/typegen.ts && node scripts/typegen-oxlint.ts && node scripts/typegen-defaults.ts && node scripts/typegen-defaults-oxlint.ts && node scripts/versiongen.ts && node scripts/stylistic-gen.ts && node scripts/type-aware-gen.ts && node scripts/gen-package-extensions.ts",
"gen": "node scripts/typegen.ts && node scripts/typegen-oxlint.ts && node scripts/typegen-defaults.ts && node scripts/typegen-defaults-oxlint.ts && node scripts/versiongen.ts && node scripts/stylistic-gen.ts && node scripts/type-aware-gen.ts && node scripts/roblox-allowed-words-gen.ts && node scripts/gen-package-extensions.ts",
"postgen": "echo 'Generation complete!'",
"lint": "isentinel-lint",
"oxlint": "oxlint",
Expand Down Expand Up @@ -134,6 +135,7 @@
"@eslint/config-inspector": "catalog:dev",
"@isentinel/eslint-config": "workspace:*",
"@isentinel/tsconfig": "catalog:dev",
"@rbxts/types": "catalog:dev",
"@stylistic/eslint-plugin-migrate": "catalog:dev",
"@total-typescript/shoehorn": "catalog:dev",
"@types/node": "catalog:dev",
Expand Down
45 changes: 28 additions & 17 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ ignoreWorkspaceRootCheck: true

minimumReleaseAgeExclude:
- "@pobammer-ts/small-rules@2.14.0"
- "@rbxts/types@1.0.943"
- eslint-plugin-flawless
- eslint-plugin-jest@29.16.0

Expand Down Expand Up @@ -36,6 +37,7 @@ catalogs:
"@eslint-react/shared": 5.17.3
"@eslint/config-inspector": 3.1.0
"@isentinel/tsconfig": 1.2.0
"@rbxts/types": 1.0.943
"@stylistic/eslint-plugin-migrate": 4.4.1
"@total-typescript/shoehorn": 0.1.2
"@types/node": 24.1.0
Expand Down Expand Up @@ -91,7 +93,7 @@ catalogs:
eslint-plugin-better-max-params: 1.0.0
eslint-plugin-comment-length: 2.3.1
eslint-plugin-de-morgan: 2.1.3
eslint-plugin-flawless: 1.5.1
eslint-plugin-flawless: 1.7.0
eslint-plugin-format-lua: 2.0.0
eslint-plugin-import-lite: 0.6.0
eslint-plugin-jsdoc: 63.2.2
Expand Down
62 changes: 62 additions & 0 deletions scripts/roblox-allowed-words-gen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import fs from "node:fs/promises";
import process from "node:process";

import { deriveRobloxAllowedWords } from "./roblox-allowed-words-shared.ts";

/**
* Snapshot the Roblox names that `naming: { allowedWords: true }` hands to
* `flawless/naming-convention` at build time.
*
* The names are scraped out of `@rbxts/types`, which ships ~4MB of `.d.ts`
* across three files. Reading those on every config load - to answer a question
* whose answer only changes when the package is bumped - would cost every
* consumer far more than the snapshot does, and `@rbxts/types` is a
* devDependency here, so a consumer may not have the same version installed (or
* any). `roblox-allowed-words.spec.ts` fails when the snapshot drifts from the
* installed package.
*
* `--check` reports drift instead of writing, for the same reason
* `type-aware-gen.ts` has one: it has to run before anything that runs
* `pnpm gen`, which repairs the file in place and hides the drift.
*/

const OUTPUT = new URL("../src/generated/roblox-allowed-words.ts", import.meta.url);

/**
* Renders the snapshot, or reports the drift when checking.
*
* @returns The process exit code.
*/
async function main(): Promise<number> {
const checking = process.argv.includes("--check");
const words = await deriveRobloxAllowedWords();
const desired = [
"// Generated by scripts/roblox-allowed-words-gen.ts — do not edit.",
`export const ROBLOX_ALLOWED_WORDS: ReadonlyArray<string> = ${JSON.stringify(words, null, 2)};`,
"",
].join("\n");

// An unreadable (or absent) file can never equal `desired`, so it falls
// through to the write or the drift report.
const current = await fs.readFile(OUTPUT, "utf8").catch(() => "");
if (current === desired) {
if (!checking) {
console.log(`[roblox-allowed-words] ${words.length} words.`);
}

return 0;
}

if (checking) {
console.error(
"[roblox-allowed-words] src/generated/roblox-allowed-words.ts is out of " +
"sync with the installed @rbxts/types. Run `pnpm gen` and commit the result.",
);
return 1;
}

await fs.writeFile(OUTPUT, desired);
return 0;
}

process.exit(await main());
Loading