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
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ on:
'**.cjs',
'config/.editorconfig',
'config/eslint/**',
'scripts/lint.sh',
'scripts/lint.ts',
'scripts/lintChanged.sh',
'.watchmanconfig',
'.imgbotconfig',
Expand Down
12 changes: 12 additions & 0 deletions config/eslint/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,18 @@ const config = defineConfig([
},
},

{
// lint.ts is excluded from the root tsconfig because it needs @types/bun, so type-aware rules have to
// be pointed at the project that does own it. See scripts/tsconfig.json.
files: ['scripts/lint.ts'],
languageOptions: {
parserOptions: {
project: path.resolve(projectRoot, 'scripts/tsconfig.json'),
projectService: false,
},
},
},

{
files: ['tests/tooling/**/*.ts'],
rules: {
Expand Down
86 changes: 0 additions & 86 deletions config/eslint/eslint.seatbelt.tsv

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion contributingGuides/LINTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ Commit the updated `config/eslint/eslint.seatbelt.tsv` alongside the rename. The

## CI behavior

The [`ESLint check`](../.github/workflows/lint.yml) workflow runs `npm run lint`. In CI, `readOnly` is off (so seatbelt can write) and `SEATBELT_FROZEN=0` is exported from [`scripts/lint.sh`](../scripts/lint.sh) so GitHub Actions' auto-set `CI=true` doesn't flip seatbelt into frozen mode.
The [`ESLint check`](../.github/workflows/lint.yml) workflow runs `npm run lint`. In CI, `readOnly` is off (so seatbelt can write) and `SEATBELT_FROZEN=0` is exported from [`scripts/lint.ts`](../scripts/lint.ts) so GitHub Actions' auto-set `CI=true` doesn't flip seatbelt into frozen mode.

- **PR runs:** counts go down → passes (TSV rewrite is ephemeral and thrown away with the runner). Counts go up without `SEATBELT_INCREASE` → fails with seatbelt's "exceeds allowed count" error.
- **`push: main` runs:** same behavior, plus an extra step — if `config/eslint/eslint.seatbelt.tsv` changed, OSBotify commits the tightened baseline straight back to `main`.
Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@
"test:verbose": "TZ=utc NODE_OPTIONS=\"--experimental-vm-modules --max_old_space_size=4096\" JEST_VERBOSE=true jest",
"test:debug": "TZ=utc NODE_OPTIONS='--inspect-brk --experimental-vm-modules' jest --runInBand",
"perf-test": "NODE_OPTIONS=--experimental-vm-modules npx reassure",
"typecheck": "NODE_OPTIONS=--max_old_space_size=8192 tsc && NODE_OPTIONS=--max_old_space_size=8192 tsc -p tests/tooling/tsconfig.json && NODE_OPTIONS=--max_old_space_size=8192 tsc -p server/tsconfig.json && NODE_OPTIONS=--max_old_space_size=8192 tsc -p server/victory-chart-renderer/tsconfig.json",
"typecheck-tsgo": "tsgo --noEmit --incremental --tsBuildInfoFile tsconfig.tsgo.tsbuildinfo && tsgo --noEmit -p tests/tooling/tsconfig.json --incremental --tsBuildInfoFile tests/tooling/tsconfig.tsgo.tsbuildinfo && tsgo --noEmit -p server/tsconfig.json --incremental --tsBuildInfoFile server/tsconfig.tsgo.tsbuildinfo && tsgo --noEmit -p server/victory-chart-renderer/tsconfig.json --incremental --tsBuildInfoFile server/victory-chart-renderer/tsconfig.tsgo.tsbuildinfo",
"lint": "./scripts/lint.sh",
"typecheck": "NODE_OPTIONS=--max_old_space_size=8192 tsc && NODE_OPTIONS=--max_old_space_size=8192 tsc -p tests/tooling/tsconfig.json && NODE_OPTIONS=--max_old_space_size=8192 tsc -p server/tsconfig.json && NODE_OPTIONS=--max_old_space_size=8192 tsc -p server/victory-chart-renderer/tsconfig.json && NODE_OPTIONS=--max_old_space_size=8192 tsc -p scripts/tsconfig.json",
"typecheck-tsgo": "tsgo --noEmit --incremental --tsBuildInfoFile tsconfig.tsgo.tsbuildinfo && tsgo --noEmit -p tests/tooling/tsconfig.json --incremental --tsBuildInfoFile tests/tooling/tsconfig.tsgo.tsbuildinfo && tsgo --noEmit -p server/tsconfig.json --incremental --tsBuildInfoFile server/tsconfig.tsgo.tsbuildinfo && tsgo --noEmit -p server/victory-chart-renderer/tsconfig.json --incremental --tsBuildInfoFile server/victory-chart-renderer/tsconfig.tsgo.tsbuildinfo && tsgo --noEmit -p scripts/tsconfig.json --incremental --tsBuildInfoFile scripts/tsconfig.tsgo.tsbuildinfo",
"lint": "bun scripts/lint.ts",
"lint-changed": "./scripts/lintChanged.sh",
"lint-watch": "onchange '**/*.{js,jsx,ts,tsx,mjs,cjs}' -- ./scripts/lint.sh {{changed}}",
"lint-watch": "onchange '**/*.{js,jsx,ts,tsx,mjs,cjs}' -- bun scripts/lint.ts {{changed}}",
"eslint-report": "bun scripts/eslint-report.ts",
"knip": "KNIP=true knip --include dependencies --exclude unlisted --no-exit-code --reporter compact",
"knip:full": "KNIP=true knip --reporter compact",
Expand Down
17 changes: 17 additions & 0 deletions patches/eslint-seatbelt/details.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,20 @@
- Upstream PR/issue: https://github.com/justjake/eslint-seatbelt/pull/29
- E/App issue: N/A
- PR introducing patch: https://github.com/Expensify/App/pull/88566

### [eslint-seatbelt+0.1.3+003+readonly-type-declarations.patch](eslint-seatbelt+0.1.3+003+readonly-type-declarations.patch)

- Reason:

Patch 002 added the `readOnly` config option to the runtime and to `README.md`,
but never added it to the `SeatbeltConfig` TypeScript interface, so `SeatbeltConfig`
and the derived `SeatbeltArgs` type didn't know about it. Any consumer of
`eslint-seatbelt/api` that reads or sets `readOnly` (e.g. `scripts/lint.ts`, which
prunes baseline rows for deleted files using the same readOnly semantics as the rest
of the seatbelt baseline) failed to typecheck. This adds the missing `readOnly?: boolean`
field to both the `.d.ts` and `.d.mts` copies of the interface, matching the JSDoc
already shipped in `README.md` by patch 002.

- Upstream PR/issue: N/A (the `readOnly` option itself is an Expensify-only patch, not upstream)
- E/App issue: N/A
- PR introducing patch: https://github.com/Expensify/App/pull/98665
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
diff --git a/node_modules/eslint-seatbelt/dist/SeatbeltConfig-CvuyHBlj.d.mts b/node_modules/eslint-seatbelt/dist/SeatbeltConfig-CvuyHBlj.d.mts
index 9215119..88fa896 100644
--- a/node_modules/eslint-seatbelt/dist/SeatbeltConfig-CvuyHBlj.d.mts
+++ b/node_modules/eslint-seatbelt/dist/SeatbeltConfig-CvuyHBlj.d.mts
@@ -298,6 +298,41 @@ interface SeatbeltConfig {
* ```
*/
disable?: boolean;
+ /**
+ * When `true`, seatbelt validates error counts (still reporting increases)
+ * but never writes the seatbelt file. Keeps the worktree clean in local /
+ * editor runs; expect an authoritative updater (e.g. post-merge CI) to run
+ * with `readOnly: false`.
+ *
+ * Unlike `frozen`, does not turn decreases into errors. If both are set,
+ * `frozen` messaging is preserved and no write occurs.
+ *
+ * `SEATBELT_INCREASE` overrides this so intentional loosening is persisted.
+ *
+ * Defaults to `false`.
+ *
+ * Set via `SEATBELT_READ_ONLY` env var:
+ *
+ * ```bash
+ * SEATBELT_READ_ONLY=1 eslint
+ * ```
+ *
+ * Or in ESLint config:
+ *
+ * ```js
+ * // in eslint.config.js
+ * const config = [
+ * {
+ * settings: {
+ * seatbelt: {
+ * readOnly: !process.env.CI,
+ * }
+ * }
+ * }
+ * ]
+ * ```
+ */
+ readOnly?: boolean;
/**
* Suppress seatbelt's informational warning messages (e.g. "tend the garden",
* "thank you for fixing"). When enabled, seatbelt still downgrades errors to
diff --git a/node_modules/eslint-seatbelt/dist/SeatbeltConfig-CvuyHBlj.d.ts b/node_modules/eslint-seatbelt/dist/SeatbeltConfig-CvuyHBlj.d.ts
index 9215119..88fa896 100644
--- a/node_modules/eslint-seatbelt/dist/SeatbeltConfig-CvuyHBlj.d.ts
+++ b/node_modules/eslint-seatbelt/dist/SeatbeltConfig-CvuyHBlj.d.ts
@@ -298,6 +298,41 @@ interface SeatbeltConfig {
* ```
*/
disable?: boolean;
+ /**
+ * When `true`, seatbelt validates error counts (still reporting increases)
+ * but never writes the seatbelt file. Keeps the worktree clean in local /
+ * editor runs; expect an authoritative updater (e.g. post-merge CI) to run
+ * with `readOnly: false`.
+ *
+ * Unlike `frozen`, does not turn decreases into errors. If both are set,
+ * `frozen` messaging is preserved and no write occurs.
+ *
+ * `SEATBELT_INCREASE` overrides this so intentional loosening is persisted.
+ *
+ * Defaults to `false`.
+ *
+ * Set via `SEATBELT_READ_ONLY` env var:
+ *
+ * ```bash
+ * SEATBELT_READ_ONLY=1 eslint
+ * ```
+ *
+ * Or in ESLint config:
+ *
+ * ```js
+ * // in eslint.config.js
+ * const config = [
+ * {
+ * settings: {
+ * seatbelt: {
+ * readOnly: !process.env.CI,
+ * }
+ * }
+ * }
+ * ]
+ * ```
+ */
+ readOnly?: boolean;
/**
* Suppress seatbelt's informational warning messages (e.g. "tend the garden",
* "thank you for fixing"). When enabled, seatbelt still downgrades errors to
33 changes: 24 additions & 9 deletions scripts/checkOnyxConnectBypass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,14 @@ function findCandidateFiles(targets: string[]): string[] {
}
}

async function run(): Promise<void> {
const candidates = findCandidateFiles(process.argv.slice(2));
/**
* Checks `targets` for new Onyx.connect() ban bypasses, reporting any to stderr.
* Returns `true` if a new bypass was found (i.e. the caller should fail).
*/
async function checkOnyxConnectBypass(targets: string[]): Promise<boolean> {
const candidates = findCandidateFiles(targets);
if (candidates.length === 0) {
return;
return false;
}

const rule = await loadNoOnyxConnectRule();
Expand All @@ -93,18 +97,29 @@ async function run(): Promise<void> {
const results = await eslint.lintFiles(candidates);
const newBypasses = findNewBypasses(collectSuppressedBans(results, projectRoot));
if (newBypasses.length === 0) {
return;
return false;
}

console.error('Onyx.connect() is banned and the ban cannot be bypassed with eslint-disable. Use the useOnyx() hook to read Onyx data instead.');
console.error('New bypasses found:');
for (const bypass of newBypasses) {
console.error(` ${bypass.file}:${bypass.line}`);
}
process.exitCode = 1;
return true;
}

if (require.main === module) {
checkOnyxConnectBypass(process.argv.slice(2))
.then((failed) => {
if (!failed) {
return;
}
process.exitCode = 1;
})
.catch((error: unknown) => {
console.error(error instanceof Error ? error.message : error);
process.exitCode = 1;
});
}

run().catch((error: unknown) => {
console.error(error instanceof Error ? error.message : error);
process.exitCode = 1;
});
export default checkOnyxConnectBypass;
68 changes: 0 additions & 68 deletions scripts/lint.sh

This file was deleted.

Loading
Loading