diff --git a/.github/actions/javascript/tsconfig.json b/.github/actions/javascript/tsconfig.json new file mode 100644 index 000000000..25254e257 --- /dev/null +++ b/.github/actions/javascript/tsconfig.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "../../../tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "moduleResolution": "node" + } +} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 9226ca617..5154ef445 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -3,9 +3,13 @@ name: Lint JavaScript on: pull_request: types: [opened, synchronize] + push: + branches: [main] jobs: lint: + # os-botify commits version bumps and seatbelt tightening to main; those commits need no lint run. + if: ${{ github.actor != 'os-botify[bot]' }} runs-on: ubuntu-latest steps: # v5.0.0 @@ -21,10 +25,7 @@ jobs: - run: npm ci - - run: npm run lint - env: - CI: true - + # Runs before lint, which may rewrite the seatbelt file and would fail this diff check. - name: Verify there's no Prettier diff run: | npm run prettier -- --loglevel silent @@ -33,3 +34,42 @@ jobs: echo 'Error: Prettier diff detected! Please run `npm run prettier` and commit the changes.' exit 1 fi + + - run: npm run lint + env: + CI: true + + # On main, lint rewrites the seatbelt file when merged PRs fixed grandfathered errors. os-botify commits it back. + - name: Check for tightened seatbelt baseline + id: seatbelt + if: github.event_name == 'push' + run: | + if git diff --quiet config/oxlint/oxlint.seatbelt.tsv; then + echo "tightened=false" >> "$GITHUB_OUTPUT" + echo "::notice::oxlint.seatbelt.tsv unchanged; skipping auto-commit." + else + echo "tightened=true" >> "$GITHUB_OUTPUT" + fi + + - name: Generate os-botify token + id: token + if: steps.seatbelt.outputs.tightened == 'true' + uses: actions/create-github-app-token@9d97a4282b2c51a2f4f0465b9326399f53c890d4 + with: + app-id: ${{ secrets.OS_BOTIFY_APP_ID }} + private-key: ${{ secrets.OS_BOTIFY_PRIVATE_KEY }} + + # If another commit changed the file meanwhile, the API rejects the stale sha. The next push to main + # tightens again, so this step is allowed to fail. + - name: Auto-commit tightened seatbelt baseline + if: steps.seatbelt.outputs.tightened == 'true' + continue-on-error: true + run: | + file=config/oxlint/oxlint.seatbelt.tsv + gh api --method PUT "/repos/${{ github.repository }}/contents/$file" \ + --field message="Auto-tighten oxlint seatbelt baseline" \ + --field content="$(base64 -w0 "$file")" \ + --field branch=main \ + --field sha="$(git rev-parse "HEAD:$file")" + env: + GITHUB_TOKEN: ${{ steps.token.outputs.token }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index de38cf64a..527c38404 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,4 +31,6 @@ jobs: env: CI: true - - run: npm run test:types \ No newline at end of file + - run: npm run test:types + + - run: npm run test:lint \ No newline at end of file diff --git a/config/oxlint/lint.mjs b/config/oxlint/lint.mjs new file mode 100644 index 000000000..2d8d7ca56 --- /dev/null +++ b/config/oxlint/lint.mjs @@ -0,0 +1,164 @@ +// Runs Oxlint, applies the seatbelt (seatbelt.mjs) and prints the result. +// +// npm run lint lint the whole repo +// npm run lint -- lib/Onyx.ts tests lint the given paths +// npm run lint -- --show-warnings also print the grandfathered warnings +// npm run lint -- --format=json print a JSON report +// npm run lint -- --fix apply Oxlint's auto-fixes +import {spawnSync} from 'node:child_process'; +import path from 'node:path'; +import {fileURLToPath} from 'node:url'; +import {parseArgs} from 'node:util'; + +import {LINT_SEVERITY, applySeatbelt, resolveSeatbeltOptions} from './seatbelt.mjs'; + +const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); +// Called directly rather than through npx, which passes the whole command line to `sh -c` as one string. +const OXLINT_BIN = path.join(projectRoot, 'node_modules', '.bin', 'oxlint'); +const FATAL_EXIT_CODE = 2; +const JS_PLUGIN_CRASH_MESSAGE = 'Error running JS plugin'; + +/** + * Prints the reason, then up to 2000 characters of the output, and exits the process with the fatal exit code. + * @param {string} reason - What went wrong. + * @param {string} [output] - Output to show after the reason. + */ +function fail(reason, output = '') { + console.error([reason, output.trim().slice(0, 2000)].filter(Boolean).join('\n')); + process.exit(FATAL_EXIT_CODE); +} + +/** + * Runs the Oxlint binary and returns its stdout, stderr and exit code. Exits the process if the binary cannot start. + * @param {string[]} args - Command line arguments for Oxlint. + */ +function runOxlint(args) { + const result = spawnSync(OXLINT_BIN, args, {cwd: projectRoot, encoding: 'utf8', maxBuffer: 512 * 1024 * 1024}); + if (result.error) { + fail(`Could not run ${OXLINT_BIN}: ${result.error.message}`); + } + return {stdout: result.stdout ?? '', stderr: result.stderr ?? '', exitCode: result.status ?? FATAL_EXIT_CODE}; +} + +/** + * Oxlint's JSON report leaves out files without findings, and the seatbelt needs them to notice fixed errors. + * @param {string[]} targets - Paths or globs passed to Oxlint. + */ +function listLintedFiles(targets) { + return runOxlint(['--debug=files', ...targets]) + .stdout.split('\n') + .map((line) => line.trim()) + .filter(Boolean) + .map((file) => path.resolve(projectRoot, file)); +} + +/** + * `typescript(no-explicit-any)` becomes `typescript/no-explicit-any` and `eslint(no-undef)` becomes `no-undef`, + * the names oxlint.config uses, so baseline rows can be matched to config and disable comments by name. + * @param {string} code - Rule code from an Oxlint diagnostic. + */ +function toRuleID(code) { + const match = /^([\w@/.-]+)\((.+)\)$/.exec(code); + if (!match) { + return code; + } + return match[1] === 'eslint' ? match[2] : `${match[1]}/${match[2]}`; +} + +function toLintMessage(diagnostic) { + const span = diagnostic.labels?.[0]?.span; + const text = [diagnostic.message, diagnostic.help, diagnostic.note].filter(Boolean).join('\n'); + return { + filePath: path.resolve(projectRoot, diagnostic.filename), + ruleID: diagnostic.code ? toRuleID(diagnostic.code) : null, + severity: ['warning', 'advice'].includes(diagnostic.severity) ? LINT_SEVERITY.WARNING : LINT_SEVERITY.ERROR, + // A diagnostic without a rule is a file Oxlint could not parse. + message: diagnostic.code ? text : `Parsing error: ${text}`, + line: span?.line ?? 0, + column: span?.column ?? 0, + }; +} + +/** + * Runs Oxlint with type-aware rules on the targets and returns its findings as lint messages. + * Exits the process if Oxlint crashes or its output is unusable. + * @param {string[]} targets - Paths or globs to lint. + * @param {boolean} fix - Whether to apply Oxlint's auto-fixes. + */ +function lint(targets, fix) { + const {stdout, stderr, exitCode} = runOxlint(['--type-aware', '--format', 'json', ...(fix ? ['--fix'] : []), ...targets]); + // Oxlint exits 1 for findings. Anything higher, or no JSON at all, means it crashed or rejected the config. + const start = stdout.indexOf('{'); + const end = stdout.lastIndexOf('}'); + if (exitCode > 1 || start < 0 || end <= start) { + fail(`Oxlint failed (exit ${exitCode}).`, `${stderr}\n${stdout}`); + } + let report; + try { + report = JSON.parse(stdout.slice(start, end + 1)); + } catch { + fail('Could not parse Oxlint JSON output.', stdout); + } + if (!Array.isArray(report?.diagnostics)) { + fail('Oxlint JSON output has no `diagnostics` array.', stdout); + } + // A JS plugin crash has no rule and no location. It hides every JS plugin finding in the file, so fail the run + // instead of reporting a parse error. + const crashes = report.diagnostics.filter((diagnostic) => !diagnostic.code && (diagnostic.message.startsWith(JS_PLUGIN_CRASH_MESSAGE) || !diagnostic.labels?.length)); + if (crashes.length) { + fail( + `Oxlint reported ${crashes.length} JS plugin crash(es):`, + crashes + .slice(0, 5) + .map((diagnostic) => ` ${diagnostic.filename}: ${diagnostic.message.split('\n').slice(0, 3).join(' ')}`) + .join('\n'), + ); + } + return report.diagnostics.map(toLintMessage); +} + +/** + * Formats messages as `file:line:column` blocks followed by an error count. Warnings are listed and counted only with showWarnings. + * @param {object[]} messages - Findings after the seatbelt. + * @param {number} errorCount - Number of errors in messages. + * @param {number} warningCount - Number of warnings in messages. + * @param {boolean} showWarnings - Whether to list and count warnings. + */ +function formatStylish(messages, errorCount, warningCount, showWarnings) { + const visible = showWarnings ? messages : messages.filter((message) => message.severity === LINT_SEVERITY.ERROR); + const lines = visible.map((message) => { + const location = `${path.relative(projectRoot, message.filePath)}:${message.line}:${message.column}`; + const level = message.severity === LINT_SEVERITY.ERROR ? 'error' : 'warning'; + return `${location}\n ${level} ${message.message}${message.ruleID ? ` ${message.ruleID}` : ''}`; + }); + const summary = `${errorCount} error${errorCount === 1 ? '' : 's'}${showWarnings ? `, ${warningCount} warning${warningCount === 1 ? '' : 's'}` : ''}`; + return [...lines, summary].join('\n\n'); +} + +const {values, positionals} = parseArgs({ + allowPositionals: true, + options: { + 'show-warnings': {type: 'boolean', default: false}, + format: {type: 'string', default: 'stylish'}, + fix: {type: 'boolean', default: false}, + }, +}); +if (!['stylish', 'json'].includes(values.format)) { + fail(`Unknown format "${values.format}". Expected stylish or json.`); +} + +const targets = positionals.length ? positionals : ['.']; +const lintedFiles = listLintedFiles(targets); +if (!lintedFiles.length) { + fail(`Oxlint matched no files for: ${targets.join(' ')}`); +} + +const {messages} = applySeatbelt(lint(targets, values.fix), resolveSeatbeltOptions(projectRoot), lintedFiles); +const errorCount = messages.filter((message) => message.severity === LINT_SEVERITY.ERROR).length; +const warningCount = messages.length - errorCount; +if (values.format === 'json') { + console.log(JSON.stringify({messages, errorCount, warningCount})); +} else { + console.log(formatStylish(messages, errorCount, warningCount, values['show-warnings'])); +} +process.exitCode = errorCount > 0 ? 1 : 0; diff --git a/config/oxlint/oxlint.seatbelt.tsv b/config/oxlint/oxlint.seatbelt.tsv new file mode 100644 index 000000000..d9251b0a4 --- /dev/null +++ b/config/oxlint/oxlint.seatbelt.tsv @@ -0,0 +1,141 @@ +# seatbelt temporarily allowed errors +# docs: config/oxlint/seatbelt.mjs + +"../../buildDocs.ts" "hosted/no-extraneous-dependencies" 1 +"../../jest-test-environment.ts" "hosted/no-extraneous-dependencies" 1 +"../../lib/DevTools/RealDevTools.ts" "typescript/no-unsafe-assignment" 1 +"../../lib/DevTools/RealDevTools.ts" "typescript/no-unsafe-member-access" 1 +"../../lib/DevTools/RealDevTools.ts" "typescript/no-unsafe-type-assertion" 1 +"../../lib/Onyx.ts" "no-loop-func" 1 +"../../lib/Onyx.ts" "typescript/no-redundant-type-constituents" 3 +"../../lib/Onyx.ts" "typescript/no-unnecessary-type-assertion" 9 +"../../lib/Onyx.ts" "typescript/no-unsafe-type-assertion" 14 +"../../lib/Onyx.ts" "typescript/non-nullable-type-assertion-style" 1 +"../../lib/OnyxCache.ts" "typescript/no-redundant-type-constituents" 2 +"../../lib/OnyxKeys.ts" "typescript/no-unsafe-type-assertion" 1 +"../../lib/OnyxKeys.ts" "typescript/restrict-template-expressions" 1 +"../../lib/OnyxMerge/index.native.ts" "typescript/no-redundant-type-constituents" 2 +"../../lib/OnyxMerge/index.native.ts" "typescript/no-unsafe-type-assertion" 4 +"../../lib/OnyxMerge/index.ts" "typescript/no-redundant-type-constituents" 2 +"../../lib/OnyxMerge/index.ts" "typescript/no-unsafe-type-assertion" 2 +"../../lib/OnyxMerge/types.ts" "typescript/no-redundant-type-constituents" 2 +"../../lib/OnyxSubscriptionManager.ts" "typescript/no-unsafe-type-assertion" 4 +"../../lib/OnyxSubscriptionManager.ts" "typescript/restrict-template-expressions" 1 +"../../lib/OnyxUtils.ts" "no-use-before-define" 28 +"../../lib/OnyxUtils.ts" "typescript/no-redundant-type-constituents" 7 +"../../lib/OnyxUtils.ts" "typescript/no-unnecessary-type-assertion" 2 +"../../lib/OnyxUtils.ts" "typescript/no-unsafe-argument" 5 +"../../lib/OnyxUtils.ts" "typescript/no-unsafe-type-assertion" 23 +"../../lib/OnyxUtils.ts" "typescript/non-nullable-type-assertion-style" 1 +"../../lib/OnyxUtils.ts" "typescript/prefer-nullish-coalescing" 3 +"../../lib/StateMachine.ts" "typescript/no-unsafe-type-assertion" 2 +"../../lib/storage/__mocks__/index.ts" "typescript/no-unsafe-argument" 1 +"../../lib/storage/InstanceSync/index.web.ts" "hosted/no-types" 1 +"../../lib/storage/InstanceSync/index.web.ts" "typescript/no-unsafe-type-assertion" 1 +"../../lib/storage/InstanceSync/index.web.ts" "typescript/restrict-template-expressions" 1 +"../../lib/storage/providers/IDBKeyValProvider/index.ts" "typescript/no-unsafe-return" 1 +"../../lib/storage/providers/IDBKeyValProvider/index.ts" "typescript/no-unsafe-type-assertion" 3 +"../../lib/storage/providers/MemoryOnlyProvider.ts" "typescript/no-unsafe-type-assertion" 5 +"../../lib/storage/providers/NoopProvider.ts" "typescript/no-unsafe-type-assertion" 1 +"../../lib/storage/providers/SQLiteProvider.ts" "hosted/naming-convention" 4 +"../../lib/storage/providers/SQLiteProvider.ts" "typescript/no-unsafe-assignment" 1 +"../../lib/storage/providers/SQLiteProvider.ts" "typescript/no-unsafe-return" 2 +"../../lib/storage/providers/SQLiteProvider.ts" "typescript/no-unsafe-type-assertion" 3 +"../../lib/storage/providers/SQLiteProvider.ts" "unicorn/prefer-at" 1 +"../../lib/Str.ts" "typescript/no-unsafe-type-assertion" 1 +"../../lib/types.ts" "typescript/consistent-type-definitions" 1 +"../../lib/types.ts" "typescript/no-empty-object-type" 1 +"../../lib/types.ts" "typescript/no-restricted-types" 2 +"../../lib/useOnyx.ts" "typescript/no-unsafe-type-assertion" 2 +"../../lib/utils.ts" "hosted/naming-convention" 2 +"../../lib/utils.ts" "typescript/no-redundant-type-constituents" 2 +"../../lib/utils.ts" "typescript/no-unsafe-type-assertion" 9 +"../../tests/perf-test/Onyx.perf-test.ts" "typescript/no-unnecessary-type-assertion" 2 +"../../tests/perf-test/Onyx.perf-test.ts" "typescript/no-unsafe-type-assertion" 1 +"../../tests/perf-test/Onyx.perf-test.ts" "typescript/restrict-template-expressions" 1 +"../../tests/perf-test/OnyxCache.perf-test.ts" "typescript/no-unnecessary-type-assertion" 1 +"../../tests/perf-test/OnyxCache.perf-test.ts" "typescript/no-unsafe-assignment" 1 +"../../tests/perf-test/OnyxCache.perf-test.ts" "typescript/no-unsafe-member-access" 1 +"../../tests/perf-test/OnyxCache.perf-test.ts" "typescript/no-unsafe-type-assertion" 1 +"../../tests/perf-test/OnyxCache.perf-test.ts" "typescript/non-nullable-type-assertion-style" 1 +"../../tests/perf-test/OnyxUtils.perf-test.ts" "typescript/no-misused-promises" 1 +"../../tests/perf-test/OnyxUtils.perf-test.ts" "typescript/no-unnecessary-type-assertion" 5 +"../../tests/perf-test/OnyxUtils.perf-test.ts" "typescript/no-unsafe-type-assertion" 1 +"../../tests/perf-test/useOnyx.perf-test.tsx" "typescript/no-unnecessary-type-assertion" 1 +"../../tests/perf-test/useOnyx.perf-test.tsx" "typescript/no-unsafe-type-assertion" 4 +"../../tests/perf-test/utils.perf-test.ts" "typescript/no-unsafe-call" 1 +"../../tests/perf-test/utils.perf-test.ts" "typescript/no-unsafe-member-access" 3 +"../../tests/unit/collectionHydrationTest.ts" "typescript/no-unsafe-assignment" 3 +"../../tests/unit/collectionHydrationTest.ts" "typescript/no-unsafe-member-access" 7 +"../../tests/unit/collectionHydrationTest.ts" "unicorn/prefer-at" 3 +"../../tests/unit/DevToolsTest.ts" "typescript/no-unnecessary-type-assertion" 1 +"../../tests/unit/DevToolsTest.ts" "typescript/no-unsafe-assignment" 2 +"../../tests/unit/DevToolsTest.ts" "typescript/no-unsafe-type-assertion" 9 +"../../tests/unit/onyxCacheTest.tsx" "typescript/no-non-null-assertion" 5 +"../../tests/unit/onyxCacheTest.tsx" "typescript/no-unsafe-assignment" 8 +"../../tests/unit/onyxCacheTest.tsx" "typescript/no-unsafe-member-access" 7 +"../../tests/unit/onyxCacheTest.tsx" "typescript/no-unsafe-type-assertion" 4 +"../../tests/unit/onyxClearConcurrentWriteTest.ts" "typescript/no-unsafe-assignment" 1 +"../../tests/unit/onyxClearConcurrentWriteTest.ts" "typescript/no-unsafe-member-access" 1 +"../../tests/unit/onyxClearConcurrentWriteTest.ts" "typescript/no-unsafe-type-assertion" 1 +"../../tests/unit/onyxClearNativeStorageTest.ts" "typescript/no-unsafe-assignment" 1 +"../../tests/unit/onyxClearNativeStorageTest.ts" "typescript/no-unsafe-member-access" 1 +"../../tests/unit/onyxClearNativeStorageTest.ts" "typescript/no-unsafe-type-assertion" 4 +"../../tests/unit/onyxClearWebStorageTest.ts" "typescript/no-unnecessary-type-assertion" 4 +"../../tests/unit/onyxClearWebStorageTest.ts" "typescript/no-unsafe-assignment" 1 +"../../tests/unit/onyxClearWebStorageTest.ts" "typescript/no-unsafe-member-access" 1 +"../../tests/unit/OnyxMergeNativeTest.ts" "typescript/no-unsafe-return" 3 +"../../tests/unit/onyxMultiMergeWebStorageTest.ts" "typescript/no-unnecessary-type-assertion" 5 +"../../tests/unit/onyxMultiMergeWebStorageTest.ts" "typescript/no-unsafe-assignment" 1 +"../../tests/unit/onyxMultiMergeWebStorageTest.ts" "typescript/no-unsafe-member-access" 1 +"../../tests/unit/onyxMultiMergeWebStorageTest.ts" "typescript/no-unsafe-type-assertion" 1 +"../../tests/unit/OnyxSubscriptionManagerTest.ts" "typescript/no-unnecessary-type-assertion" 1 +"../../tests/unit/OnyxSubscriptionManagerTest.ts" "typescript/no-unsafe-member-access" 1 +"../../tests/unit/OnyxSubscriptionManagerTest.ts" "typescript/no-unsafe-type-assertion" 4 +"../../tests/unit/onyxTest.ts" "hosted/no-extraneous-dependencies" 2 +"../../tests/unit/onyxTest.ts" "typescript/no-deprecated" 2 +"../../tests/unit/onyxTest.ts" "typescript/no-unnecessary-type-assertion" 70 +"../../tests/unit/onyxTest.ts" "typescript/no-unsafe-assignment" 16 +"../../tests/unit/onyxTest.ts" "typescript/no-unsafe-call" 7 +"../../tests/unit/onyxTest.ts" "typescript/no-unsafe-member-access" 29 +"../../tests/unit/onyxTest.ts" "typescript/no-unsafe-return" 1 +"../../tests/unit/onyxTest.ts" "typescript/no-unsafe-type-assertion" 47 +"../../tests/unit/onyxUtilsTest.ts" "typescript/no-unnecessary-type-assertion" 21 +"../../tests/unit/onyxUtilsTest.ts" "typescript/no-unsafe-assignment" 9 +"../../tests/unit/onyxUtilsTest.ts" "typescript/no-unsafe-call" 2 +"../../tests/unit/onyxUtilsTest.ts" "typescript/no-unsafe-member-access" 20 +"../../tests/unit/onyxUtilsTest.ts" "typescript/no-unsafe-return" 1 +"../../tests/unit/onyxUtilsTest.ts" "typescript/no-unsafe-type-assertion" 4 +"../../tests/unit/storage/instanceSyncWebTest.ts" "typescript/no-unsafe-type-assertion" 4 +"../../tests/unit/storage/providers/createStoreTest.ts" "typescript/no-non-null-assertion" 1 +"../../tests/unit/storage/providers/createStoreTest.ts" "typescript/no-unnecessary-type-assertion" 4 +"../../tests/unit/storage/providers/createStoreTest.ts" "typescript/no-unsafe-assignment" 27 +"../../tests/unit/storage/providers/createStoreTest.ts" "typescript/no-unsafe-type-assertion" 11 +"../../tests/unit/storage/providers/createStoreTest.ts" "typescript/unbound-method" 15 +"../../tests/unit/storage/providers/IDBKeyvalProviderTest.ts" "typescript/no-unnecessary-type-assertion" 2 +"../../tests/unit/storage/providers/IDBKeyvalProviderTest.ts" "typescript/no-unsafe-member-access" 2 +"../../tests/unit/storage/providers/IDBKeyvalProviderTest.ts" "typescript/no-unsafe-type-assertion" 4 +"../../tests/unit/storage/providers/IDBKeyvalProviderTest.ts" "typescript/unbound-method" 2 +"../../tests/unit/storage/providers/SQLiteProviderTest.ts" "hosted/naming-convention" 3 +"../../tests/unit/storage/providers/SQLiteProviderTest.ts" "typescript/no-non-null-assertion" 8 +"../../tests/unit/storage/providers/SQLiteProviderTest.ts" "typescript/no-unnecessary-type-assertion" 7 +"../../tests/unit/storage/providers/SQLiteProviderTest.ts" "typescript/no-unsafe-member-access" 2 +"../../tests/unit/storage/providers/SQLiteProviderTest.ts" "typescript/no-unsafe-type-assertion" 5 +"../../tests/unit/storage/providers/SQLiteProviderTest.ts" "typescript/unbound-method" 5 +"../../tests/unit/storage/SQLiteStorageFallbackTest.ts" "typescript/no-unsafe-assignment" 1 +"../../tests/unit/storage/SQLiteStorageFallbackTest.ts" "typescript/no-unsafe-return" 1 +"../../tests/unit/storage/tryOrDegradePerformanceTest.ts" "typescript/no-unsafe-assignment" 3 +"../../tests/unit/storage/tryOrDegradePerformanceTest.ts" "typescript/no-unsafe-member-access" 2 +"../../tests/unit/storage/tryOrDegradePerformanceTest.ts" "typescript/no-unsafe-return" 1 +"../../tests/unit/storage/tryOrDegradePerformanceTest.ts" "typescript/no-unsafe-type-assertion" 1 +"../../tests/unit/useOnyxTest.ts" "typescript/no-unnecessary-type-assertion" 17 +"../../tests/unit/useOnyxTest.ts" "typescript/no-unsafe-assignment" 2 +"../../tests/unit/useOnyxTest.ts" "typescript/no-unsafe-call" 2 +"../../tests/unit/useOnyxTest.ts" "typescript/no-unsafe-member-access" 2 +"../../tests/unit/useOnyxTest.ts" "typescript/no-unsafe-type-assertion" 24 +"../../tests/unit/useOnyxTest.ts" "typescript/prefer-nullish-coalescing" 1 +"../../tests/unit/utilsTest.ts" "typescript/no-restricted-types" 1 +"../../tests/unit/utilsTest.ts" "typescript/no-unnecessary-type-assertion" 2 +"../../tests/unit/utilsTest.ts" "typescript/no-unsafe-member-access" 3 +"../../tests/unit/utilsTest.ts" "typescript/no-unsafe-type-assertion" 8 +"../../tests/utils/collections/reportActions.ts" "typescript/restrict-template-expressions" 1 diff --git a/config/oxlint/seatbelt.mjs b/config/oxlint/seatbelt.mjs new file mode 100644 index 000000000..8a1280d27 --- /dev/null +++ b/config/oxlint/seatbelt.mjs @@ -0,0 +1,517 @@ +// config/oxlint/oxlint.seatbelt.tsv records, per file and rule, how many errors are allowed. Errors up to that count +// report as warnings; one more fails the run. Counts can only go down, unless SEATBELT_INCREASE names the rule. +// +// Local runs only read the file, so fixing errors leaves the working tree clean. CI writes it, and the lint job on +// main commits the tightened file back as os-botify (.github/workflows/lint.yml). +// +// Env: +// SEATBELT_INCREASE= allow and record an increase (commit the file with your change) +// SEATBELT_READ_ONLY=0 write the file locally +// SEATBELT_FROZEN=1 fail on any difference from the file +// SEATBELT_KEEP= keep rows that reached zero +// SEATBELT_DISABLE=1 turn the seatbelt off +// SEATBELT_QUIET=1 hide the grandfathered warnings +// SEATBELT_VERBOSE=1 log every decision +import fs from 'node:fs'; +import path from 'node:path'; + +const SEATBELT_NAME = 'seatbelt'; +const SEATBELT_TSV_RELATIVE = 'config/oxlint/oxlint.seatbelt.tsv'; +const DEFAULT_FILE_HEADER = `# ${SEATBELT_NAME} temporarily allowed errors +# docs: config/oxlint/seatbelt.mjs`; + +const LINT_SEVERITY = {WARNING: 1, ERROR: 2}; + +const toRelativePath = (baseFile, filename) => (path.isAbsolute(filename) ? path.relative(path.dirname(baseFile), filename) : filename); +const toAbsolutePath = (baseFile, filename) => (path.isAbsolute(filename) ? filename : path.resolve(path.dirname(baseFile), filename)); + +/** + * Each data line holds tab-separated JSON values. Comments before the first row are the header, comments after the + * last row are trailing, and any other comment travels with the row below it when rows are sorted. + * @param {string} text - Contents of the file. + */ +function parseTSV(text) { + const rows = []; + const leading = []; + const pending = []; + let seenData = false; + for (const [index, rawLine] of text.split(/\r?\n/).entries()) { + if (!/\S/.test(rawLine)) { + continue; + } + if (/^\s*#/.test(rawLine)) { + (seenData ? pending : leading).push(rawLine); + continue; + } + const cells = rawLine.split('\t').map((cell, column) => { + try { + return JSON.parse(cell); + } catch { + throw new Error(`Invalid JSON in column ${column + 1} at line ${index + 1}`); + } + }); + rows.push({cells, comments: pending.join('\n')}); + pending.length = 0; + seenData = true; + } + return {leadingComments: leading.join('\n'), trailingComments: pending.join('\n'), rows}; +} + +/** + * Writes the header, then the rows sorted with their comments, then the trailing comments. + * @param {object} parts - The header comments, trailing comments and rows to write. + * @param {string} parts.leadingComments - Header comments. + * @param {string} parts.trailingComments - Comments after the last row. + * @param {object[]} parts.rows - Rows with their cells and comments. + */ +function serializeTSV({leadingComments, trailingComments, rows}) { + const appendBlock = (output, block) => (block ? output + (block.endsWith('\n') ? block : `${block}\n`) : output); + const items = rows.map((row) => ({comments: row.comments, encoded: `${row.cells.map((cell) => JSON.stringify(cell)).join('\t')}\n`})); + items.sort((left, right) => left.encoded.localeCompare(right.encoded)); + let output = leadingComments ? `${appendBlock('', leadingComments)}\n` : ''; + for (const item of items) { + output = appendBlock(output, item.comments) + item.encoded; + } + return appendBlock(output, trailingComments); +} + +const ruleSetHas = (ruleSet, ruleID) => ruleSet === 'all' || ruleSet.has(ruleID); + +/** + * Checks that a parsed row is a file name, a rule ID and a count, and returns it as a baseline line. + * @param {unknown[]} row - Parsed cells of one line. + * @param {number} lineNumber - Position of the row, used in error messages. + */ +function asSeatbeltLine(row, lineNumber) { + if (row.length !== 3) { + throw new Error(`Expected 3 tab-separated JSON values at line ${lineNumber}, instead have ${row.length}`); + } + const [filename, ruleID, maxErrors] = row; + if (typeof filename !== 'string' || typeof ruleID !== 'string' || typeof maxErrors !== 'number') { + throw new Error(`Expected "file""rule"count at line ${lineNumber}`); + } + return {filename, ruleID, maxErrors, comments: ''}; +} + +/** + * Reads baseline text into a Map from file name to that file's lines, plus the header and trailing comments. + * Throws on a row that is not a file name, a rule ID and a count. + * @param {string} text - Contents of the baseline file. + */ +function parseSeatbeltTSV(text) { + const parsed = parseTSV(text); + const data = new Map(); + for (const [index, row] of parsed.rows.entries()) { + const line = asSeatbeltLine(row.cells, index + 1); + line.comments = row.comments; + const fileState = data.get(line.filename) ?? {maxErrors: undefined, lines: []}; + fileState.lines.push(line); + data.set(line.filename, fileState); + } + return {data, comments: parsed.leadingComments, trailingComments: parsed.trailingComments}; +} + +/** + * Turns the baseline Map back into text. A file whose counts were updated (maxErrors) is written from those counts, + * and each row keeps its comments. + * @param {Map} data - Baseline per file name. + * @param {string} comments - Header comments. + * @param {string} [trailingComments] - Comments after the last row. + */ +function serializeSeatbeltTSV(data, comments, trailingComments = '') { + const rows = []; + for (const [filename, fileState] of data) { + if (fileState.maxErrors) { + const commentsByRule = new Map(fileState.lines.map((line) => [line.ruleID, line.comments])); + fileState.lines = [...fileState.maxErrors].map(([ruleID, maxErrors]) => ({filename, ruleID, maxErrors, comments: commentsByRule.get(ruleID) ?? ''})); + } + for (const line of fileState.lines) { + rows.push({cells: [line.filename, line.ruleID, line.maxErrors], comments: line.comments}); + } + } + return serializeTSV({leadingComments: comments, trailingComments, rows}); +} + +/** + * Returns a file's allowed error count per rule, or undefined if the baseline has no rows for it. The Map is built + * from the file's lines on the first call and kept in fileState.maxErrors, where updateMaxErrors also stores new counts. + * @param {Map} data - Baseline per file name. + * @param {string} relativeFilename - File name as written in the baseline. + */ +function getMaxErrors(data, relativeFilename) { + const fileState = data.get(relativeFilename); + if (!fileState) { + return undefined; + } + fileState.maxErrors ??= new Map(fileState.lines.map((line) => [line.ruleID, line.maxErrors])); + return fileState.maxErrors; +} + +const isCountableError = (message) => message.severity === LINT_SEVERITY.ERROR && !!message.ruleID; + +/** + * Counts the errors per rule ID. Warnings and messages without a rule are skipped. + * @param {object[]} messages - Findings of one file. + */ +function countRuleIDs(messages) { + const counts = new Map(); + for (const message of messages) { + if (isCountableError(message)) { + counts.set(message.ruleID, (counts.get(message.ruleID) ?? 0) + 1); + } + } + return counts; +} + +const pluralErrors = (count) => (count === 1 ? 'error' : 'errors'); + +/** + * Orders messages by file, then rule (messages without a rule first), then line and column. + * @param {object} a - First message. + * @param {object} b - Second message. + */ +function compareMessages(a, b) { + if (a.filePath !== b.filePath) { + return a.filePath < b.filePath ? -1 : 1; + } + if (a.ruleID !== b.ruleID) { + if (a.ruleID === null) { + return -1; + } + if (b.ruleID === null) { + return 1; + } + return a.ruleID < b.ruleID ? -1 : 1; + } + return a.line - b.line || a.column - b.column; +} + +/** + * Sorted by file, rule, line and column, so "the first N of M errors" are the same ones on every run. + * @param {object[]} messages - Findings to sort. They are copied, not changed. + */ +const canonicalizeMessages = (messages) => [...messages].sort(compareMessages); + +function verboseLog(options, makeMessage) { + if (options.verbose) { + console.error(`[${SEATBELT_NAME}]:`, makeMessage()); + } +} + +const withNote = (message, note, severity = message.severity) => ({...message, severity, message: `${message.message}\n[${SEATBELT_NAME}]: ${note}`}); + +function frozenText(seatbeltFilename, errorCount, maxErrorCount) { + const fixed = maxErrorCount - errorCount; + return `[${SEATBELT_NAME}]: SEATBELT_FROZEN: Expected ${maxErrorCount} ${pluralErrors(maxErrorCount)}, found ${errorCount}. +If you fixed ${fixed} ${fixed === 1 ? 'error' : 'errors'}, thank you, but you'll need to update the seatbelt file to match. +Try running npm run lint, then committing ${seatbeltFilename}.`; +} + +/** + * Applies a file's baseline to its messages: errors up to the allowed count for a rule become warnings, and the rest + * stay errors unless SEATBELT_INCREASE names the rule. With SEATBELT_FROZEN, a count below the baseline is an error + * too. With SEATBELT_QUIET, the warnings are dropped. + * @param {object} options - Seatbelt options. + * @param {Map} data - Baseline per file name. + * @param {string} filename - Absolute path of the file. + * @param {object[]} messages - Findings of that file. + */ +function transformMessages(options, data, filename, messages) { + const ruleToMaxErrorCount = getMaxErrors(data, toRelativePath(options.seatbeltFile, filename)); + const allowIncrease = options.allowIncreaseRules === 'all' || options.allowIncreaseRules.size > 0; + if (!ruleToMaxErrorCount && !allowIncrease) { + return messages; + } + + const ruleToErrorCount = countRuleIDs(messages); + const demoteRemaining = new Map(); + const seenVerbose = new Set(); + const logOnce = (ruleID, text) => { + if (options.verbose && !seenVerbose.has(ruleID)) { + seenVerbose.add(ruleID); + verboseLog(options, () => `${filename}: rule ${ruleID}: ${text}`); + } + }; + + return messages.flatMap((message) => { + if (!isCountableError(message)) { + return message; + } + const errorCount = ruleToErrorCount.get(message.ruleID); + const maxErrorCount = ruleToMaxErrorCount?.get(message.ruleID) ?? 0; + const allowThisIncrease = ruleSetHas(options.allowIncreaseRules, message.ruleID); + if (maxErrorCount === 0 && !allowThisIncrease) { + return message; + } + + if (errorCount > maxErrorCount) { + if (allowThisIncrease) { + const increase = errorCount - maxErrorCount; + return options.quiet ? [] : withNote(message, `SEATBELT_INCREASE: Temporarily allowing ${increase} new ${pluralErrors(increase)} of this type.`, LINT_SEVERITY.WARNING); + } + logOnce(message.ruleID, `error: ${errorCount} ${pluralErrors(errorCount)} found > max ${maxErrorCount}`); + const remaining = demoteRemaining.get(message.ruleID) ?? maxErrorCount; + if (remaining > 0) { + demoteRemaining.set(message.ruleID, remaining - 1); + return options.quiet + ? [] + : withNote( + message, + `This file is temporarily allowed to have ${maxErrorCount} ${pluralErrors(maxErrorCount)} of this type.\nPlease tend the garden by fixing if you have the time.`, + LINT_SEVERITY.WARNING, + ); + } + return withNote( + message, + `There are ${errorCount} ${pluralErrors(errorCount)} of this type, but only ${maxErrorCount} are allowed.\nRemove ${ + errorCount - maxErrorCount + } to turn these errors into warnings.`, + ); + } + + if (errorCount === maxErrorCount) { + logOnce(message.ruleID, `ok: ${errorCount} ${pluralErrors(errorCount)} found == max ${maxErrorCount}`); + return options.quiet + ? [] + : withNote( + message, + `This file is temporarily allowed to have ${errorCount} ${pluralErrors(errorCount)} of this type.\nPlease tend the garden by fixing if you have the time.`, + LINT_SEVERITY.WARNING, + ); + } + + if (options.frozen) { + // Frozen fails on any difference, so a decrease that has not reached zero still blocks the run. + return {...message, severity: LINT_SEVERITY.ERROR, message: `${message.message}\n${frozenText(options.seatbeltFile, errorCount, maxErrorCount)}`}; + } + const fixed = maxErrorCount - errorCount; + return options.quiet + ? [] + : withNote( + message, + `This file is temporarily allowed to have ${maxErrorCount} ${pluralErrors(maxErrorCount)} of this type.\nThank you for fixing ${ + fixed === 1 ? 'one' : `${fixed} errors` + }, it really helps.`, + LINT_SEVERITY.WARNING, + ); + }); +} + +/** + * Works out a file's new allowed counts from its current error counts. Counts go down when errors were fixed, go up + * only for SEATBELT_INCREASE rules, and rules that reached zero are dropped unless SEATBELT_KEEP names them. The new + * counts are stored in data unless SEATBELT_FROZEN is set. + * @param {object} options - Seatbelt options. + * @param {Map} data - Baseline per file name. + * @param {string} filename - Absolute path of the file. + * @param {Map} ruleToErrorCount - Current error count per rule in that file. + * @returns - The rules that were dropped, and whether the stored counts changed. + */ +function updateMaxErrors(options, data, filename, ruleToErrorCount) { + const removedRules = new Set(); + let changedRules = 0; + const relativeFilename = toRelativePath(options.seatbeltFile, filename); + const maxErrors = new Map(getMaxErrors(data, relativeFilename) ?? []); + + for (const [ruleID, errorCount] of ruleToErrorCount) { + const maxErrorCount = maxErrors.get(ruleID) ?? 0; + if (errorCount !== maxErrorCount && (errorCount < maxErrorCount || ruleSetHas(options.allowIncreaseRules, ruleID))) { + verboseLog(options, () => `${filename}: rule ${ruleID}: ${options.frozen ? "SEATBELT_FROZEN: didn't update" : 'update'} max errors ${maxErrorCount} -> ${errorCount}`); + maxErrors.set(ruleID, errorCount); + changedRules++; + } + } + + for (const [ruleID, maxErrorCount] of [...maxErrors]) { + if (maxErrorCount !== 0 && ruleToErrorCount.has(ruleID)) { + continue; + } + if (ruleSetHas(options.keepRules, ruleID)) { + verboseLog(options, () => `${filename}: rule ${ruleID}: SEATBELT_KEEP: didn't update max errors ${maxErrorCount} -> 0`); + continue; + } + verboseLog(options, () => `${filename}: rule ${ruleID}: ${options.frozen ? "SEATBELT_FROZEN: didn't update" : 'update'} max errors ${maxErrorCount} -> 0`); + maxErrors.delete(ruleID); + removedRules.add(ruleID); + } + + const changed = (changedRules > 0 || removedRules.size > 0) && !options.frozen; + if (changed) { + const fileState = data.get(relativeFilename); + if (fileState) { + fileState.maxErrors = maxErrors; + } else { + data.set(relativeFilename, {maxErrors, lines: []}); + } + } + return {removedRules, changed}; +} + +/** + * Creates one error per rule that still has a baseline row but no longer has any errors, which frozen mode cannot update. + * @param {string} filename - Absolute path of the file. + * @param {string} seatbeltFile - Absolute path of the baseline file. + * @param {Set} removedRules - Rules whose errors are all fixed. + * @param {Map} maxErrorsBefore - Allowed counts per rule before the run. + */ +function frozenRemovedRuleMessages(filename, seatbeltFile, removedRules, maxErrorsBefore) { + return [...removedRules].map((ruleID) => ({ + filePath: filename, + ruleID, + line: 1, + column: 0, + severity: LINT_SEVERITY.ERROR, + message: frozenText(seatbeltFile, 0, maxErrorsBefore.get(ruleID)), + })); +} + +/** + * Writes through a temp file and a rename, so an interrupted run cannot leave a half-written baseline. Writes directly if the rename crosses devices (EXDEV). + * @param {string} seatbeltFile - Absolute path of the baseline file. + * @param {string} tsv - Text to write. + */ +function writeTSVAtomically(seatbeltFile, tsv) { + const tempPath = `${seatbeltFile}.${process.pid}.${Date.now()}.tmp`; + fs.writeFileSync(tempPath, tsv); + try { + fs.renameSync(tempPath, seatbeltFile); + } catch (error) { + fs.rmSync(tempPath, {force: true}); + if (error?.code !== 'EXDEV') { + throw error; + } + fs.writeFileSync(seatbeltFile, tsv); + } +} + +const readText = (file) => { + try { + return fs.readFileSync(file, 'utf8'); + } catch { + return undefined; + } +}; + +/** + * Applies the baseline to the messages, then writes the updated baseline unless the run is read-only or frozen. + * lintedFilenames must include files without findings, so rows for fully fixed files are removed. + * @param {object[]} messages - Findings from Oxlint. + * @param {object} options - Seatbelt options. + * @param {string[]} lintedFilenames - Absolute paths of every linted file. + */ +function applySeatbelt(messages, options, lintedFilenames) { + if (options.disable) { + const existing = readText(options.seatbeltFile) ?? DEFAULT_FILE_HEADER; + return {messages, tsv: existing.endsWith('\n') || !existing ? existing : `${existing}\n`, wrote: false, changed: false}; + } + + const existingText = readText(options.seatbeltFile); + const {data, comments, trailingComments} = existingText ? parseSeatbeltTSV(existingText) : {data: new Map(), comments: DEFAULT_FILE_HEADER, trailingComments: ''}; + + const byFile = new Map(); + for (const message of canonicalizeMessages(messages)) { + const fileMessages = byFile.get(message.filePath) ?? []; + fileMessages.push(message); + byFile.set(message.filePath, fileMessages); + } + for (const filename of lintedFilenames) { + if (!byFile.has(filename)) { + byFile.set(filename, []); + } + } + + const transformed = []; + let anyChanged = false; + for (const [filename, fileMessages] of byFile) { + const maxErrorsBefore = new Map(getMaxErrors(data, toRelativePath(options.seatbeltFile, filename)) ?? []); + const after = transformMessages(options, data, filename, fileMessages); + const {removedRules, changed} = updateMaxErrors(options, data, filename, countRuleIDs(fileMessages)); + anyChanged ||= changed; + transformed.push(...after); + if (options.frozen) { + transformed.push(...frozenRemovedRuleMessages(filename, options.seatbeltFile, removedRules, maxErrorsBefore)); + } + } + + // Rows for files that no longer exist are dropped. + let pruned = 0; + for (const relativeFilename of [...data.keys()]) { + if (fs.existsSync(toAbsolutePath(options.seatbeltFile, relativeFilename))) { + continue; + } + if (options.frozen) { + verboseLog(options, () => `${relativeFilename}: SEATBELT_FROZEN: didn't remove max errors`); + continue; + } + verboseLog(options, () => `${relativeFilename}: remove max errors`); + data.delete(relativeFilename); + anyChanged = true; + pruned++; + } + + const tsv = serializeSeatbeltTSV(data, comments || DEFAULT_FILE_HEADER, trailingComments); + const shouldWrite = anyChanged && !options.frozen && !options.readOnly; + if (pruned > 0) { + console.error(`${SEATBELT_NAME}: ${shouldWrite ? 'removed' : 'would remove'} ${pruned} baseline row(s) for deleted files`); + } + if (shouldWrite) { + writeTSVAtomically(options.seatbeltFile, tsv); + } + return {messages: canonicalizeMessages(transformed), tsv, wrote: shouldWrite, changed: anyChanged}; +} + +function readBooleanEnvVar(value) { + if (value === undefined || value === '') { + return undefined; + } + return !['0', 'false', 'no'].includes(value.toLowerCase()); +} + +/** + * Reads an env var holding rule IDs split by commas or spaces. `all`, `1` and `true` mean every rule, and an unset variable gives undefined. + * @param {string | undefined} value - Value of the env var. + */ +function parseRuleSetEnvVar(value) { + if (value === undefined) { + return undefined; + } + if (!value) { + return new Set(); + } + return ['all', '1', 'true'].includes(value.toLowerCase()) ? 'all' : new Set(value.split(/[\s,]+/g).filter(Boolean)); +} + +/** + * Builds the options from the SEATBELT_* env vars. The file is read-only unless CI is set, SEATBELT_READ_ONLY says + * otherwise, or SEATBELT_INCREASE is set. + * @param {string} projectRoot - Absolute path of the repo root. + * @param {Record} [env] - Environment variables to read. + */ +function resolveSeatbeltOptions(projectRoot, env = process.env) { + const allowIncreaseRules = parseRuleSetEnvVar(env.SEATBELT_INCREASE) ?? new Set(); + const isIncreaseSet = allowIncreaseRules === 'all' || allowIncreaseRules.size > 0; + return { + seatbeltFile: path.join(projectRoot, SEATBELT_TSV_RELATIVE), + projectRoot, + disable: readBooleanEnvVar(env.SEATBELT_DISABLE) ?? false, + frozen: readBooleanEnvVar(env.SEATBELT_FROZEN) ?? false, + readOnly: isIncreaseSet ? false : readBooleanEnvVar(env.SEATBELT_READ_ONLY) ?? !readBooleanEnvVar(env.CI), + allowIncreaseRules, + keepRules: parseRuleSetEnvVar(env.SEATBELT_KEEP) ?? new Set(), + quiet: readBooleanEnvVar(env.SEATBELT_QUIET) ?? false, + verbose: readBooleanEnvVar(env.SEATBELT_VERBOSE) ?? false, + }; +} + +export { + LINT_SEVERITY, + SEATBELT_TSV_RELATIVE, + applySeatbelt, + canonicalizeMessages, + countRuleIDs, + parseSeatbeltTSV, + resolveSeatbeltOptions, + serializeSeatbeltTSV, + transformMessages, + updateMaxErrors, +}; diff --git a/config/oxlint/seatbelt.test.mjs b/config/oxlint/seatbelt.test.mjs new file mode 100644 index 000000000..8eb1ffb82 --- /dev/null +++ b/config/oxlint/seatbelt.test.mjs @@ -0,0 +1,236 @@ +// Runs on Node's test runner, not Jest: `npm run test:lint`. +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import {afterEach, describe, it} from 'node:test'; + +import {LINT_SEVERITY, applySeatbelt, canonicalizeMessages, parseSeatbeltTSV, resolveSeatbeltOptions, serializeSeatbeltTSV, transformMessages} from './seatbelt.mjs'; + +function makeOptions(overrides = {}) { + return { + seatbeltFile: '/tmp/config/oxlint/oxlint.seatbelt.tsv', + projectRoot: '/tmp', + disable: false, + frozen: false, + readOnly: true, + allowIncreaseRules: new Set(), + keepRules: new Set(), + quiet: false, + verbose: false, + ...overrides, + }; +} + +function makeMessage(ruleID, overrides = {}) { + return {filePath: '/tmp/src/file.ts', ruleID, severity: LINT_SEVERITY.ERROR, message: `Original: ${ruleID}`, line: 1, column: 1, ...overrides}; +} + +describe('canonicalizeMessages', () => { + it('sorts by filename, ruleID, line, then column', () => { + const messages = [ + makeMessage('b', {filePath: '/z.ts', line: 2, column: 1}), + makeMessage('a', {filePath: '/a.ts', line: 9, column: 1}), + makeMessage('a', {filePath: '/a.ts', line: 1, column: 9}), + makeMessage('a', {filePath: '/a.ts', line: 1, column: 1}), + ]; + assert.deepEqual( + canonicalizeMessages(messages).map((message) => `${message.filePath}:${message.ruleID}:${message.line}:${message.column}`), + ['/a.ts:a:1:1', '/a.ts:a:1:9', '/a.ts:a:9:1', '/z.ts:b:2:1'], + ); + }); +}); + +describe('transformMessages', () => { + it('demotes errors at the baseline to warnings', () => { + const {data} = parseSeatbeltTSV(`"../../src/file.ts"\t"no-console"\t2\n`); + const result = transformMessages(makeOptions(), data, '/tmp/src/file.ts', [makeMessage('no-console'), makeMessage('no-console', {line: 2})]); + assert.equal(result.length, 2); + assert.equal(result[0].severity, LINT_SEVERITY.WARNING); + assert.match(result[0].message, /tend the garden/); + }); + + it('keeps overflow errors as errors', () => { + const {data} = parseSeatbeltTSV(`"../../src/file.ts"\t"no-console"\t1\n`); + const result = transformMessages(makeOptions(), data, '/tmp/src/file.ts', [ + makeMessage('no-console', {line: 1}), + makeMessage('no-console', {line: 2}), + makeMessage('no-console', {line: 3}), + ]); + assert.equal(result.filter((message) => message.severity === LINT_SEVERITY.ERROR).length, 2); + assert.equal(result.filter((message) => message.severity === LINT_SEVERITY.WARNING).length, 1); + assert.match(result.at(-1).message, /Remove/); + }); + + it('demotes the first N overflow occurrences, not an arbitrary subset', () => { + const {data} = parseSeatbeltTSV(`"../../src/file.ts"\t"no-console"\t1\n`); + const result = transformMessages(makeOptions(), data, '/tmp/src/file.ts', [makeMessage('no-console', {line: 10}), makeMessage('no-console', {line: 20})]); + assert.equal(result[0].severity, LINT_SEVERITY.WARNING); + assert.equal(result[0].line, 10); + assert.equal(result[1].severity, LINT_SEVERITY.ERROR); + assert.equal(result[1].line, 20); + }); + + it('quiet suppresses at-max warnings but keeps overflow errors', () => { + const {data} = parseSeatbeltTSV(`"../../src/file.ts"\t"no-console"\t1\n`); + const result = transformMessages(makeOptions({quiet: true}), data, '/tmp/src/file.ts', [makeMessage('no-console', {line: 1}), makeMessage('no-console', {line: 2})]); + assert.equal(result.length, 1); + assert.equal(result[0].severity, LINT_SEVERITY.ERROR); + }); + + it('frozen keeps a partial decrease as an error rather than writing', () => { + const {data} = parseSeatbeltTSV(`"../../src/file.ts"\t"no-console"\t5\n`); + const result = transformMessages(makeOptions({frozen: true}), data, '/tmp/src/file.ts', [makeMessage('no-console'), makeMessage('no-console', {line: 2})]); + assert.equal(result[0].severity, LINT_SEVERITY.ERROR); + assert.match(result[0].message, /SEATBELT_FROZEN/); + assert.match(result[0].message, /oxlint\.seatbelt\.tsv/); + assert.doesNotMatch(result[0].message, /\/tmp\/src\/file\.ts/); + }); + + it('leaves unbaselined rules untouched', () => { + const {data} = parseSeatbeltTSV(`"../../src/file.ts"\t"no-console"\t1\n`); + assert.deepEqual(transformMessages(makeOptions(), data, '/tmp/src/file.ts', [makeMessage('no-debugger')]), [makeMessage('no-debugger')]); + }); +}); + +describe('serializeSeatbeltTSV', () => { + const rewriteThroughMaxErrors = (data) => { + for (const fileState of data.values()) { + fileState.maxErrors = new Map(fileState.lines.map((line) => [line.ruleID, line.maxErrors])); + } + }; + + it('round-trips the committed header and row format', () => { + const original = `# seatbelt temporarily allowed errors +# docs: config/oxlint/seatbelt.mjs + +"../../src/a.ts" "no-console" 1 +"../../src/b.ts" "typescript/no-deprecated" 2 +`; + const {data, comments} = parseSeatbeltTSV(original); + rewriteThroughMaxErrors(data); + assert.equal(serializeSeatbeltTSV(data, comments), original); + }); + + it('keeps per-row and trailing comments when rewriting counts', () => { + const original = `# seatbelt temporarily allowed errors + +"../../src/a.ts" "no-console" 1 +# tracked in Expensify/react-native-onyx#123 +"../../src/b.ts" "no-debugger" 2 +# leftover +`; + const {data, comments, trailingComments} = parseSeatbeltTSV(original); + rewriteThroughMaxErrors(data); + assert.equal(serializeSeatbeltTSV(data, comments, trailingComments), original); + }); +}); + +describe('applySeatbelt', () => { + const dirs = []; + afterEach(() => { + for (const dir of dirs.splice(0)) { + fs.rmSync(dir, {recursive: true, force: true}); + } + }); + + function setup(tsv, files = ['src.ts']) { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'lint-seatbelt-')); + dirs.push(dir); + for (const file of files) { + fs.writeFileSync(path.join(dir, file), 'x\n'); + } + const tsvPath = path.join(dir, 'oxlint.seatbelt.tsv'); + fs.writeFileSync(tsvPath, tsv); + return {dir, tsvPath, file: (name) => path.join(dir, name)}; + } + + it('does not write in readOnly mode when counts go down', () => { + const tsv = `"src.ts"\t"no-console"\t2\n`; + const {dir, tsvPath, file} = setup(tsv); + const result = applySeatbelt([makeMessage('no-console', {filePath: file('src.ts')})], makeOptions({seatbeltFile: tsvPath, projectRoot: dir, readOnly: true}), [file('src.ts')]); + assert.equal(result.wrote, false); + assert.equal(result.changed, true); + assert.equal(fs.readFileSync(tsvPath, 'utf8'), tsv); + }); + + it('writes a tightened TSV when readOnly is off', () => { + const {dir, tsvPath, file} = setup(`"src.ts"\t"no-console"\t2\n`); + const result = applySeatbelt([makeMessage('no-console', {filePath: file('src.ts')})], makeOptions({seatbeltFile: tsvPath, projectRoot: dir, readOnly: false}), [file('src.ts')]); + assert.equal(result.wrote, true); + assert.match(result.tsv, /"src\.ts"\t"no-console"\t1/); + assert.equal(fs.readFileSync(tsvPath, 'utf8'), result.tsv); + }); + + it('fails an increase without SEATBELT_INCREASE and does not write', () => { + const original = `"src.ts"\t"no-console"\t1\n`; + const {dir, tsvPath, file} = setup(original); + const result = applySeatbelt( + [makeMessage('no-console', {filePath: file('src.ts'), line: 1}), makeMessage('no-console', {filePath: file('src.ts'), line: 2})], + makeOptions({seatbeltFile: tsvPath, projectRoot: dir, readOnly: false}), + [file('src.ts')], + ); + assert.equal(result.wrote, false); + assert.ok(result.messages.some((message) => message.severity === LINT_SEVERITY.ERROR && message.message.includes('Remove'))); + assert.equal(fs.readFileSync(tsvPath, 'utf8'), original); + }); + + it('allows an increase when SEATBELT_INCREASE names the rule', () => { + const {dir, tsvPath, file} = setup(`"src.ts"\t"no-console"\t1\n`); + const result = applySeatbelt( + [makeMessage('no-console', {filePath: file('src.ts'), line: 1}), makeMessage('no-console', {filePath: file('src.ts'), line: 2})], + makeOptions({seatbeltFile: tsvPath, projectRoot: dir, readOnly: false, allowIncreaseRules: new Set(['no-console'])}), + [file('src.ts')], + ); + assert.equal(result.wrote, true); + assert.match(result.tsv, /"src\.ts"\t"no-console"\t2/); + assert.ok(result.messages.every((message) => message.severity === LINT_SEVERITY.WARNING)); + }); + + it('prunes rows for deleted files on the write path', () => { + const {dir, tsvPath, file} = setup(`"gone.ts"\t"no-console"\t1\n"kept.ts"\t"no-console"\t1\n`, ['kept.ts']); + const result = applySeatbelt([makeMessage('no-console', {filePath: file('kept.ts')})], makeOptions({seatbeltFile: tsvPath, projectRoot: dir, readOnly: false}), [file('kept.ts')]); + assert.equal(result.wrote, true); + assert.doesNotMatch(result.tsv, /gone\.ts/); + assert.match(result.tsv, /kept\.ts/); + }); + + it('does not prune deleted-file rows when frozen', () => { + const original = `"gone.ts"\t"no-console"\t1\n"kept.ts"\t"no-console"\t1\n`; + const {dir, tsvPath, file} = setup(original, ['kept.ts']); + const result = applySeatbelt([makeMessage('no-console', {filePath: file('kept.ts')})], makeOptions({seatbeltFile: tsvPath, projectRoot: dir, frozen: true, readOnly: false}), [ + file('kept.ts'), + ]); + assert.equal(result.wrote, false); + assert.equal(fs.readFileSync(tsvPath, 'utf8'), original); + }); + + it('names the TSV path when a frozen run drops a rule entirely', () => { + const {dir, tsvPath, file} = setup(`"kept.ts"\t"no-console"\t1\n`, ['kept.ts']); + const result = applySeatbelt([], makeOptions({seatbeltFile: tsvPath, projectRoot: dir, frozen: true, readOnly: false}), [file('kept.ts')]); + assert.ok(result.messages.some((message) => message.message.includes(tsvPath) && message.message.includes('SEATBELT_FROZEN'))); + assert.ok(!result.messages.some((message) => message.message.includes('kept.ts') && !message.message.includes(tsvPath))); + }); + + it('is a no-op when SEATBELT_DISABLE is set', () => { + const {tsvPath} = setup(`"src.ts"\t"no-console"\t1\n`); + const incoming = [makeMessage('no-console')]; + const result = applySeatbelt(incoming, makeOptions({seatbeltFile: tsvPath, disable: true}), ['/tmp/src/file.ts']); + assert.equal(result.messages, incoming); + assert.equal(result.wrote, false); + }); +}); + +describe('resolveSeatbeltOptions', () => { + it('reads only outside CI, writes in CI, and is never frozen by default', () => { + assert.equal(resolveSeatbeltOptions('/repo', {}).readOnly, true); + assert.equal(resolveSeatbeltOptions('/repo', {CI: 'true'}).readOnly, false); + assert.equal(resolveSeatbeltOptions('/repo', {CI: 'true'}).frozen, false); + }); + + it('lets SEATBELT_FROZEN freeze and SEATBELT_INCREASE force writes', () => { + assert.equal(resolveSeatbeltOptions('/repo', {SEATBELT_FROZEN: '1'}).frozen, true); + assert.equal(resolveSeatbeltOptions('/repo', {SEATBELT_READ_ONLY: '1', SEATBELT_INCREASE: 'no-console'}).readOnly, false); + assert.equal(resolveSeatbeltOptions('/repo', {SEATBELT_INCREASE: 'all'}).allowIncreaseRules, 'all'); + }); +}); diff --git a/eslint.config.mjs b/eslint.config.mjs deleted file mode 100644 index 99fb430ab..000000000 --- a/eslint.config.mjs +++ /dev/null @@ -1,141 +0,0 @@ -import expensify from 'eslint-config-expensify'; -import tsPlugin from '@typescript-eslint/eslint-plugin'; -import tsParser from '@typescript-eslint/parser'; -import prettierConfig from 'eslint-config-prettier'; - -export default [ - ...expensify, - prettierConfig, - { - ignores: ['dist/**', 'node_modules/**', '.github/**', '*.d.ts', '*.config.js', '*.config.cjs', 'tests/types/**/*.ts'], - }, - { - // Overwriting this for now because web-e will conflict with this - files: ['**/*.js', '**/*.jsx'], - settings: { - 'import/resolver': { - node: { - extensions: ['.js', '.website.js', '.desktop.js', '.native.js', '.ios.js', '.android.js', '.config.js', '.ts', '.tsx'], - }, - }, - }, - rules: { - 'react/jsx-filename-extension': [1, {extensions: ['.js']}], - 'rulesdir/no-multiple-onyx-in-file': 'off', - 'import/extensions': [ - 'error', - 'ignorePackages', - { - js: 'never', - jsx: 'never', - ts: 'never', - tsx: 'never', - }, - ], - }, - }, - { - files: ['**/*.ts', '**/*.tsx'], - plugins: { - '@typescript-eslint': tsPlugin, - }, - languageOptions: { - parser: tsParser, - parserOptions: { - project: './tsconfig.json', - }, - }, - settings: { - 'import/resolver': { - typescript: { - alwaysTryTypes: true, - project: './tsconfig.json', - }, - }, - }, - rules: { - ...tsPlugin.configs.recommended.rules, - ...tsPlugin.configs.stylistic.rules, - 'rulesdir/prefer-underscore-method': 'off', - 'react/jsx-props-no-spreading': 'off', - 'react/require-default-props': 'off', - 'react/jsx-filename-extension': ['error', {extensions: ['.tsx', '.jsx']}], - 'import/no-unresolved': 'error', - 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], - 'no-use-before-define': 'off', - '@typescript-eslint/no-use-before-define': 'off', - '@typescript-eslint/no-unused-vars': ['error', {argsIgnorePattern: '^_', caughtErrors: 'none'}], - '@typescript-eslint/consistent-type-imports': ['error', {prefer: 'type-imports'}], - '@typescript-eslint/consistent-type-exports': ['error', {fixMixedExportsWithInlineTypeSpecifier: false}], - '@typescript-eslint/no-non-null-assertion': 'off', - '@typescript-eslint/array-type': ['error', {default: 'array-simple'}], - '@typescript-eslint/consistent-type-definitions': 'off', - '@typescript-eslint/no-empty-object-type': 'off', - 'rulesdir/no-multiple-onyx-in-file': 'off', - 'valid-jsdoc': 'off', - 'rulesdir/prefer-import-module-contents': 'off', - 'es/no-optional-chaining': 'off', - 'es/no-nullish-coalescing-operators': 'off', - // Disable JSDoc type rules for TypeScript files (TypeScript provides the types) - 'jsdoc/require-param': 'off', - 'jsdoc/require-param-type': 'off', - 'jsdoc/check-param-names': 'off', - 'jsdoc/check-tag-names': 'off', - 'jsdoc/check-types': 'off', - 'no-func-assign': 'off', - 'no-loop-func': 'off', - 'no-redeclare': 'off', - '@typescript-eslint/no-redeclare': 'error', - 'import/extensions': [ - 'error', - 'ignorePackages', - { - js: 'never', - jsx: 'never', - ts: 'never', - tsx: 'never', - }, - ], - 'rulesdir/prefer-onyx-connect-in-libs': 'off', - }, - }, - { - files: ['tests/**/*.{js,jsx,ts,tsx}', 'jestSetup.js', 'lib/**/__mocks__/**/*.{js,ts}'], - languageOptions: { - globals: { - jest: 'readonly', - describe: 'readonly', - it: 'readonly', - test: 'readonly', - expect: 'readonly', - beforeEach: 'readonly', - afterEach: 'readonly', - beforeAll: 'readonly', - afterAll: 'readonly', - fail: 'readonly', - }, - }, - rules: { - '@lwc/lwc/no-async-await': 'off', - 'no-await-in-loop': 'off', - 'no-restricted-syntax': ['error', 'ForInStatement', 'LabeledStatement', 'WithStatement'], - 'import/extensions': 'off', - '@typescript-eslint/no-require-imports': 'off', - 'no-restricted-imports': 'off', - }, - }, - { - files: ['**/*.native.ts', '**/*.native.tsx'], - rules: { - '@typescript-eslint/no-require-imports': 'off', - }, - }, - { - files: ['lib/storage/providers/MemoryOnlyProvider.ts'], - languageOptions: { - globals: { - jest: 'readonly', - }, - }, - }, -]; diff --git a/lib/Logger.ts b/lib/Logger.ts index 922b1dd0b..615853e25 100644 --- a/lib/Logger.ts +++ b/lib/Logger.ts @@ -7,7 +7,6 @@ type LogData = { }; type LoggerCallback = (data: LogData) => void; -// eslint-disable-next-line @typescript-eslint/no-empty-function let logger: LoggerCallback = () => {}; /** diff --git a/lib/OnyxCache.ts b/lib/OnyxCache.ts index 96df4627e..8e4a26cbe 100644 --- a/lib/OnyxCache.ts +++ b/lib/OnyxCache.ts @@ -250,7 +250,7 @@ class OnyxCache { const affectedCollections = new Set(); // Use for-in loop to avoid an unnecessary array allocation from Object.keys() - // eslint-disable-next-line no-restricted-syntax, guard-for-in + // oxlint-disable-next-line core/no-restricted-syntax, guard-for-in for (const key in data) { const value = data[key]; this.addKey(key); @@ -321,7 +321,7 @@ class OnyxCache { const affectedCollections = new Set(); // Use for-in loop to avoid an unnecessary array allocation from Object.entries() - // eslint-disable-next-line no-restricted-syntax, guard-for-in + // oxlint-disable-next-line core/no-restricted-syntax, guard-for-in for (const key in data) { const value = data[key]; this.addKey(key); @@ -545,7 +545,7 @@ class OnyxCache { // We can't rely on count comparison alone — if one key is removed and another added, // the counts match but the snapshot content is different. if (!hasMemberChanges && previousSnapshot) { - // eslint-disable-next-line no-restricted-syntax + // oxlint-disable-next-line core/no-restricted-syntax for (const key in previousSnapshot) { if (!(key in members)) { hasMemberChanges = true; diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index ed557f585..806eeaa46 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -91,7 +91,6 @@ let deferredInitTask = createDeferredTask(); * to be distinct from real values, not unique per subscription, so one module-level instance * is reused by every connection. */ -// eslint-disable-next-line rulesdir/no-negated-variables const NOT_DELIVERED = Symbol('NOT_DELIVERED'); // Collection member IDs that Onyx should silently ignore across all operations — reads, writes, cache, and subscriber diff --git a/lib/types/modules/react-native-nitro-sqlite.d.ts b/lib/types/modules/react-native-nitro-sqlite.d.ts index 27cb3ffc2..154b85ce9 100644 --- a/lib/types/modules/react-native-nitro-sqlite.d.ts +++ b/lib/types/modules/react-native-nitro-sqlite.d.ts @@ -1,9 +1,4 @@ -// Expose the native-free subpaths to TypeScript's legacy Node module resolver. +// Expose the native-free errors subpath to TypeScript's legacy Node module resolver (used by tsconfig.build.json). declare module 'react-native-nitro-sqlite/errors' { export {NitroSQLiteError, NitroSQLiteExceptionType} from 'react-native-nitro-sqlite'; } - -declare module 'react-native-nitro-sqlite/mock' { - // eslint-disable-next-line import/no-unresolved -- Legacy Node type resolution uses this declaration file outside package exports. - export * from 'react-native-nitro-sqlite/lib/typescript/commonjs/mock'; -} diff --git a/lib/utils.ts b/lib/utils.ts index 164ae4792..41f432057 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -212,7 +212,7 @@ function isEmptyObject(obj: T | EmptyValue): obj is EmptyValue { } // Use for-in loop to avoid an unnecessary array allocation from Object.keys() - // eslint-disable-next-line no-restricted-syntax + // oxlint-disable-next-line core/no-restricted-syntax for (const key in obj) { if (Object.hasOwn(obj, key)) { return false; @@ -246,7 +246,7 @@ function needsNormalization(value: unknown): boolean { } // Use for-in loop to avoid an unnecessary array allocation from Object.keys() - // eslint-disable-next-line no-restricted-syntax, guard-for-in + // oxlint-disable-next-line core/no-restricted-syntax, guard-for-in for (const key in value) { if (key === ONYX_INTERNALS__REPLACE_OBJECT_MARK) { return true; @@ -288,7 +288,7 @@ function removeNestedNullValues | null>(value: let hasChanged = false; const result: Record = {}; - // eslint-disable-next-line no-restricted-syntax, guard-for-in + // oxlint-disable-next-line core/no-restricted-syntax, guard-for-in for (const key in value) { const propertyValue = value[key]; diff --git a/oxlint.config.mts b/oxlint.config.mts new file mode 100644 index 000000000..c7351457c --- /dev/null +++ b/oxlint.config.mts @@ -0,0 +1,129 @@ +// Rules come from eslint-config-expensify's presets via buildOxlintConfig. The expensify preset is not used. Existing +// findings are baselined in config/oxlint/oxlint.seatbelt.tsv. The rest of this file is Onyx's own ignores, settings and +// overrides. +import {buildOxlintConfig, getPresetRuleEntry} from 'eslint-config-expensify/oxlint'; + +// The jest preset applies to Onyx's test and mock files only. +const JEST_FILES = ['tests/**', 'lib/**/__mocks__/**', 'jestSetup.js', 'jest-sequencer.js', 'jest-test-environment.ts']; + +// Onyx's tsconfig rejects a standalone top-level await (TS1378), so the preset rule entries are awaited inline below. +export default await buildOxlintConfig({ + presets: ['browser', 'react', 'typescript', {preset: 'jest', files: JEST_FILES}, 'scripts', 'prettier'], + // Keep oxlint's built-in exhaustive-deps instead of the hosted copy. + nativeRules: ['react-hooks/exhaustive-deps'], + ignorePatterns: ['dist/**', 'node_modules/**', '**/*.d.ts', '**/*.config.js', '**/*.config.cjs', 'tests/types/**/*.ts', 'cpp/**', 'bench-results/**', '.github/**'], + settings: { + 'import/resolver': { + typescript: { + alwaysTryTypes: true, + project: './tsconfig.json', + }, + }, + }, + overrides: [ + { + files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], + rules: { + 'hosted/prop-types': 'off', + 'hosted/require-default-props': 'off', + }, + }, + { + files: ['**/*.ts', '**/*.tsx'], + rules: { + // oxlint has no import resolution rule of its own, so the hosted one checks imports in TS files. + 'hosted/no-unresolved': 'error', + // The typescript preset's options, plus a `_` prefix for parameters kept on purpose. An override replaces + // the whole setting, so it starts from the preset's. oxlint's no-unused-vars needs + // reportVarsOnlyUsedAsTypes to report what typescript-eslint's does. + 'no-unused-vars': [ + 'error', + { + ...(await getPresetRuleEntry<[Record]>('typescript', '@typescript-eslint/no-unused-vars'))[1], + argsIgnorePattern: '^_', + reportVarsOnlyUsedAsTypes: true, + }, + ], + }, + }, + // Node tooling that runs on the current Node, so the old-browser `es/*` rules do not apply. `.mjs` imports need + // their extension in Node, and the lint CLI prints its result with console. + { + files: ['oxlint.config.mts', 'config/oxlint/**/*'], + env: { + node: true, + }, + rules: { + 'es/no-nullish-coalescing-operators': 'off', + 'es/no-optional-chaining': 'off', + 'import/extensions': 'off', + 'no-console': 'off', + }, + }, + // The presets turn these on only for TypeScript files, and the config file is TypeScript: its rule names are + // quoted keys, and it imports eslint-config-expensify, a devDependency. + { + files: ['oxlint.config.mts'], + rules: { + 'hosted/naming-convention': 'off', + 'hosted/no-extraneous-dependencies': 'off', + }, + }, + { + files: ['.prettierrc.cjs', 'jest-sequencer.js', 'jestSetup.js', 'jest-test-environment.ts', 'buildDocs.ts'], + env: { + node: true, + }, + }, + // The typescript preset's naming-convention, plus Onyx key strings (underscores) and numeric collection IDs as + // object keys in tests. An override replaces the whole setting, so it starts from the preset's. + { + files: ['tests/**/*'], + rules: { + 'hosted/naming-convention': [ + ...(await getPresetRuleEntry('typescript', '@typescript-eslint/naming-convention')), + { + selector: 'objectLiteralProperty', + format: null, + filter: { + regex: '_', + match: true, + }, + }, + { + selector: 'objectLiteralProperty', + format: null, + filter: { + regex: '^[0-9]+$', + match: true, + }, + }, + ], + }, + }, + { + files: JEST_FILES, + rules: { + 'import/extensions': 'off', + }, + }, + { + files: ['lib/storage/providers/MemoryOnlyProvider.ts'], + env: { + jest: true, + }, + }, + { + files: ['tests/**/*Test.ts', 'tests/perf-test/**/*'], + rules: { + 'typescript/dot-notation': 'off', + }, + }, + { + files: ['tests/perf-test/**/*'], + rules: { + 'unicorn/prefer-at': 'off', + }, + }, + ], +}); diff --git a/package-lock.json b/package-lock.json index 359c3c8f9..4fd64b6c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "@jest/globals": "^29.7.0", "@lwc/eslint-plugin-lwc": "^3.5.0", "@ngneat/falso": "^7.3.0", + "@oxlint/migrate": "1.92.0", "@react-native-community/eslint-config": "^3.2.0", "@react-native/babel-preset": "0.76.3", "@react-native/polyfills": "^2.0.0", @@ -43,12 +44,12 @@ "better-sqlite3": "^12.11.1", "date-fns": "^4.1.0", "eslint": "^9.39.2", - "eslint-config-expensify": "2.0.108", - "eslint-config-prettier": "^9.1.0", + "eslint-config-expensify": "github:callstack-internal/eslint-config-expensify#c36e57c42863272f60c06b9b2d1173c863df30f0", "eslint-import-resolver-typescript": "^4.4.4", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-react": "^7.37.5", + "eslint-plugin-rulesdir": "^0.2.2", "fake-indexeddb": "^6.2.5", "idb-keyval": "^6.2.1", "jest": "^29.7.0", @@ -56,6 +57,8 @@ "jest-environment-jsdom": "^29.7.0", "jsdoc-to-markdown": "^7.1.0", "nodemon": "^3.0.3", + "oxlint": "1.92.0", + "oxlint-tsgolint": "7.0.2005", "prettier": "^2.8.8", "prop-types": "^15.7.2", "react": "18.2.0", @@ -2534,9 +2537,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { @@ -3316,6 +3319,19 @@ "eslint": "^9" } }, + "node_modules/@lwc/eslint-plugin-lwc/node_modules/globals": { + "version": "15.14.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz", + "integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -3466,111 +3482,938 @@ "dev": true, "license": "MIT" }, - "node_modules/@octokit/plugin-paginate-rest": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.2.tgz", - "integrity": "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ==", + "node_modules/@octokit/plugin-paginate-rest": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.2.tgz", + "integrity": "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^12.6.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^20.0.0" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.4.1.tgz", + "integrity": "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^12.6.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^20.0.0" + } + }, + "node_modules/@octokit/request": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz", + "integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^9.0.6", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz", + "integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@oxc-parser/binding-android-arm-eabi": { + "version": "0.156.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.156.0.tgz", + "integrity": "sha512-RMq7DKNzEp1T51QKNLTn0NryNEt2IYfh7mZBlIdi2Ju52ltOEDMeIXv0XBpA6dL7j7ChjlcovtGcc1Jv4qMipg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-android-arm64": { + "version": "0.156.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.156.0.tgz", + "integrity": "sha512-mwkWKhOBpmh8fWc3m4MiLsmEAtx0YuHmvyo9ff557cnb3QZ2kt2eauSz75wCGIpiZG3gUsJ4WwzEjm4R4/BHig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-arm64": { + "version": "0.156.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.156.0.tgz", + "integrity": "sha512-6pGR5fUszM9M4eBBapqpGjCRJolFwuCldx8bRQjqXyM09cQysBTWVF2Ttg4UNJVxYSaw5RRINbveCOOCrsuGvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-x64": { + "version": "0.156.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.156.0.tgz", + "integrity": "sha512-M9QghL7uCAqMKjYJv1rDGblfEvkeQu3QzTPd7Uwo16gG3uO7m9ikVQ9OL8FZTnKzU4vbCEby6kTlEkqN+JeEMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-freebsd-x64": { + "version": "0.156.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.156.0.tgz", + "integrity": "sha512-lX0ANxSwAOrpOT7NHB+YsrBqDJ8sOVon5m7GS9eQtkuUeNzQMMGpFsJLsStawTbrdwOF5KDVxgPWcKmt/DIZUQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { + "version": "0.156.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.156.0.tgz", + "integrity": "sha512-iPgYpasRcqk7X3TGncQg7B+6RotDvG8weug0UrgF+FXLf4KmaNDyT91BKONlqbHcNgK9Nryp7AEZGuQn32fP6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { + "version": "0.156.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.156.0.tgz", + "integrity": "sha512-sJqJDEZevtkEfoxrXfaRTkSt6Itckr549cIfBbs11UgXv8bFyqQsXTQa6iKm8mTiWYmCDDFJf7TLnqxcbqzPOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-gnu": { + "version": "0.156.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.156.0.tgz", + "integrity": "sha512-mrQs5OdRiqKskRuWEORPKT6YGIFhrpVvCvL9hqVaIh60EzgR4qjPvSsV6hWZcOfyWHBTbdEvfnDJPh/eQlxPRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-musl": { + "version": "0.156.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.156.0.tgz", + "integrity": "sha512-ZtPqgCU3+wpU7dvX7YbaEiw71r0UdguzuPhcx4PIEZSs4XFqc6F5wXCtNdb6M7m3DMErlxrwosryoXrzYOY/Gg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-ppc64-gnu": { + "version": "0.156.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.156.0.tgz", + "integrity": "sha512-ZuGRT/adReZZJn3PYNneb1HQIOH0iIZ2fzFgC1/XJYpMQ5KsRKmWIEzy3wEFsUwIOTomCmnmTZ/zbFYo9z9VeA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-gnu": { + "version": "0.156.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.156.0.tgz", + "integrity": "sha512-T+iWCeAOoEAJpHqr6/pogHIv98g9p8Ctccysm/h962hG4ayX5OL2c0Ih4oYe+JuFndLXy/OASvwMlTPe76sIiw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-musl": { + "version": "0.156.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.156.0.tgz", + "integrity": "sha512-wL55bI2kVKfwdxScbw60zVgzAqbQbmE5zpSPWPZoZ9RSG8egm6ITE/UWE5mDWlglxz1QElJCcUbpaJCtHD11BQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-s390x-gnu": { + "version": "0.156.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.156.0.tgz", + "integrity": "sha512-+D+JilhLqwrDLODYEl4qtovVwgxKALb23FkAWBiPvgmUWYgjCV83KKuwwxZJM8GCdKlNA1vt3iKtow8HUFjXtg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-gnu": { + "version": "0.156.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.156.0.tgz", + "integrity": "sha512-jRfwpN4xKrsEpC+SgrUPY5k0mINYZn7aLrt22I3uQFoumiSgHgsLYgS9f84ntbkvSydH4HiwGuDa2b1XCHaJzw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-musl": { + "version": "0.156.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.156.0.tgz", + "integrity": "sha512-P/ajnypgLmxmuRRyCIZcp2o4lOwmCaXYH5+eqDTcCW+MCTI+csKSw/Zc6GB67jZoVa4ey3q1jXEt7S60adBEMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-openharmony-arm64": { + "version": "0.156.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.156.0.tgz", + "integrity": "sha512-WSN6rQZZc9KqgZ4lMfjVDvqKhVOCjey/YEZIPisGXmwBqwgWOzru207dyiH50LHcf1n9LO2oceGk/id6RL9v0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-arm64-msvc": { + "version": "0.156.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.156.0.tgz", + "integrity": "sha512-l21Tz6tjraGBC3JefVeJZ9pojD0fCiB62PydAv+i9nrqJX7z/pDmyDNYLamDr/PRcQ4UQbFwkSnz7NKBB9UMLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-ia32-msvc": { + "version": "0.156.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.156.0.tgz", + "integrity": "sha512-CUq0ZFl/RODqPiIgfNNSamkpTj5x8AjBTkR/y1DNVIQiTGnmWqjHcn3o08ZW3q/LgkI4g9G9g6UirmmQFvuOZw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-x64-msvc": { + "version": "0.156.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.156.0.tgz", + "integrity": "sha512-dcpx5Y1x2v/cs7U/q3SZMvmxfyXjVvqC86jMuw2ZZUbHDd7W2cW7uSMQfg3GLVaE68Su3H9xd/Pc4v6QNyeSRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.156.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.156.0.tgz", + "integrity": "sha512-1Duyg48TdJNdyhEB8Xn1L36bT9Pv6GUtlKjHxOys8xkaF/rQq3XJE+bAurSK/AgIz5mGFsb77cn9Pb20+WCScw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@oxlint-tsgolint/darwin-arm64": { + "version": "7.0.2005", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-arm64/-/darwin-arm64-7.0.2005.tgz", + "integrity": "sha512-VqDZxcZVjlonNLuuvraVNgDQt+1ndwa4yJo8OqsK4HYh/61ba9IlMStCVfi/aaIqRoDhNbAKC8gdcTrXqnb+Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxlint-tsgolint/darwin-x64": { + "version": "7.0.2005", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-x64/-/darwin-x64-7.0.2005.tgz", + "integrity": "sha512-4kUfLRNYoVpv0Tm3jPYrvxyfOc/LmdvFC3pKZ4tL6K7lA7K/VS81SAtmxRzbYXYSyb3SBWhwsuX+df4neqzxxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxlint-tsgolint/linux-arm64": { + "version": "7.0.2005", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/linux-arm64/-/linux-arm64-7.0.2005.tgz", + "integrity": "sha512-rhUNcPsBlKvhXtrbhbBUidtRBRdJ8fbEHN9H5Q5N59gby18ruGuDA7KUxl7oRnUzp797uykYQhkBDm+0KpfK/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxlint-tsgolint/linux-x64": { + "version": "7.0.2005", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/linux-x64/-/linux-x64-7.0.2005.tgz", + "integrity": "sha512-tKs9yinDQZOHygM0e8EJnjTU3mzJtdfkOZP6Udu82iLMePUSHOhTJ9zzU/mSd1hQxURI4LTRRiFhfzC+PzwdNA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxlint-tsgolint/win32-arm64": { + "version": "7.0.2005", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/win32-arm64/-/win32-arm64-7.0.2005.tgz", + "integrity": "sha512-VYPnu1gjfbSv+ROhuyNfMs382D4zQU2ZRc8Uc3nuEZo1k2kf+1KfTrg4hrlWh73h9GI+dUWyo+DYCQzUBlPwcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxlint-tsgolint/win32-x64": { + "version": "7.0.2005", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/win32-x64/-/win32-x64-7.0.2005.tgz", + "integrity": "sha512-ekU89cTGy9fpQzuvxIsFHrnoP8XoFjGIRgPEW6eqgY2NYjGy9pwpjF79o04WXRMJFKRrcaVqkZXUUBdLsqpQDw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.92.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.92.0.tgz", + "integrity": "sha512-k3Sd4ixQfiOrybv1gT+AMI81HSdtAFsiCFx+xXxWdFkmKW2HNrSiOfwn20o5nl/ggwbzU1oNLDeU/fRPinTVZQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.92.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.92.0.tgz", + "integrity": "sha512-OZzgEuiTSG11QkBWdXEguiLOT6lL09KtfcC/BmvUQWBAjU5eVyBxIAsmWAXv8L85/eT6RE0GBEWTEFBAbYE74g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.92.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.92.0.tgz", + "integrity": "sha512-We6XS7tIqxgUoany6uyGjj7mNsh/mwf4Gt9gUzhxG65NKo2Ow9s6tDfrVuI0FVg8n3YmpW5PBx80qFxiLwAfsA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.92.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.92.0.tgz", + "integrity": "sha512-jKJWijmi6AxAZcXXWA6R/NLVZHxoRTbIMnV8znsHzeiVfuEefYcyR8SJGfbBPmkG+kveLFRcBGw0fBrjLqtGmQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.92.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.92.0.tgz", + "integrity": "sha512-wkw01wvJPM8/4jh+MV3cp11859VuBc94N2q5uOzcBgdGIRAqMCg1O0W/+xsZ1fsInvuUcv2Xo4nd1nzcKhi9kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.92.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.92.0.tgz", + "integrity": "sha512-Elm6nW308MzmR8knvk0tPqQ4XE+PGKztjXv7kVZTRLkhBQZRXHUQoBLc4E30fiIuF6gkZO5f3EpJtQc4Ok/fXw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.92.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.92.0.tgz", + "integrity": "sha512-oylpBAoeogddjhTlSPsXrzGq/aeJsvaULz/hYXvWtGn/Wt8LPLC7gFhucCicbRnzEKsShrZxIA0KyHmtFNnijQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.92.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.92.0.tgz", + "integrity": "sha512-/EWOI0wwY+74UGvC93TldpoNP8moIcidVUPbUSoX16/SEdF7ah209yxSQia4+9JdCRWYA3+yQMofXMgWKk/16Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.92.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.92.0.tgz", + "integrity": "sha512-9Se7Zuazr104yc2PaluriJW2rqvtaN6FSi9MSjQgZGeYb6LKvEcJ586tpjek0l7hNQNoir8MID7OENPH6juN1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.92.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.92.0.tgz", + "integrity": "sha512-88IAchs204I/xxAbAo3NRyz1m/29tn8jUAPvc4tK4FzrT2PLwZertd93ANF6231vwbAhYiD5gl0cX8Po5xsjWQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.92.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.92.0.tgz", + "integrity": "sha512-KtFdVdkfvBGcsrmnbjYJ9/XTDABnaniiwnptAcAm0pkaR2zhZ3sr/Xy+N1Xbz1Yq5Jegu7SoeNv9caTP0b5OTA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.92.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.92.0.tgz", + "integrity": "sha512-V43CO5FPgHtwHup4H5+dIdwzZX2FhREQGsSBZx8V1UyFEAul0Q13qviKqZfSMM6G41CGtCekVpJGGOxW5v1y8Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.92.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.92.0.tgz", + "integrity": "sha512-WooxeACzGH5q34olo7JgAk7XbVkTUNfwhR/TzmhwVs/y1p2R7ZWJo14hiyxiGcoYZR1iBnbBFHectADqb/KSog==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.92.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.92.0.tgz", + "integrity": "sha512-lW2pLF1oFvNN780YUCtDyoUm8oFzZGJNcOFe9RtLMX5v5+FcWfAQvHUMMPF8H8kJB0HAi3dFmWDWLa5VPuY9mw==", + "cpu": [ + "x64" + ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", - "dependencies": { - "@octokit/types": "^12.6.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@octokit/core": "5" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": { - "version": "20.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", - "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.92.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.92.0.tgz", + "integrity": "sha512-2BJAAB/eqPn7iD7mFVfs+1ZVUogAYOMCTaUgjAvbtNGg2ppCDE7gcv/9VSO6GCDybdFUqlcyeyjn9hRpl1ZHog==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { - "version": "12.6.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", - "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.92.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.92.0.tgz", + "integrity": "sha512-iLtjNqqa/2IV5qu/3rrkpVd42Ia3yeCn4fSZoEr+m5FoGHtZvAV6J6YuU3lewg5Rg5ORGvwhdgN+7gNzwkl7tA==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^20.0.0" + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@octokit/plugin-rest-endpoint-methods": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.4.1.tgz", - "integrity": "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg==", + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.92.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.92.0.tgz", + "integrity": "sha512-ryxiR3ZqB0OJzydMu+bVC3R7ZtoU7X1rI8sEgTBCCvQvgVwoCwqSFlfmhgiZ+9wZ4IdTsFeu9tdiF9KtRCbSHQ==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@octokit/types": "^12.6.0" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@octokit/core": "5" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": { - "version": "20.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", - "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.92.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.92.0.tgz", + "integrity": "sha512-DaE9OChj4nsdEST32BP7saGu8WtjOinTy/eS88dZ0TvTfDV26ebRDhsI1rL9OKXfO+DgehzhAQMqdPr2jgfcFw==", + "cpu": [ + "ia32" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { - "version": "12.6.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", - "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.92.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.92.0.tgz", + "integrity": "sha512-0AyhqX0ciFuEnHIL+B4PIMMX3s/IpGHNEbbUqwsNhSrzWUSlEOPw/q7qFnRsEJfiKedcqVfUE/aXYuSMtpiFyw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^20.0.0" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@octokit/request": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz", - "integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==", + "node_modules/@oxlint/migrate": { + "version": "1.92.0", + "resolved": "https://registry.npmjs.org/@oxlint/migrate/-/migrate-1.92.0.tgz", + "integrity": "sha512-QqdP+mFFYD70qjHxFKKbIE2T4p2e+EEJGGv2tzdF313vefgcz9h7QLo6ijIkTWe6Bt9LQUywiuD5jhuq3OdpHA==", "dev": true, "license": "MIT", "dependencies": { - "@octokit/endpoint": "^9.0.6", - "@octokit/request-error": "^5.1.1", - "@octokit/types": "^13.1.0", - "universal-user-agent": "^6.0.0" + "commander": "^15.0.0", + "globals": "^17.0.0", + "oxc-parser": "^0.156.0", + "tinyglobby": "^0.2.14" }, - "engines": { - "node": ">= 18" + "bin": { + "migrate": "dist/bin/oxlint-migrate.mjs" } }, - "node_modules/@octokit/request-error": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz", - "integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==", + "node_modules/@oxlint/migrate/node_modules/commander": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", "dev": true, "license": "MIT", - "dependencies": { - "@octokit/types": "^13.1.0", - "deprecation": "^2.0.0", - "once": "^1.4.0" - }, "engines": { - "node": ">= 18" + "node": ">=22.12.0" } }, - "node_modules/@octokit/types": { - "version": "13.10.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", - "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "node_modules/@oxlint/migrate/node_modules/globals": { + "version": "17.14.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.14.0.tgz", + "integrity": "sha512-UtdkTVyAJePQv0J2M9Xe4L0ruNSDV314XiIJsADI2Zf40bcdAonE/D/OhAU8AT99oij9mhLByb+NvA3QWZYgCg==", "dev": true, "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^24.2.0" + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@react-native-community/eslint-config": { @@ -4549,20 +5392,20 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.51.0.tgz", - "integrity": "sha512-XtssGWJvypyM2ytBnSnKtHYOGT+4ZwTnBVl36TA4nRO2f4PRNGz5/1OszHzcZCvcBMh+qb7I06uoCmLTRdR9og==", + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.72.1.tgz", + "integrity": "sha512-5CokTWXpmbylfFabsbvHC0CAw3XLaGnoa+nFJX5FR592h/LoZsENUE6p/rg5xvkKrFEXz6pYmgI29AgXc2N3BQ==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.51.0", - "@typescript-eslint/type-utils": "8.51.0", - "@typescript-eslint/utils": "8.51.0", - "@typescript-eslint/visitor-keys": "8.51.0", - "ignore": "^7.0.0", + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.72.1", + "@typescript-eslint/type-utils": "8.72.1", + "@typescript-eslint/utils": "8.72.1", + "@typescript-eslint/visitor-keys": "8.72.1", + "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.2.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4572,9 +5415,9 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.51.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "@typescript-eslint/parser": "^8.72.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { @@ -4588,17 +5431,17 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.51.0.tgz", - "integrity": "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A==", + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.72.1.tgz", + "integrity": "sha512-ZGwZP4OCfyA0AQTpEAMWz+rtX01DRhQWW5hB0ucKDEIX5U7jdmPSaMx94rQfaPAQzIYz2BimOgKOJtjfpRFQ9g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.51.0", - "@typescript-eslint/types": "8.51.0", - "@typescript-eslint/typescript-estree": "8.51.0", - "@typescript-eslint/visitor-keys": "8.51.0", - "debug": "^4.3.4" + "@typescript-eslint/scope-manager": "8.72.1", + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/typescript-estree": "8.72.1", + "@typescript-eslint/visitor-keys": "8.72.1", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4608,20 +5451,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.51.0.tgz", - "integrity": "sha512-Luv/GafO07Z7HpiI7qeEW5NW8HUtZI/fo/kE0YbtQEFpJRUuR0ajcWfCE5bnMvL7QQFrmT/odMe8QZww8X2nfQ==", + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.72.1.tgz", + "integrity": "sha512-fAuVMkwdO/huI8nyo/WKnNKf6Gag93S56Rh3Ts5mGcavwSdaN8tRg1gCeIGX8PF+nzp1BjGfO4gHhGzW1J6jTg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.51.0", - "@typescript-eslint/types": "^8.51.0", - "debug": "^4.3.4" + "@typescript-eslint/tsconfig-utils": "^8.72.1", + "@typescript-eslint/types": "^8.72.1", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4631,18 +5474,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.51.0.tgz", - "integrity": "sha512-JhhJDVwsSx4hiOEQPeajGhCWgBMBwVkxC/Pet53EpBVs7zHHtayKefw1jtPaNRXpI9RA2uocdmpdfE7T+NrizA==", + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.72.1.tgz", + "integrity": "sha512-lC/t6eMCZZQAAKZEYGL6RDVSuFehpdmMT6h+nJIjIa6oylN/PwDgDVtEIbB7O9xFfZYtZAYLoLlDNWKJhtF+uQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.51.0", - "@typescript-eslint/visitor-keys": "8.51.0" + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/visitor-keys": "8.72.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4653,9 +5496,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.51.0.tgz", - "integrity": "sha512-Qi5bSy/vuHeWyir2C8u/uqGMIlIDu8fuiYWv48ZGlZ/k+PRPHtaAu7erpc7p5bzw2WNNSniuxoMSO4Ar6V9OXw==", + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.72.1.tgz", + "integrity": "sha512-e4D5R5vpeAEPBxT2BAzsFQL3sbwyeeeo5+aczgNSRK1V734jE7pTqaGFU+fXapWEQwnZbpFaTn32+Z8DwS6OTg==", "dev": true, "license": "MIT", "engines": { @@ -4666,21 +5509,21 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.51.0.tgz", - "integrity": "sha512-0XVtYzxnobc9K0VU7wRWg1yiUrw4oQzexCG2V2IDxxCxhqBMSMbjB+6o91A+Uc0GWtgjCa3Y8bi7hwI0Tu4n5Q==", + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.72.1.tgz", + "integrity": "sha512-iY2wlmyrXJmx4yxUGP3aYCFF3cA/6ShddOKmLU2dx7LnEykbAM8WThlxZEfd1NddqceF6eJmZUayxx9oKIYNlg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.51.0", - "@typescript-eslint/typescript-estree": "8.51.0", - "@typescript-eslint/utils": "8.51.0", - "debug": "^4.3.4", - "ts-api-utils": "^2.2.0" + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/typescript-estree": "8.72.1", + "@typescript-eslint/utils": "8.72.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4690,14 +5533,14 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.51.0.tgz", - "integrity": "sha512-TizAvWYFM6sSscmEakjY3sPqGwxZRSywSsPEiuZF6d5GmGD9Gvlsv0f6N8FvAAA0CD06l3rIcWNbsN1e5F/9Ag==", + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.72.1.tgz", + "integrity": "sha512-LmE5IhQet1BWOWEXqBrVtBPsMAOQ6Nzk35d+WLh3mk4EUF33sqlfxjio1MdYeSvKSG5skactKKuixBIKXhL4Rw==", "dev": true, "license": "MIT", "engines": { @@ -4709,21 +5552,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.51.0.tgz", - "integrity": "sha512-1qNjGqFRmlq0VW5iVlcyHBbCjPB7y6SxpBkrbhNWMy/65ZoncXCEPJxkRZL8McrseNH6lFhaxCIaX+vBuFnRng==", + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.72.1.tgz", + "integrity": "sha512-DAlUoUV93hIvrtIQLwCbmZQYVZwOMhiHJrOVUUIWicFZaIYjTRqlY3I/NZevfkLmSqkNHRu2qYDcq9lrn16oOQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.51.0", - "@typescript-eslint/tsconfig-utils": "8.51.0", - "@typescript-eslint/types": "8.51.0", - "@typescript-eslint/visitor-keys": "8.51.0", - "debug": "^4.3.4", - "minimatch": "^9.0.4", - "semver": "^7.6.0", + "@typescript-eslint/project-service": "8.72.1", + "@typescript-eslint/tsconfig-utils": "8.72.1", + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/visitor-keys": "8.72.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.2.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4733,13 +5576,52 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, "license": "ISC", "bin": { @@ -4750,16 +5632,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.51.0.tgz", - "integrity": "sha512-11rZYxSe0zabiKaCP2QAwRf/dnmgFgvTmeDTtZvUvXG3UuAdg/GU02NExmmIXzz3vLGgMdtrIosI84jITQOxUA==", + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.72.1.tgz", + "integrity": "sha512-5v5liypurJBFHCBNqh/RjsymAD+VWcE0djuGY/BUqp5fC3Qn+luWDEksjuTtWjPn6i5zQlUtkZCRG/CYG0yCkA==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.51.0", - "@typescript-eslint/types": "8.51.0", - "@typescript-eslint/typescript-estree": "8.51.0" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.72.1", + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/typescript-estree": "8.72.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4769,19 +5651,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.51.0.tgz", - "integrity": "sha512-mM/JRQOzhVN1ykejrvwnBRV3+7yTKK8tVANVN3o1O0t0v7o+jqdVu9crPy5Y9dov15TJk/FTIgoUGHrTOVL3Zg==", + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.1.tgz", + "integrity": "sha512-SB8xqipSzZHUePCCLNeYxqnxq83X61/qzNhjQGbTfjxGZIeOEP7tBgWYX05Tcd6CgE02WdTJnh5b8kMZJ/+bnQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.51.0", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "8.72.1", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4792,13 +5674,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" @@ -7622,29 +8504,47 @@ } }, "node_modules/eslint-config-expensify": { - "version": "2.0.108", - "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.108.tgz", - "integrity": "sha512-J+tW0s5rli+ewOdY3fyf8Hk3G8WJho4YPNlTeSyeS+zD4JsQcqpOFF9wKzQFr8Tl/2E9f/4gxcYo2xiHolONLA==", + "version": "4.0.20", + "resolved": "git+ssh://git@github.com/callstack-internal/eslint-config-expensify.git#c36e57c42863272f60c06b9b2d1173c863df30f0", + "integrity": "sha512-O5Yg2QyP40yaGcHOLkj/XVembYobTrGFf3H7iorVVnAAEu587A5sQzEeve+/xAc3r5iPYBqCXqitB7ZVrvpqgQ==", "dev": true, "license": "ISC", "dependencies": { "@babel/eslint-parser": "^7.28.4", - "@lwc/eslint-plugin-lwc": "^3.2.0", "@typescript-eslint/parser": "^8.44.1", "@typescript-eslint/utils": "^8.44.1", "confusing-browser-globals": "^1.0.11", "eslint": "^9.36.0", + "eslint-config-prettier": "^9.1.2", "eslint-plugin-es": "^4.1.0", "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jest": "^29.0.1", "eslint-plugin-jsdoc": "^60.2.0", "eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-react": "^7.37.5", - "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-rulesdir": "^0.2.2", "eslint-plugin-unicorn": "^61.0.2", "globals": "^15.14.0", - "lodash": "^4.17.21", - "underscore": "^1.13.6" + "lodash": "^4.18.1", + "typescript-eslint": "^8.61.0", + "underscore": "^1.13.8" + }, + "peerDependencies": { + "@oxlint/migrate": ">=1.90.0 <2", + "oxlint": ">=1.90.0 <2", + "oxlint-tsgolint": ">=7.0.2005 <8" + }, + "peerDependenciesMeta": { + "@oxlint/migrate": { + "optional": true + }, + "oxlint": { + "optional": true + }, + "oxlint-tsgolint": { + "optional": true + } } }, "node_modules/eslint-config-expensify/node_modules/ci-info": { @@ -7664,9 +8564,9 @@ } }, "node_modules/eslint-config-expensify/node_modules/eslint-plugin-react-hooks": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", - "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", "dev": true, "license": "MIT", "dependencies": { @@ -7680,7 +8580,7 @@ "node": ">=18" }, "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, "node_modules/eslint-config-expensify/node_modules/eslint-plugin-unicorn": { @@ -8087,6 +8987,36 @@ "node": "*" } }, + "node_modules/eslint-plugin-jest": { + "version": "29.17.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-29.17.0.tgz", + "integrity": "sha512-6jtpU6MhYKbagkES02TupWAUUooJpZtKg94Ax0b8HhUorv3InSTBU8RRtlSBe0LocOcShXOpyec3PHzPDkJCSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^8.0.0" + }, + "engines": { + "node": "^20.12.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^8.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "jest": "*", + "typescript": ">=4.8.4 <8.0.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "jest": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/eslint-plugin-jsdoc": { "version": "60.8.3", "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-60.8.3.tgz", @@ -9411,9 +10341,9 @@ } }, "node_modules/globals": { - "version": "15.14.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz", - "integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==", + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", "dev": true, "license": "MIT", "engines": { @@ -11244,12 +12174,12 @@ "dev": true, "funding": [ { - "url": "https://github.com/sponsors/puzrin", - "type": "github" + "type": "github", + "url": "https://github.com/sponsors/puzrin" }, { - "url": "https://github.com/sponsors/nodeca", - "type": "github" + "type": "github", + "url": "https://github.com/sponsors/nodeca" } ], "license": "MIT", @@ -13138,6 +14068,110 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/oxc-parser": { + "version": "0.156.0", + "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.156.0.tgz", + "integrity": "sha512-7JhjgOfZjrIFJH5TlHrpRua864Si5GvwrPr5+eIRCY2sszhKXo6pyhP0HQKOGTtfO4UowwZ72XTGba0+lZphOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "^0.156.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + }, + "optionalDependencies": { + "@oxc-parser/binding-android-arm-eabi": "0.156.0", + "@oxc-parser/binding-android-arm64": "0.156.0", + "@oxc-parser/binding-darwin-arm64": "0.156.0", + "@oxc-parser/binding-darwin-x64": "0.156.0", + "@oxc-parser/binding-freebsd-x64": "0.156.0", + "@oxc-parser/binding-linux-arm-gnueabihf": "0.156.0", + "@oxc-parser/binding-linux-arm-musleabihf": "0.156.0", + "@oxc-parser/binding-linux-arm64-gnu": "0.156.0", + "@oxc-parser/binding-linux-arm64-musl": "0.156.0", + "@oxc-parser/binding-linux-ppc64-gnu": "0.156.0", + "@oxc-parser/binding-linux-riscv64-gnu": "0.156.0", + "@oxc-parser/binding-linux-riscv64-musl": "0.156.0", + "@oxc-parser/binding-linux-s390x-gnu": "0.156.0", + "@oxc-parser/binding-linux-x64-gnu": "0.156.0", + "@oxc-parser/binding-linux-x64-musl": "0.156.0", + "@oxc-parser/binding-openharmony-arm64": "0.156.0", + "@oxc-parser/binding-win32-arm64-msvc": "0.156.0", + "@oxc-parser/binding-win32-ia32-msvc": "0.156.0", + "@oxc-parser/binding-win32-x64-msvc": "0.156.0" + } + }, + "node_modules/oxlint": { + "version": "1.92.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.92.0.tgz", + "integrity": "sha512-y9iRiRwVjVFqzKNXZJ9bT/GYRdf+F9Gy+a1Yju2Aq3xqqa4FJYXHI8VrxlpzxKDvkYRohvJX42m/eH+qkQPOFw==", + "dev": true, + "license": "MIT", + "bin": { + "oxlint": "bin/oxlint" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + }, + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.92.0", + "@oxlint/binding-android-arm64": "1.92.0", + "@oxlint/binding-darwin-arm64": "1.92.0", + "@oxlint/binding-darwin-x64": "1.92.0", + "@oxlint/binding-freebsd-x64": "1.92.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.92.0", + "@oxlint/binding-linux-arm-musleabihf": "1.92.0", + "@oxlint/binding-linux-arm64-gnu": "1.92.0", + "@oxlint/binding-linux-arm64-musl": "1.92.0", + "@oxlint/binding-linux-ppc64-gnu": "1.92.0", + "@oxlint/binding-linux-riscv64-gnu": "1.92.0", + "@oxlint/binding-linux-riscv64-musl": "1.92.0", + "@oxlint/binding-linux-s390x-gnu": "1.92.0", + "@oxlint/binding-linux-x64-gnu": "1.92.0", + "@oxlint/binding-linux-x64-musl": "1.92.0", + "@oxlint/binding-openharmony-arm64": "1.92.0", + "@oxlint/binding-win32-arm64-msvc": "1.92.0", + "@oxlint/binding-win32-ia32-msvc": "1.92.0", + "@oxlint/binding-win32-x64-msvc": "1.92.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=7.0.2005", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, + "node_modules/oxlint-tsgolint": { + "version": "7.0.2005", + "resolved": "https://registry.npmjs.org/oxlint-tsgolint/-/oxlint-tsgolint-7.0.2005.tgz", + "integrity": "sha512-6ccPGu4u4J9bwYPHXBFPee3RZrX3BboRdU7k14zRDDB1lvN3O7J/vPRFdaEfa1+CWx4xnQQOp4ctoaF66oO0uw==", + "dev": true, + "license": "MIT", + "bin": { + "tsgolint": "bin/tsgolint.js" + }, + "optionalDependencies": { + "@oxlint-tsgolint/darwin-arm64": "7.0.2005", + "@oxlint-tsgolint/darwin-x64": "7.0.2005", + "@oxlint-tsgolint/linux-arm64": "7.0.2005", + "@oxlint-tsgolint/linux-x64": "7.0.2005", + "@oxlint-tsgolint/win32-arm64": "7.0.2005", + "@oxlint-tsgolint/win32-x64": "7.0.2005" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -15800,9 +16834,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.3.0.tgz", - "integrity": "sha512-6eg3Y9SF7SsAvGzRHQvvc1skDAhwI4YQ32ui1scxD1Ccr0G5qIIbUBT3pFTKX8kmWIQClHobtUdNuaBgwdfdWg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { @@ -16360,6 +17394,30 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.72.1.tgz", + "integrity": "sha512-3Li9FfPmQp45nxEtFD1ZCrYFI3cYK5kpyNtmm1H7lMagm8u9sy6PTjy3ytwQZs7KNukOyKFHAZ6z6GjYFeaeXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.72.1", + "@typescript-eslint/parser": "8.72.1", + "@typescript-eslint/typescript-estree": "8.72.1", + "@typescript-eslint/utils": "8.72.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, "node_modules/typical": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/typical/-/typical-2.6.1.tgz", diff --git a/package.json b/package.json index 701164371..ddd0e76bb 100644 --- a/package.json +++ b/package.json @@ -29,16 +29,17 @@ "types": "dist/index.d.ts", "scripts": { "prepare": "npm run build", - "lint": "eslint .", + "lint": "node config/oxlint/lint.mjs", "typecheck": "tsc --noEmit", "test": "jest", + "test:lint": "node --test config/oxlint/seatbelt.test.mjs", "test:types": "npm run build && tsc --noEmit --project tsconfig.test.json", "perf-test": "npx reassure", "build": "tsc -p tsconfig.build.json", "build:watch": "nodemon --watch lib --ext js,json,ts,tsx --exec \"npm run build && npm pack\"", "prebuild:docs": "npm run build", "build:docs": "ts-node buildDocs.ts", - "lint-tests": "eslint tests/**", + "lint-tests": "node config/oxlint/lint.mjs tests", "prettier": "prettier --write .", "gh-actions-build": "./.github/scripts/buildActions.sh" }, @@ -57,6 +58,7 @@ "@jest/globals": "^29.7.0", "@lwc/eslint-plugin-lwc": "^3.5.0", "@ngneat/falso": "^7.3.0", + "@oxlint/migrate": "1.92.0", "@react-native-community/eslint-config": "^3.2.0", "@react-native/babel-preset": "0.76.3", "@react-native/polyfills": "^2.0.0", @@ -77,12 +79,12 @@ "better-sqlite3": "^12.11.1", "date-fns": "^4.1.0", "eslint": "^9.39.2", - "eslint-config-expensify": "2.0.108", - "eslint-config-prettier": "^9.1.0", + "eslint-config-expensify": "github:callstack-internal/eslint-config-expensify#c36e57c42863272f60c06b9b2d1173c863df30f0", "eslint-import-resolver-typescript": "^4.4.4", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-react": "^7.37.5", + "eslint-plugin-rulesdir": "^0.2.2", "fake-indexeddb": "^6.2.5", "idb-keyval": "^6.2.1", "jest": "^29.7.0", @@ -90,6 +92,8 @@ "jest-environment-jsdom": "^29.7.0", "jsdoc-to-markdown": "^7.1.0", "nodemon": "^3.0.3", + "oxlint": "1.92.0", + "oxlint-tsgolint": "7.0.2005", "prettier": "^2.8.8", "prop-types": "^15.7.2", "react": "18.2.0", diff --git a/tests/types.ts b/tests/types.ts index 1aef55855..d1022e4b2 100644 --- a/tests/types.ts +++ b/tests/types.ts @@ -8,5 +8,5 @@ import type {DeepRecord} from '../lib/types'; // eslint-disable-next-line @typescript-eslint/no-explicit-any type GenericDeepRecord = DeepRecord; -// eslint-disable-next-line import/prefer-default-export +// oxlint-disable-next-line hosted/prefer-default-export export type {GenericDeepRecord}; diff --git a/tests/unit/DevToolsTest.ts b/tests/unit/DevToolsTest.ts index 60664c5b3..6ab1232e8 100644 --- a/tests/unit/DevToolsTest.ts +++ b/tests/unit/DevToolsTest.ts @@ -1,4 +1,3 @@ -/* eslint-disable dot-notation */ import Onyx from '../../lib'; import {getDevToolsInstance} from '../../lib/DevTools'; import type {DevtoolsConnection, RealDevTools as RealDevToolsType} from '../../lib/DevTools'; diff --git a/tests/unit/OnyxSubscriptionManagerTest.ts b/tests/unit/OnyxSubscriptionManagerTest.ts index bbecc4ad5..5c551a0b5 100644 --- a/tests/unit/OnyxSubscriptionManagerTest.ts +++ b/tests/unit/OnyxSubscriptionManagerTest.ts @@ -11,7 +11,6 @@ import waitForPromisesToResolve from '../utils/waitForPromisesToResolve'; // We need access to some internal properties of `onyxSubscriptionManager` during the tests but they are private, // so this workaround allows us to have access to them. The maps are created once in the constructor // and only ever `.clear()`ed (never reassigned), so capturing the references here stays valid. -// eslint-disable-next-line dot-notation const keyListeners = onyxSubscriptionManager['keyListeners']; const ONYXKEYS = { diff --git a/tsconfig.build.json b/tsconfig.build.json index 78e6b52a3..79205ce73 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,5 +1,9 @@ { "$schema": "https://json.schemastore.org/tsconfig", "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "moduleResolution": "node" + }, "include": ["./lib"] } diff --git a/tsconfig.json b/tsconfig.json index d920b8700..ff8656621 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { "target": "es2015", - "module": "commonjs", + "module": "esnext", "types": ["react-native", "react", "jest", "node"], "lib": ["esnext", "dom"], "allowJs": true, @@ -10,12 +10,18 @@ "jsx": "react", "isolatedModules": true, "strict": true, - "moduleResolution": "node", + "moduleResolution": "bundler", "resolveJsonModule": true, "esModuleInterop": true, "skipLibCheck": true, "declaration": true, "outDir": "./dist" }, + "ts-node": { + "compilerOptions": { + "module": "commonjs", + "moduleResolution": "node" + } + }, "exclude": ["**/node_modules/**/*", "**/dist/**/*", "tests/types/**/*.ts"] }