diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index a20edab4f..330657566 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -24,6 +24,7 @@ jobs: - name: Checkout uses: actions/checkout@v6 with: + fetch-depth: 0 persist-credentials: false - uses: ./.github/actions/install-node-dependencies @@ -34,6 +35,15 @@ jobs: - name: Check switch exhaustiveness run: pnpm run lint:switch-exhaustiveness + - name: Enforce changed-code quality + run: pnpm run check:code-quality:changed -- "${{ github.event.pull_request.base.sha }}" + + - name: Enforce React Doctor on changed lines + run: pnpm run check:react-doctor:changed -- "${{ github.event.pull_request.base.sha }}" + + - name: Check Zustand selector fan-out budget + run: pnpm run check:zustand-selector-fanout + - name: Check styled scrollbars run: pnpm check:styled-scrollbars diff --git a/.oxlintrc.json b/.oxlintrc.json index 4b0cbed3e..5eb541a89 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -1,6 +1,12 @@ { "$schema": "./node_modules/oxlint/configuration_schema.json", "plugins": ["typescript", "react", "react-hooks", "react-perf", "unicorn"], + "jsPlugins": [ + { + "name": "mobile-pairing", + "specifier": "./config/oxlint-plugins/mobile-pairing-qrcode-import.mjs" + } + ], "categories": { "correctness": "error" }, @@ -57,15 +63,11 @@ "name": "@linear/sdk", "allowTypeImports": true, "message": "Value-importing @linear/sdk hoists its ~2.6MB CJS bundle into the eager top-level require block and defeats the lazy loader. Use `import type` plus loadLinearSdk() from src/main/linear/linear-sdk.ts." - }, - { - "name": "qrcode", - "allowTypeImports": true, - "message": "qrcode is only reachable from mobile pairing; a static import parses it at launch for everyone. Use `import type` plus `await import('qrcode')` at the call site." } ] } ], + "mobile-pairing/no-eager-qrcode-import": "error", "no-useless-return": "error", "prefer-template": "error", "unicorn/consistent-empty-array-spread": "error", diff --git a/config/oxlint-code-quality-type-aware.json b/config/oxlint-code-quality-type-aware.json new file mode 100644 index 000000000..409215908 --- /dev/null +++ b/config/oxlint-code-quality-type-aware.json @@ -0,0 +1,22 @@ +{ + "$schema": "../node_modules/oxlint/configuration_schema.json", + "plugins": ["typescript"], + "categories": { + "correctness": "off", + "suspicious": "off", + "pedantic": "off", + "perf": "off", + "style": "off", + "restriction": "off", + "nursery": "off" + }, + "rules": { + "typescript/await-thenable": "warn", + "typescript/no-floating-promises": "warn", + "typescript/no-misused-promises": "warn", + "typescript/only-throw-error": "warn", + "typescript/restrict-plus-operands": "warn", + "typescript/restrict-template-expressions": "warn" + }, + "ignorePatterns": ["**/node_modules", "**/dist", "**/out"] +} diff --git a/config/oxlint-code-quality.json b/config/oxlint-code-quality.json new file mode 100644 index 000000000..3ab7f3bae --- /dev/null +++ b/config/oxlint-code-quality.json @@ -0,0 +1,71 @@ +{ + "$schema": "../node_modules/oxlint/configuration_schema.json", + "plugins": ["import", "jsx-a11y", "promise", "react", "react-hooks", "typescript", "vitest"], + "categories": { + "correctness": "off", + "suspicious": "off", + "pedantic": "off", + "perf": "off", + "style": "off", + "restriction": "off", + "nursery": "off" + }, + "options": { + "reportUnusedDisableDirectives": "warn" + }, + "jsPlugins": [ + { + "name": "app-store-performance", + "specifier": "./oxlint-plugins/app-store-performance.mjs" + } + ], + "rules": { + "app-store-performance/require-selector": "warn", + "app-store-performance/no-identity-selector": "warn", + "app-store-performance/no-fresh-selector-result": "warn", + "import/export": "warn", + "import/named": "warn", + "import/namespace": "warn", + "import/no-cycle": ["warn", { "maxDepth": 3 }], + "import/no-duplicates": "warn", + "import/no-self-import": "warn", + "max-params": ["warn", 5], + "no-fallthrough": "warn", + "no-loop-func": "warn", + "no-promise-executor-return": "warn", + "no-unmodified-loop-condition": "warn", + "preserve-caught-error": "warn", + "promise/no-multiple-resolved": "warn", + "react/no-unstable-nested-components": "warn", + "react/react-compiler": "warn" + }, + "overrides": [ + { + "files": ["src/renderer/src/**/*.{ts,tsx}"], + "rules": { + "jsx-a11y/alt-text": "warn", + "jsx-a11y/anchor-has-content": "warn", + "jsx-a11y/aria-props": "warn", + "jsx-a11y/aria-role": "warn", + "jsx-a11y/click-events-have-key-events": "warn" + } + }, + { + "files": ["**/*.{test,spec}.{ts,tsx}", "tests/**/*.{ts,tsx}"], + "rules": { + "vitest/no-conditional-tests": "warn", + "vitest/no-focused-tests": "warn", + "vitest/no-identical-title": "warn", + "vitest/valid-expect": "warn", + "vitest/valid-title": "warn" + } + }, + { + "files": ["mobile/**/*.{ts,tsx}"], + "rules": { + "react-hooks/exhaustive-deps": "warn" + } + } + ], + "ignorePatterns": ["**/node_modules", "**/dist", "**/out"] +} diff --git a/config/oxlint-plugins/app-store-performance.mjs b/config/oxlint-plugins/app-store-performance.mjs new file mode 100644 index 000000000..9da732f58 --- /dev/null +++ b/config/oxlint-plugins/app-store-performance.mjs @@ -0,0 +1,244 @@ +const ALLOCATING_METHODS = new Set([ + 'filter', + 'flat', + 'flatMap', + 'map', + 'toReversed', + 'toSorted', + 'toSpliced', + 'with' +]) + +function identifierName(node) { + return node?.type === 'Identifier' ? node.name : null +} + +function propertyName(node) { + if (node?.type !== 'MemberExpression') { + return null + } + if (!node.computed) { + return identifierName(node.property) + } + return node.property?.type === 'Literal' && typeof node.property.value === 'string' + ? node.property.value + : null +} + +function returnedExpressions(selector) { + if (selector?.type !== 'ArrowFunctionExpression' && selector?.type !== 'FunctionExpression') { + return [] + } + if (selector.body.type !== 'BlockStatement') { + return [selector.body] + } + const expressions = [] + const visit = (node) => { + if (!node || typeof node !== 'object') { + return + } + if ( + node !== selector.body && + ['ArrowFunctionExpression', 'FunctionDeclaration', 'FunctionExpression'].includes(node.type) + ) { + return + } + if (node.type === 'ReturnStatement') { + if (node.argument) { + expressions.push(node.argument) + } + return + } + for (const [key, child] of Object.entries(node)) { + if (key === 'parent') { + continue + } + if (Array.isArray(child)) { + child.forEach(visit) + } else { + visit(child) + } + } + } + visit(selector.body) + return expressions +} + +function unwrapShallowSelector(selector, shallowHooks) { + if ( + selector?.type === 'CallExpression' && + selector.callee.type === 'Identifier' && + shallowHooks.has(selector.callee.name) + ) { + return { selector: selector.arguments[0], shallow: true } + } + return { selector, shallow: false } +} + +function isIdentitySelector(selector) { + if (selector?.type !== 'ArrowFunctionExpression' && selector?.type !== 'FunctionExpression') { + return false + } + const parameter = selector.params[0] + if (parameter?.type !== 'Identifier') { + return false + } + return returnedExpressions(selector).some( + (expression) => expression.type === 'Identifier' && expression.name === parameter.name + ) +} + +function isAllocatingExpression(expression) { + if (expression?.type === 'ConditionalExpression') { + return ( + isAllocatingExpression(expression.consequent) || isAllocatingExpression(expression.alternate) + ) + } + if (expression?.type === 'LogicalExpression') { + return isAllocatingExpression(expression.left) || isAllocatingExpression(expression.right) + } + if ( + expression?.type === 'ArrayExpression' || + expression?.type === 'ObjectExpression' || + expression?.type === 'NewExpression' + ) { + return true + } + if (expression?.type !== 'CallExpression') { + return false + } + const method = propertyName(expression.callee) + if (method && ALLOCATING_METHODS.has(method)) { + return true + } + const callee = expression.callee + return ( + callee.type === 'MemberExpression' && + identifierName(callee.object) === 'Object' && + ['assign', 'create', 'entries', 'fromEntries', 'keys', 'values'].includes(propertyName(callee)) + ) +} + +function importedLocalName(specifier, importedName) { + if (specifier.type !== 'ImportSpecifier' || identifierName(specifier.imported) !== importedName) { + return null + } + return identifierName(specifier.local) +} + +function createRuleState() { + return { + appStoreHooks: new Set(), + shallowHooks: new Set() + } +} + +function recordImports(node, state) { + if (node.source?.value === 'zustand/react/shallow') { + for (const specifier of node.specifiers) { + const localName = importedLocalName(specifier, 'useShallow') + if (localName) { + state.shallowHooks.add(localName) + } + } + } + for (const specifier of node.specifiers) { + const localName = importedLocalName(specifier, 'useAppStore') + if (localName) { + state.appStoreHooks.add(localName) + } + } +} + +function isAppStoreCall(node, state) { + return ( + node.callee.type === 'Identifier' && + state.appStoreHooks.has(node.callee.name) && + node.optional !== true + ) +} + +function requireSelectorRule() { + const state = createRuleState() + return { + ImportDeclaration(node) { + recordImports(node, state) + }, + CallExpression(node) { + if (isAppStoreCall(node, state) && node.arguments.length === 0) { + this.report({ + node, + message: + 'Pass a selector to useAppStore so the component does not rerender for every store write.' + }) + } + } + } +} + +function noIdentitySelectorRule() { + const state = createRuleState() + return { + ImportDeclaration(node) { + recordImports(node, state) + }, + CallExpression(node) { + if (!isAppStoreCall(node, state)) { + return + } + const { selector } = unwrapShallowSelector(node.arguments[0], state.shallowHooks) + if (isIdentitySelector(selector)) { + this.report({ + node: selector, + message: + 'Select the smallest required fields instead of subscribing to the entire app store.' + }) + } + } + } +} + +function noFreshSelectorResultRule() { + const state = createRuleState() + return { + ImportDeclaration(node) { + recordImports(node, state) + }, + CallExpression(node) { + if (!isAppStoreCall(node, state)) { + return + } + const { selector, shallow } = unwrapShallowSelector(node.arguments[0], state.shallowHooks) + if (shallow) { + return + } + const freshResult = returnedExpressions(selector).find(isAllocatingExpression) + if (freshResult) { + this.report({ + node: freshResult, + message: + 'This selector returns a fresh reference on every store write; select a stable field, cache the result, or use useShallow.' + }) + } + } + } +} + +function bindContext(createVisitors) { + return (context) => { + const visitors = createVisitors() + for (const [nodeType, visit] of Object.entries(visitors)) { + visitors[nodeType] = visit.bind(context) + } + return visitors + } +} + +export default { + meta: { name: 'app-store-performance' }, + rules: { + 'require-selector': { create: bindContext(requireSelectorRule) }, + 'no-identity-selector': { create: bindContext(noIdentitySelectorRule) }, + 'no-fresh-selector-result': { create: bindContext(noFreshSelectorResultRule) } + } +} diff --git a/config/oxlint-plugins/mobile-pairing-qrcode-import.mjs b/config/oxlint-plugins/mobile-pairing-qrcode-import.mjs new file mode 100644 index 000000000..f5ecae20d --- /dev/null +++ b/config/oxlint-plugins/mobile-pairing-qrcode-import.mjs @@ -0,0 +1,29 @@ +const MESSAGE = + "qrcode is only reachable from mobile pairing. Use `import type` plus `await import('qrcode')` so startup does not parse its bundle." + +function hasRuntimeImport(node) { + if (node.importKind === 'type') { + return false + } + return ( + node.specifiers.length === 0 || + node.specifiers.some((specifier) => specifier.importKind !== 'type') + ) +} + +export default { + meta: { name: 'mobile-pairing' }, + rules: { + 'no-eager-qrcode-import': { + create(context) { + return { + ImportDeclaration(node) { + if (node.source?.value === 'qrcode' && hasRuntimeImport(node)) { + context.report({ node, message: MESSAGE }) + } + } + } + } + } + } +} diff --git a/config/oxlint-react-doctor.json b/config/oxlint-react-doctor.json index 7083b899e..3c91b01d5 100644 --- a/config/oxlint-react-doctor.json +++ b/config/oxlint-react-doctor.json @@ -12,9 +12,18 @@ }, "jsPlugins": [{ "name": "react-doctor", "specifier": "oxlint-plugin-react-doctor" }], "rules": { + "react-doctor/effect-needs-cleanup": "warn", + "react-doctor/no-array-index-as-key": "warn", "react-doctor/no-adjust-state-on-prop-change": "warn", + "react-doctor/no-create-store-in-render": "warn", "react-doctor/no-derived-state-effect": "warn", - "react-doctor/no-initialize-state": "warn" + "react-doctor/no-initialize-state": "warn", + "react-doctor/no-side-effect-in-state-updater-function": "warn", + "react-doctor/no-unstable-nested-components": "warn", + "react-doctor/zustand-no-fresh-selector-result": "warn", + "react-doctor/zustand-no-get-during-initialization": "warn", + "react-doctor/zustand-no-mutating-state": "warn", + "react-doctor/zustand-no-whole-store-destructure": "warn" }, "ignorePatterns": ["**/node_modules", "**/dist", "**/out"] } diff --git a/config/scripts/app-store-performance-plugin.test.mjs b/config/scripts/app-store-performance-plugin.test.mjs new file mode 100644 index 000000000..344918309 --- /dev/null +++ b/config/scripts/app-store-performance-plugin.test.mjs @@ -0,0 +1,76 @@ +import { spawnSync } from 'node:child_process' +import { mkdtempSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import path from 'node:path' +import { describe, expect, it } from 'vitest' + +const pluginPath = path.resolve('config/oxlint-plugins/app-store-performance.mjs') +const oxlintPath = path.resolve( + process.platform === 'win32' ? 'node_modules/.bin/oxlint.cmd' : 'node_modules/.bin/oxlint' +) + +function lintSource(source) { + const directory = mkdtempSync(path.join(tmpdir(), 'orca-app-store-lint-')) + const sourcePath = path.join(directory, 'sample.tsx') + const configPath = path.join(directory, 'oxlint.json') + writeFileSync(sourcePath, source) + writeFileSync( + configPath, + JSON.stringify({ + categories: { + correctness: 'off' + }, + jsPlugins: [{ name: 'app-store-performance', specifier: pluginPath }], + rules: { + 'app-store-performance/require-selector': 'warn', + 'app-store-performance/no-identity-selector': 'warn', + 'app-store-performance/no-fresh-selector-result': 'warn' + } + }) + ) + const result = spawnSync(oxlintPath, ['--config', configPath, '--format', 'json', sourcePath], { + encoding: 'utf8' + }) + if (result.error) { + throw result.error + } + expect(result.status).toBe(0) + return JSON.parse(result.stdout).diagnostics +} + +describe('app store performance Oxlint plugin', () => { + it('reports whole-store and fresh-reference subscriptions', () => { + const diagnostics = lintSource(` + import { useAppStore as useStore } from '@/store' + const WholeStore = () => useStore() + const Identity = () => useStore((state) => state) + const Fresh = () => useStore((state) => ({ active: state.active })) + const Conditional = () => useStore((state) => state.active ? state.items : []) + const Nested = () => useStore((state) => { + if (state.active) return state.items.filter(Boolean) + return state.items + }) + `) + + expect(diagnostics.map((diagnostic) => diagnostic.code)).toEqual([ + 'app-store-performance(require-selector)', + 'app-store-performance(no-identity-selector)', + 'app-store-performance(no-fresh-selector-result)', + 'app-store-performance(no-fresh-selector-result)', + 'app-store-performance(no-fresh-selector-result)' + ]) + }) + + it('allows focused, cached, and useShallow selectors', () => { + const diagnostics = lintSource(` + import { useAppStore } from '@/store' + import { useShallow as shallow } from 'zustand/react/shallow' + const selectActive = (state) => state.active + const Focused = () => useAppStore(selectActive) + const Cached = () => useAppStore((state) => state.cachedProjection) + const Shallow = () => useAppStore(shallow((state) => ({ active: state.active }))) + `) + + expect(diagnostics).toEqual([]) + }) +}) diff --git a/config/scripts/check-changed-code-quality.mjs b/config/scripts/check-changed-code-quality.mjs new file mode 100644 index 000000000..7efcbe480 --- /dev/null +++ b/config/scripts/check-changed-code-quality.mjs @@ -0,0 +1,225 @@ +import { execFileSync, spawnSync } from 'node:child_process' +import { existsSync, readFileSync } from 'node:fs' +import path from 'node:path' +import process from 'node:process' +import { pathToFileURL } from 'node:url' +import { resolvePullRequestDiffBase } from './git-pull-request-diff-base.mjs' + +const SOURCE_FILE_PATTERN = /\.(?:[cm]?[jt]sx?)$/ +const OXLINT_SCANS = [ + { + label: 'code quality', + args: [ + '--config', + 'config/oxlint-code-quality.json', + '--report-unused-disable-directives-severity', + 'warn' + ] + }, + { + label: 'type-aware code quality', + args: ['--type-aware', '--config', 'config/oxlint-code-quality-type-aware.json'] + }, + { + label: 'React Doctor', + args: ['--config', 'config/oxlint-react-doctor.json'] + } +] + +export function parseAddedLineRanges(diff) { + const ranges = [] + const hunkPattern = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/ + for (const line of diff.split(/\r?\n/)) { + const match = hunkPattern.exec(line) + if (!match) { + continue + } + const start = Number.parseInt(match[1], 10) + const count = match[2] === undefined ? 1 : Number.parseInt(match[2], 10) + if (count > 0) { + ranges.push({ start, end: start + count - 1 }) + } + } + return ranges +} + +export function overlapsAddedLines(startLine, endLine, ranges) { + return ranges.some((range) => startLine <= range.end && endLine >= range.start) +} + +function runGit(root, args, options = {}) { + return execFileSync('git', args, { + cwd: root, + encoding: options.encoding ?? 'utf8', + maxBuffer: 64 * 1024 * 1024 + }) +} + +function splitNullDelimited(output) { + return output.split('\0').filter(Boolean) +} + +function resolveBase(root, requestedBase) { + for (const candidate of [ + requestedBase, + process.env.ORCA_CODE_QUALITY_BASE, + 'origin/main', + 'main' + ]) { + if (!candidate) { + continue + } + const result = spawnSync('git', ['rev-parse', '--verify', `${candidate}^{commit}`], { + cwd: root, + stdio: 'ignore' + }) + if (result.status === 0) { + return candidate + } + } + throw new Error('Pass the pull request base SHA or make origin/main available locally.') +} + +export function collectAddedLineRanges(root, requestedBase) { + const base = resolveBase(root, requestedBase) + const mergeBase = runGit(root, ['merge-base', base, 'HEAD']).trim() + const comparisonBase = resolvePullRequestDiffBase(root, mergeBase) + const changedFiles = splitNullDelimited( + runGit(root, ['diff', '--name-only', '-z', '--diff-filter=ACMRTUB', comparisonBase, '--']) + ) + const untrackedFiles = splitNullDelimited( + runGit(root, ['ls-files', '--others', '--exclude-standard', '-z']) + ) + const rangesByFile = new Map() + + for (const file of changedFiles) { + if (!SOURCE_FILE_PATTERN.test(file) || !existsSync(path.join(root, file))) { + continue + } + const diff = runGit(root, ['diff', '--unified=0', '--no-color', comparisonBase, '--', file]) + const ranges = parseAddedLineRanges(diff) + if (ranges.length > 0) { + rangesByFile.set(file, ranges) + } + } + + for (const file of untrackedFiles) { + const absolutePath = path.join(root, file) + if (!SOURCE_FILE_PATTERN.test(file) || !existsSync(absolutePath)) { + continue + } + const lineCount = readFileSync(absolutePath, 'utf8').split(/\r?\n/).length + rangesByFile.set(file, [{ start: 1, end: lineCount }]) + } + return { base, comparisonBase, rangesByFile } +} + +function parseOxlintOutput(stdout, label) { + const start = stdout.indexOf('{') + const end = stdout.lastIndexOf('}') + if (start === -1 || end === -1) { + throw new Error(`${label} did not return Oxlint JSON output.`) + } + return JSON.parse(stdout.slice(start, end + 1)) +} + +function normalizedDiagnosticPath(root, filename) { + const absolutePath = path.isAbsolute(filename) ? filename : path.join(root, filename) + return path.relative(root, absolutePath).split(path.sep).join('/') +} + +function diagnosticLineRange(root, filename, span) { + const startLine = span.line + if (!Number.isInteger(startLine)) { + return null + } + if (!Number.isInteger(span.offset) || !Number.isInteger(span.length) || span.length === 0) { + return { start: startLine, end: startLine } + } + const absolutePath = path.isAbsolute(filename) ? filename : path.join(root, filename) + const source = readFileSync(absolutePath) + const highlighted = source.subarray(span.offset, span.offset + span.length).toString('utf8') + return { start: startLine, end: startLine + (highlighted.match(/\n/g)?.length ?? 0) } +} + +export function diagnosticTouchesAddedLines(diagnostic, rangesByFile, root = process.cwd()) { + const file = normalizedDiagnosticPath(root, diagnostic.filename) + const ranges = rangesByFile.get(file) + if (!ranges) { + return false + } + return (diagnostic.labels ?? []).some((label) => { + const lineRange = diagnosticLineRange(root, diagnostic.filename, label.span) + return lineRange !== null && overlapsAddedLines(lineRange.start, lineRange.end, ranges) + }) +} + +function annotationValue(value) { + return String(value).replaceAll('%', '%25').replaceAll('\r', '%0D').replaceAll('\n', '%0A') +} + +function printDiagnostic(diagnostic, root) { + const file = normalizedDiagnosticPath(root, diagnostic.filename) + const line = diagnostic.labels?.[0]?.span?.line ?? 1 + const code = diagnostic.code ?? 'oxlint' + console.error( + `::error file=${annotationValue(file)},line=${line},title=${annotationValue(code)}::${annotationValue(diagnostic.message)}` + ) + console.error(`${file}:${line} ${code}: ${diagnostic.message}`) +} + +function runOxlintScan(root, scan, files) { + const pnpm = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm' + const result = spawnSync(pnpm, ['exec', 'oxlint', ...scan.args, '--format', 'json', ...files], { + cwd: root, + encoding: 'utf8', + maxBuffer: 128 * 1024 * 1024 + }) + if (result.error) { + throw result.error + } + if (!result.stdout.trim()) { + process.stderr.write(result.stderr) + throw new Error(`${scan.label} failed before producing diagnostics.`) + } + return parseOxlintOutput(result.stdout, scan.label).diagnostics ?? [] +} + +export function main( + root = process.cwd(), + requestedBase = process.argv.slice(2).find((argument) => argument !== '--') +) { + const { base, comparisonBase, rangesByFile } = collectAddedLineRanges(root, requestedBase) + const files = [...rangesByFile.keys()] + if (files.length === 0) { + console.log(`Changed-code quality gate: no changed JavaScript or TypeScript since ${base}.`) + return 0 + } + + let failures = 0 + for (const scan of OXLINT_SCANS) { + const diagnostics = runOxlintScan(root, scan, files).filter((diagnostic) => + diagnosticTouchesAddedLines(diagnostic, rangesByFile, root) + ) + for (const diagnostic of diagnostics) { + printDiagnostic(diagnostic, root) + } + failures += diagnostics.length + console.log( + `${scan.label}: ${diagnostics.length} new finding(s) across ${files.length} changed file(s).` + ) + } + + if (failures > 0) { + console.error( + `Changed-code quality gate failed with ${failures} finding(s) since ${comparisonBase.slice(0, 12)}.` + ) + return 1 + } + console.log(`Changed-code quality gate passed since ${comparisonBase.slice(0, 12)}.`) + return 0 +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + process.exit(main()) +} diff --git a/config/scripts/check-changed-code-quality.test.mjs b/config/scripts/check-changed-code-quality.test.mjs new file mode 100644 index 000000000..0f68172a5 --- /dev/null +++ b/config/scripts/check-changed-code-quality.test.mjs @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest' +import { + diagnosticTouchesAddedLines, + overlapsAddedLines, + parseAddedLineRanges +} from './check-changed-code-quality.mjs' + +describe('changed-code quality line matching', () => { + it('parses added and replaced hunk ranges while ignoring deletions', () => { + const ranges = parseAddedLineRanges( + ['@@ -10,2 +10,3 @@', '@@ -20 +21 @@', '@@ -40,4 +42,0 @@', '@@ -50 +48,2 @@'].join('\n') + ) + + expect(ranges).toEqual([ + { start: 10, end: 12 }, + { start: 21, end: 21 }, + { start: 48, end: 49 } + ]) + }) + + it('matches diagnostics that overlap any added line', () => { + const ranges = [ + { start: 5, end: 7 }, + { start: 12, end: 12 } + ] + + expect(overlapsAddedLines(3, 5, ranges)).toBe(true) + expect(overlapsAddedLines(8, 11, ranges)).toBe(false) + expect(overlapsAddedLines(12, 14, ranges)).toBe(true) + }) + + it('normalizes absolute diagnostic paths before matching', () => { + const root = process.cwd() + const file = 'config/scripts/check-changed-code-quality.test.mjs' + const diagnostic = { + filename: `${root}/${file}`, + labels: [{ span: { line: 24 } }] + } + + expect( + diagnosticTouchesAddedLines(diagnostic, new Map([[file, [{ start: 24, end: 24 }]]]), root) + ).toBe(true) + }) +}) diff --git a/config/scripts/check-react-doctor-changed.mjs b/config/scripts/check-react-doctor-changed.mjs new file mode 100644 index 000000000..f659eefb3 --- /dev/null +++ b/config/scripts/check-react-doctor-changed.mjs @@ -0,0 +1,35 @@ +import { spawnSync } from 'node:child_process' +import process from 'node:process' +import { resolvePullRequestDiffBase } from './git-pull-request-diff-base.mjs' + +const requestedBase = + process.argv.slice(2).find((argument) => argument !== '--') ?? + process.env.ORCA_CODE_QUALITY_BASE ?? + 'origin/main' +const base = resolvePullRequestDiffBase(process.cwd(), requestedBase) +const pnpm = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm' +const result = spawnSync( + pnpm, + [ + 'dlx', + 'react-doctor@0.9.1', + '.', + '--yes', + '--scope', + 'lines', + '--base', + base, + '--include-untracked', + '--no-dead-code', + '--no-supply-chain', + '--no-telemetry', + '--blocking', + 'error' + ], + { stdio: 'inherit' } +) + +if (result.error) { + throw result.error +} +process.exit(result.status ?? 1) diff --git a/config/scripts/git-pull-request-diff-base.mjs b/config/scripts/git-pull-request-diff-base.mjs new file mode 100644 index 000000000..3e2021a1e --- /dev/null +++ b/config/scripts/git-pull-request-diff-base.mjs @@ -0,0 +1,23 @@ +import { execFileSync } from 'node:child_process' +import process from 'node:process' + +export function selectPullRequestDiffBase(requestedBase, headParents, eventName) { + if (eventName === 'pull_request' && headParents.length >= 2) { + return headParents[0] + } + return requestedBase +} + +export function resolvePullRequestDiffBase( + root, + requestedBase, + eventName = process.env.GITHUB_EVENT_NAME +) { + const [, ...headParents] = execFileSync('git', ['rev-list', '--parents', '-n', '1', 'HEAD'], { + cwd: root, + encoding: 'utf8' + }) + .trim() + .split(/\s+/) + return selectPullRequestDiffBase(requestedBase, headParents, eventName) +} diff --git a/config/scripts/git-pull-request-diff-base.test.mjs b/config/scripts/git-pull-request-diff-base.test.mjs new file mode 100644 index 000000000..4c1e0b849 --- /dev/null +++ b/config/scripts/git-pull-request-diff-base.test.mjs @@ -0,0 +1,19 @@ +import { describe, expect, it } from 'vitest' +import { selectPullRequestDiffBase } from './git-pull-request-diff-base.mjs' + +describe('pull request diff base selection', () => { + it('uses the merge commit first parent for pull request checkouts', () => { + expect( + selectPullRequestDiffBase('event-base', ['current-base', 'pull-request-head'], 'pull_request') + ).toBe('current-base') + }) + + it('keeps the requested base outside synthetic pull request merges', () => { + expect(selectPullRequestDiffBase('requested-base', ['parent'], 'pull_request')).toBe( + 'requested-base' + ) + expect(selectPullRequestDiffBase('requested-base', ['parent', 'other'], 'push')).toBe( + 'requested-base' + ) + }) +}) diff --git a/config/scripts/mobile-pairing-qrcode-import-plugin.test.mjs b/config/scripts/mobile-pairing-qrcode-import-plugin.test.mjs new file mode 100644 index 000000000..bed29fe18 --- /dev/null +++ b/config/scripts/mobile-pairing-qrcode-import-plugin.test.mjs @@ -0,0 +1,47 @@ +import { spawnSync } from 'node:child_process' +import { mkdtempSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import path from 'node:path' +import { describe, expect, it } from 'vitest' + +const pluginPath = path.resolve('config/oxlint-plugins/mobile-pairing-qrcode-import.mjs') +const oxlintPath = path.resolve( + process.platform === 'win32' ? 'node_modules/.bin/oxlint.cmd' : 'node_modules/.bin/oxlint' +) + +function lintSource(source) { + const directory = mkdtempSync(path.join(tmpdir(), 'orca-qrcode-import-lint-')) + const sourcePath = path.join(directory, 'sample.ts') + const configPath = path.join(directory, 'oxlint.json') + writeFileSync(sourcePath, source) + writeFileSync( + configPath, + JSON.stringify({ + categories: { correctness: 'off' }, + jsPlugins: [{ name: 'mobile-pairing', specifier: pluginPath }], + rules: { 'mobile-pairing/no-eager-qrcode-import': 'error' } + }) + ) + const result = spawnSync(oxlintPath, ['--config', configPath, '--format', 'json', sourcePath], { + encoding: 'utf8' + }) + if (result.error) { + throw result.error + } + return JSON.parse(result.stdout).diagnostics +} + +describe('mobile pairing qrcode import rule', () => { + it('rejects eager runtime imports', () => { + const diagnostics = lintSource("import QRCode from 'qrcode'\nvoid QRCode") + + expect(diagnostics.map((diagnostic) => diagnostic.code)).toEqual([ + 'mobile-pairing(no-eager-qrcode-import)' + ]) + }) + + it('allows type-only and lazy imports', () => { + expect(lintSource("import type QRCode from 'qrcode'\nlet qr: typeof QRCode")).toEqual([]) + expect(lintSource("const QRCode = await import('qrcode')\nvoid QRCode")).toEqual([]) + }) +}) diff --git a/config/scripts/zustand-selector-fanout-benchmark.mjs b/config/scripts/zustand-selector-fanout-benchmark.mjs new file mode 100644 index 000000000..300aef429 --- /dev/null +++ b/config/scripts/zustand-selector-fanout-benchmark.mjs @@ -0,0 +1,81 @@ +#!/usr/bin/env node +import { performance } from 'node:perf_hooks' +import process from 'node:process' +import { createStore } from 'zustand/vanilla' + +const SUBSCRIBERS = Number.parseInt(process.env.ORCA_ZUSTAND_BENCH_SUBSCRIBERS ?? '2500', 10) +const WRITES = Number.parseInt(process.env.ORCA_ZUSTAND_BENCH_WRITES ?? '2000', 10) +const MAX_MILLISECONDS_PER_WRITE = Number.parseFloat( + process.env.ORCA_ZUSTAND_BENCH_MAX_MS_PER_WRITE ?? '5' +) + +for (const [name, value] of [ + ['ORCA_ZUSTAND_BENCH_SUBSCRIBERS', SUBSCRIBERS], + ['ORCA_ZUSTAND_BENCH_WRITES', WRITES], + ['ORCA_ZUSTAND_BENCH_MAX_MS_PER_WRITE', MAX_MILLISECONDS_PER_WRITE] +]) { + if (!Number.isFinite(value) || value <= 0) { + throw new Error(`${name} must be positive, received ${value}`) + } +} + +function measureRound() { + const stableProjection = Object.freeze({ activeRepoId: 'repo-1' }) + const store = createStore(() => ({ unrelatedWrite: 0, stableProjection })) + let selectorRuns = 0 + let renderInvalidations = 0 + const unsubscribe = Array.from({ length: SUBSCRIBERS }, () => { + let previous = store.getState().stableProjection + return store.subscribe((state) => { + selectorRuns += 1 + const next = state.stableProjection + if (!Object.is(previous, next)) { + renderInvalidations += 1 + } + previous = next + }) + }) + + const start = performance.now() + for (let index = 1; index <= WRITES; index += 1) { + store.setState({ unrelatedWrite: index }) + } + const elapsed = performance.now() - start + for (const release of unsubscribe) { + release() + } + return { elapsed, selectorRuns, renderInvalidations } +} + +measureRound() +const rounds = Array.from({ length: 5 }, measureRound).sort( + (left, right) => left.elapsed - right.elapsed +) +const median = rounds[2] +const expectedSelectorRuns = SUBSCRIBERS * WRITES +const millisecondsPerWrite = median.elapsed / WRITES + +if (median.selectorRuns !== expectedSelectorRuns) { + throw new Error( + `Expected ${expectedSelectorRuns} selector runs, observed ${median.selectorRuns}; update the fan-out model.` + ) +} +if (median.renderInvalidations !== 0) { + throw new Error( + `${median.renderInvalidations} unrelated writes changed a stable selector result.` + ) +} + +console.log( + `Zustand fan-out: ${SUBSCRIBERS} subscribers × ${WRITES} unrelated writes = ${expectedSelectorRuns.toLocaleString()} selector runs` +) +console.log( + `Median ${median.elapsed.toFixed(2)} ms total, ${millisecondsPerWrite.toFixed(4)} ms/write, 0 render invalidations` +) + +if (process.argv.includes('--check') && millisecondsPerWrite > MAX_MILLISECONDS_PER_WRITE) { + console.error( + `Zustand fan-out exceeded ${MAX_MILLISECONDS_PER_WRITE.toFixed(2)} ms/write. Inspect selector work and store subscription growth.` + ) + process.exit(1) +} diff --git a/package.json b/package.json index afbdf38bf..8c6fe2147 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,14 @@ "scripts": { "format": "oxfmt --write .", "lint": "oxlint && pnpm run lint:switch-exhaustiveness && node config/scripts/check-styled-scrollbars.mjs && pnpm run check:quadratic-buffer-concat && pnpm run check:reliability-gates && pnpm run check:max-lines-ratchet && pnpm run verify:bundled-skill-guides && pnpm run verify:skill-bundle-manifest && pnpm run verify:localization-catalog && pnpm run verify:localization-coverage", + "audit:code-quality": "pnpm run audit:code-quality:native && pnpm run audit:code-quality:type-aware && pnpm run audit:react-doctor", + "audit:code-quality:native": "oxlint --config config/oxlint-code-quality.json --report-unused-disable-directives-severity warn src config tests mobile", + "audit:code-quality:type-aware": "oxlint --type-aware --config config/oxlint-code-quality-type-aware.json src config tests mobile", + "audit:react-doctor": "pnpm dlx react-doctor@0.9.1 . --yes --no-supply-chain --no-telemetry --blocking none", + "check:code-quality:changed": "node config/scripts/check-changed-code-quality.mjs", + "check:react-doctor:changed": "node config/scripts/check-react-doctor-changed.mjs", + "check:zustand-selector-fanout": "node config/scripts/zustand-selector-fanout-benchmark.mjs --check", + "doctor": "pnpm dlx react-doctor@0.9.1 . --no-telemetry", "lint:react-doctor": "oxlint --config config/oxlint-react-doctor.json", "lint:react-doctor:changed": "node config/scripts/lint-react-doctor-changed.mjs", "lint:switch-exhaustiveness": "oxlint --type-aware --config config/oxlint-switch-exhaustiveness.json src/main src/preload src/shared src/relay src/cli src/renderer/src config tests --quiet", @@ -104,6 +112,7 @@ "bench:startup": "pnpm run ensure:electron-runtime && node tools/benchmarks/startup-time-bench.mjs", "bench:daemon-coldstart": "pnpm run ensure:electron-runtime && node tools/benchmarks/daemon-coldstart-bench.mjs", "bench:main-thread-jank": "pnpm run ensure:electron-runtime && node tools/benchmarks/main-thread-jank-bench.mjs", + "bench:zustand-selector-fanout": "node config/scripts/zustand-selector-fanout-benchmark.mjs", "bench:multi-workspace-typing": "pnpm run ensure:electron-runtime && node config/scripts/run-multi-workspace-typing-bench.mjs", "bench:cold-park-reveal": "pnpm run ensure:electron-runtime && node tools/benchmarks/terminal-cold-park-reveal-bench.mjs", "bench:cold-park-resource": "pnpm run ensure:electron-runtime && node tools/benchmarks/terminal-cold-park-resource-bench.mjs", @@ -197,9 +206,9 @@ "mermaid": "^11.15.0", "monaco-editor": "^0.55.1", "oxfmt": "^0.52.0", - "oxlint": "^1.71.0", - "oxlint-plugin-react-doctor": "0.2.10", - "oxlint-tsgolint": "0.23.0", + "oxlint": "^1.75.0", + "oxlint-plugin-react-doctor": "0.9.1", + "oxlint-tsgolint": "7.0.2001", "pdfjs-dist": "^5.7.284", "pngjs": "^7.0.0", "radix-ui": "^1.6.2", @@ -230,7 +239,7 @@ "vitest": "^4.1.5", "vscode-oniguruma": "^2.0.1", "vscode-textmate": "^9.3.2", - "zustand": "^5.0.13" + "zustand": "^5.0.14" }, "optionalDependencies": { "sherpa-onnx-darwin-arm64": "1.12.37", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 109f230e4..2b1a656bd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -286,14 +286,14 @@ importers: specifier: ^0.52.0 version: 0.52.0 oxlint: - specifier: ^1.71.0 - version: 1.71.0(oxlint-tsgolint@0.23.0) + specifier: ^1.75.0 + version: 1.75.0(oxlint-tsgolint@7.0.2001) oxlint-plugin-react-doctor: - specifier: 0.2.10 - version: 0.2.10 + specifier: 0.9.1 + version: 0.9.1 oxlint-tsgolint: - specifier: 0.23.0 - version: 0.23.0 + specifier: 7.0.2001 + version: 7.0.2001 pdfjs-dist: specifier: ^5.7.284 version: 5.7.284 @@ -377,7 +377,7 @@ importers: version: rolldown-vite@7.3.1(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(@types/node@25.9.5)(jiti@2.7.0)(yaml@2.8.4) vitest: specifier: ^4.1.5 - version: 4.1.5(@types/node@25.9.5)(happy-dom@20.9.0)(msw@2.14.3(@types/node@25.9.5)(typescript@7.0.2))(rolldown-vite@7.3.1(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(@types/node@25.9.5)(jiti@2.7.0)(yaml@2.8.4)) + version: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@25.9.5)(happy-dom@20.9.0)(msw@2.14.3(@types/node@25.9.5)(typescript@7.0.2))(rolldown-vite@7.3.1(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(@types/node@25.9.5)(jiti@2.7.0)(yaml@2.8.4)) vscode-oniguruma: specifier: ^2.0.1 version: 2.0.1 @@ -385,8 +385,8 @@ importers: specifier: ^9.3.2 version: 9.3.2 zustand: - specifier: ^5.0.13 - version: 5.0.13(@types/react@19.2.17)(react@19.2.7)(use-sync-external-store@1.6.0(react@19.2.7)) + specifier: ^5.0.14 + version: 5.0.14(@types/react@19.2.17)(react@19.2.7)(use-sync-external-store@1.6.0(react@19.2.7)) optionalDependencies: sherpa-onnx-darwin-arm64: specifier: 1.12.37 @@ -1079,6 +1079,137 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@opentelemetry/api@1.9.1': + resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} + engines: {node: '>=8.0.0'} + + '@oxc-parser/binding-android-arm-eabi@0.141.0': + resolution: {integrity: sha512-jk7086MFvR/T4DG9IY7MKBVt1PMxvSZoz/TvnifodvS0pjghVwJHRttnAExhlwdMOgHv1TmLdENnbNpYk2zjvA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxc-parser/binding-android-arm64@0.141.0': + resolution: {integrity: sha512-a4XDQ27ZT7e7zwAlxJDTiCA7IBGWDuy2+MhFq85Of7XlBSmpkfcBFml11q0Zx6f7RMuI0B4xCtt2ytBS4yOptg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxc-parser/binding-darwin-arm64@0.141.0': + resolution: {integrity: sha512-m/kVk6rzYmBeHYnz+1Y5fod00AVTTxMbC71azFfm/zjx1j9XxwKtA0+VfkKuVMC8rbghb9TtfevnuWZa9OuPEg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxc-parser/binding-darwin-x64@0.141.0': + resolution: {integrity: sha512-o0X+6KZlfucWU/v5oKRQPwdFXsXAjW8jmpo/Gpw/qyKsbKtlfkHoeH9Bjp/m13TwjewvJnCkwF0DWzgpC4HjTQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxc-parser/binding-freebsd-x64@0.141.0': + resolution: {integrity: sha512-W5KbTnNkTMMMylqj6dYqnsXvkmESVPodPKYLJ5zdzIPdl9fUJtolkpUeSzYEbGGYB4a4A4avl3EePnZ/wLIdJg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxc-parser/binding-linux-arm-gnueabihf@0.141.0': + resolution: {integrity: sha512-g3dtbJa8zeOGK36Sr9cQavsdi5H/ie2hVjrSjIxsNAR1qZA40ZYVXnfdfoMAlq8CmB9qFL1yhsSCUHeNmdmt8w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm-musleabihf@0.141.0': + resolution: {integrity: sha512-e6hwQqd+3lvP13G2jxvFpoA7dzHcFLN+Mq47JCVMtdNHbbyBRo756JCtbbJH6ca8inTfyqZoqBmS3vhQlzAK2w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm64-gnu@0.141.0': + resolution: {integrity: sha512-vXz2BLAuypA+4MLyBg94pzEo6THVnzYnCtAjXoihIIQo0t2pnp/AmW+SH1EI+4VbuJnC//KplIJ5yyaCGua4jA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-arm64-musl@0.141.0': + resolution: {integrity: sha512-jMkS/EztNW34HKsXIaT/SoHcmtocq/vWhwFOVduF9kduuuRIVwfwQ6uxzIO+qPKSXdd2TXt54of0BJ2zFMXnmw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-ppc64-gnu@0.141.0': + resolution: {integrity: sha512-vo+MR+n3zQJ6Mq92hiP084NZcgDv5iJlVR02gMf28neMvVT1tKVm7VeiW/DxhdqOi3QLeaXIk9cUcLL1qrkngw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-gnu@0.141.0': + resolution: {integrity: sha512-oh80w+7RuiO5gBp9Jnoa/H8Qlt3JsHL2MkW+0dwEdlDMdslVZX/YsekSK6EeyEenY66/mhCfypsNATQ7Ph3qlQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-musl@0.141.0': + resolution: {integrity: sha512-LOyEmFA8sCnYbEXP1+iQvCC/P1YXHMA/t6x1Ksp0Y9VwhLFsiBJFzV1zIxrOIE2LKaGGhDjQ29xq9cbq6omDXA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-s390x-gnu@0.141.0': + resolution: {integrity: sha512-3wnwk/l1CvszVE5TJR1wSl/zSEfydRqrNhn6s7Vr9IzSJpUQIroqVsIoPARHRFA+FQwkxAFDAHDAasa7v8OobQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-gnu@0.141.0': + resolution: {integrity: sha512-qtyQVAAebFq57B2tifTlel3TgGqUtsYNI/e+p6aya9rN9lOZVTDvr215fGYSA9XWooxzMxDiVxkBLk2jQHbsOQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-musl@0.141.0': + resolution: {integrity: sha512-SkGV1nKw40roEc94pv5EaaeH2ay14G6+roe8Q0wIUC1LcEKxzKW921h7+ZuZX0D3q2Mb/7aSFmxEVqnko3lPRw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-openharmony-arm64@0.141.0': + resolution: {integrity: sha512-cVgDM7n8QziQqOaP5hNgUYfMG7S/ZeuPxFWXnnHRv7rh025COk0rfQ6eEdKG3j/GaUuyvNZN4ifF1J8KmuXLLA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxc-parser/binding-wasm32-wasi@0.141.0': + resolution: {integrity: sha512-HggH++Fkn3OilBn+bs3jpgIFQa34oMAyUUHy0vpGum+gt1Eb5nyLc8dNU/RAPSw6lsLrx7ncKtHSZE+3Sp0l2g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@oxc-parser/binding-win32-arm64-msvc@0.141.0': + resolution: {integrity: sha512-KLSEH9GwgbrqbJOjtGHt9STw96s+78yDzp7IDN8Lno+7Ut9sNBfZ4jYZIz4mD50qmWUjoOI7i9I6UENbhNbMZQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxc-parser/binding-win32-ia32-msvc@0.141.0': + resolution: {integrity: sha512-9UVWUOOCI/1YkiSSNjg2zyBJYM9E/t1A/8GNobd48JDn/fQ6mzxcVO3H08jb3rAaW/B1VBf8eCORTvSsO9T08g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxc-parser/binding-win32-x64-msvc@0.141.0': + resolution: {integrity: sha512-HI/wsvbWT5RHHw5c37D0fEgeTd8/1Q4OJs5jUmEBc17VZFG6SsCIe4barq7NsAPPks/JW+3ayi3Rp+PQI5h4Kg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@oxc-project/runtime@0.101.0': resolution: {integrity: sha512-t3qpfVZIqSiLQ5Kqt/MC4Ge/WCOGrrcagAdzTcDaggupjiGxUx4nJF2v6wUCXWSzWHn5Ns7XLv13fCJEwCOERQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1086,6 +1217,9 @@ packages: '@oxc-project/types@0.101.0': resolution: {integrity: sha512-nuFhqlUzJX+gVIPPfuE6xurd4lST3mdcWOhyK/rZO0B9XWMKm79SuszIQEnSMmmDhq1DC8WWVYGVd+6F93o1gQ==} + '@oxc-project/types@0.141.0': + resolution: {integrity: sha512-S4as7z0j0xQkXcJlyY5ehntwK8/wRkQb9Cyqw+J/N2rkWGQGK0SxD6X6DhQTc7qsxVTBxXbxZtBJh3mr3PtIzQ==} + '@oxfmt/binding-android-arm-eabi@0.52.0': resolution: {integrity: sha512-17EMSJnQ9g+upVHrAUYDMfH5lvRKQ9Nvg8WtEoH72oDr1VpWz+7/o3tD97U1EToen2YAQ/68JmtDYkQUi20dfQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1208,154 +1342,154 @@ packages: cpu: [x64] os: [win32] - '@oxlint-tsgolint/darwin-arm64@0.23.0': - resolution: {integrity: sha512-gOs9PVr2wEg4ox9z0aJo+RKhhImW86YL5N6yav8BK/rgPsIrwN/igSZ+pbRr723NFvUNKde9fgMhRA6JrXAOZw==} + '@oxlint-tsgolint/darwin-arm64@7.0.2001': + resolution: {integrity: sha512-CUJEdbSZ54+Xy9OXqOhWLTKZKV0BBiV7C2i/ygyVmXtkUNXx5YCzN8DpSSshTAKktoL7S+tnQ/ftFG/i7X896w==} cpu: [arm64] os: [darwin] - '@oxlint-tsgolint/darwin-x64@0.23.0': - resolution: {integrity: sha512-kjJ8B+7n4tB9VJdxS5A9GdJt6/bYpzbu4lXp2uO1S3sRmCB5gDEABlGoiePNApRWaW+xqL4b4xgiE727jSLhuA==} + '@oxlint-tsgolint/darwin-x64@7.0.2001': + resolution: {integrity: sha512-pXfBb5BqONCcgrXQNUZWXgiYmRSWJzd97S8i41VVOh6ut0tyo+cJ5FKFpczDHxiVNfj/3e7c9B4MtztNdpIVCw==} cpu: [x64] os: [darwin] - '@oxlint-tsgolint/linux-arm64@0.23.0': - resolution: {integrity: sha512-6dCZuKNu135seMXilkRk9SpCx6i1XgmiipYGalLij5WVRX6ZYS8c4xI7preN/zv9fCXhsQclTIMDu2Y/cytTjw==} + '@oxlint-tsgolint/linux-arm64@7.0.2001': + resolution: {integrity: sha512-roP7zujb/QDPzDwEKsFFpzNHHy91/Y7oX9vQXk78ekyZtcQj1QXDIMH33gjDdHBfRl4K9pZ36xhRgrP4Zr+R8A==} cpu: [arm64] os: [linux] - '@oxlint-tsgolint/linux-x64@0.23.0': - resolution: {integrity: sha512-3bdilnyA7kmSTjK27rvjIjSxL5SIg3wt7vwNiRkouWB83ytssyKnuGvxSYJxgMEmFpSutzaBzcCUM2jDtPGcgA==} + '@oxlint-tsgolint/linux-x64@7.0.2001': + resolution: {integrity: sha512-UDezNqdECVmngu2TPnjaS1YoAmcTaBoI5lV9vk3VahBxoi+I5r9k3iJTT7qZoYWOXTD/7T7bNcwRgrocR6BscQ==} cpu: [x64] os: [linux] - '@oxlint-tsgolint/win32-arm64@0.23.0': - resolution: {integrity: sha512-j+OEp44SVYiQ+ZD+uttsX7u6L9SvmbbQ77SO1pSFCcJlsVMeCk8qZsjhKfGKuT/jIA+ipOJMVs/+pqUfObBWNw==} + '@oxlint-tsgolint/win32-arm64@7.0.2001': + resolution: {integrity: sha512-uJZhqB6pdXLuN+AD1F5082byyQti/NPmJA77GtcFlmT2HzRelqbNls3SaIqxpjdFgvSBF9g0yOKGBkGFg7kX8Q==} cpu: [arm64] os: [win32] - '@oxlint-tsgolint/win32-x64@0.23.0': - resolution: {integrity: sha512-5MyjFuqf+g8OUPJBSGWHJtmoWnzFJYyOg4To9WMQshZYEWig/vtu7JtJ03VWnzHv9LJkAUeApY0gVCOywFR/iQ==} + '@oxlint-tsgolint/win32-x64@7.0.2001': + resolution: {integrity: sha512-FkDRm8hx9OwzGQqyWG1tO5QrTLRApff9DzSgpz9QZau37BR8d1VYKOxMLGf6shPZntJFoTwIIJYT68VndYDCog==} cpu: [x64] os: [win32] - '@oxlint/binding-android-arm-eabi@1.71.0': - resolution: {integrity: sha512-ImGmd1njEg4FEJH03jhRnveEegtO3czCtfptvaHivKAZQIYATbVFBrrzbaYMYv0oJioTnxZAZVSyV+oL7W8S2g==} + '@oxlint/binding-android-arm-eabi@1.75.0': + resolution: {integrity: sha512-lutovtFzJqlRaqpZrCqSSGaHZzl9nIxxpjLzhSRLunN6dCLylj0uzlCyQGaQDIys7rrv8kVXiFO+R4Zpn0bX7g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxlint/binding-android-arm64@1.71.0': - resolution: {integrity: sha512-4A5BEexBrwY1YFF8Kiq/lp/wQPRG79G3BWIE1FuWaM5MvmpYSd+7ZySVcKkHdwo0UDzdQGddp6pD9mpctMqLnw==} + '@oxlint/binding-android-arm64@1.75.0': + resolution: {integrity: sha512-hXI0hDgHkw4w5nfru72aG7y+2iQJmC4waH/KV6H/hbgA6yAP5jYNx0P9yug15Hs0tWl/+mda3Jjn/2gmDT48tw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxlint/binding-darwin-arm64@1.71.0': - resolution: {integrity: sha512-9wJA9GJulLwS2usU3CEisI/ESDO1n1z9eyTCvApMDrAkbJ1ve0mORgTMjcWWsKxkzkeZ2N/Gpra5IQE7x8tYgQ==} + '@oxlint/binding-darwin-arm64@1.75.0': + resolution: {integrity: sha512-D91BWbK/dMYfCcrghspPIuKs2D9LF4Z/OabVSQjw1AO6PWxArD7teDA48bm0ySFqWDaPVqmQRl5GMWNglTXyrQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxlint/binding-darwin-x64@1.71.0': - resolution: {integrity: sha512-PlLCjS06V0PeJMAJwzjrExw1sYNW9Gch3JtNlcwwZDXGlTYDuwHNN89zYH8LTXFfgkVtsYvs2nv0FqrzyuFDzg==} + '@oxlint/binding-darwin-x64@1.75.0': + resolution: {integrity: sha512-02mpwzf12BonZ6PT0TuQoomvEh2kVl2WGBIKWezCyToIS+rYkQZ6GXnARBAl9A4Ovm2V+Xe7M4KretyqmmcnJQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxlint/binding-freebsd-x64@1.71.0': - resolution: {integrity: sha512-Lhil7bWre0ncxbUoDoxfS0JzpTz17BRQKW7iwoAUY8GJ66+WwJEfYPCFJ1P0WgVZR5/O/b3Q2pENlHOjeXLOGQ==} + '@oxlint/binding-freebsd-x64@1.75.0': + resolution: {integrity: sha512-qZJgLnDaBsiL5YESx2t/TZ8eXkL9fEkKoXEdzegROhlz9A0lgyGnZ0dAzJrh7LJAHQl2K9RdRueN2s/9N7+odg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxlint/binding-linux-arm-gnueabihf@1.71.0': - resolution: {integrity: sha512-Oo9/L58PYD3RC0x05d2upAPLllHytTjHQGsnC06P6Ynn7jKkp5mdImQxXdJ3+FnBaKspNpGogzgVsi6g872LiA==} + '@oxlint/binding-linux-arm-gnueabihf@1.75.0': + resolution: {integrity: sha512-7XlaWA5BJD3XpCfrEqjEe6Zseeb14S7QGa304XfwKignRaKQ+eIj775BQ7nIslggWickl4IsPUFqJ+/gAyNHVg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm-musleabihf@1.71.0': - resolution: {integrity: sha512-mSHfyfgJrEbyIR29ejaeS50BdPk+GoNPlC1dckpDiUZbJAIel68sjSMdOt4WY0/gva+ECC7FNITQkxMJU+vSBw==} + '@oxlint/binding-linux-arm-musleabihf@1.75.0': + resolution: {integrity: sha512-av6Tpv8yrcMMMOadOqENBhlsLRcGFXXwoQ0hzHhsmS9FJ4Wioy8we427GbcMe2XTxmL2e60T67H1Dyr3up+tAA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm64-gnu@1.71.0': - resolution: {integrity: sha512-n9yY4M2tiy3aij4AqtlnspzpfdpeT5JQfK2/w2d8oyp5W0FRwOb1dIeX99nORNcxGr08iD9bH8N5XFz3I2iy8w==} + '@oxlint/binding-linux-arm64-gnu@1.75.0': + resolution: {integrity: sha512-WcUhd8fHT5plrA14lANevl+hOl815mVI5t2hU21oFWrZKFXIVV/Sr4rWQV0NzSvzBupbMLNc5ErEA6Ehxh5jMg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-arm64-musl@1.71.0': - resolution: {integrity: sha512-fJZrs5sDZtTaPIOiemRQQmo82Ezy+vOGXemPc4Ok7iVVsYsFa7SlW6Z5XN819VfsqBHRm3NJ3rTdnR8+bJYJdQ==} + '@oxlint/binding-linux-arm64-musl@1.75.0': + resolution: {integrity: sha512-UWzp5wRHFe/ESO3+eEaxXsTkYTGLYjnTsi/I5neEacXSItQ6WNleapfOAeA4x2b8nyhJ4uQxqvtv9pHv8kWJtQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxlint/binding-linux-ppc64-gnu@1.71.0': - resolution: {integrity: sha512-cwl7VKGERIy9p+G+AvZdfy/06q0aHXaTt/mMRReC751iuNYJgqKjB7NydXSS30nBT9vtr2tunciOtrR4fD6FUA==} + '@oxlint/binding-linux-ppc64-gnu@1.75.0': + resolution: {integrity: sha512-XEVRwGMLKCUKrvhLAz4F6AIh8MJrQVdSZtAmPpRZt9tGPsUnamPOcl3dS/ZQzJnar/Ymgc//+xho0L60Emzuxg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-gnu@1.71.0': - resolution: {integrity: sha512-eZ8ieVXvzGi8jr7+ybQGPK2STw3mldfxZlgA2738iflfB/rzA69sE6m5rDRpQaxC7dpm745Enlh1Tod0QAk9Gg==} + '@oxlint/binding-linux-riscv64-gnu@1.75.0': + resolution: {integrity: sha512-mAG4DUXqfLC8cTjMD2kt3jDmVzFREYtDyeLNdLdsCcBc4Zbl2EMuiFektGBilQwkNjYnMvCqJs55U+Hyb+b+jw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-musl@1.71.0': - resolution: {integrity: sha512-puMDbQYe6+NXwfMusojoA7CXGn2b3utukmd23PQqc1E3XhVCwyZ+FueSMzDYeNgDV2dUfIVXAAKZBcFDeCL6sA==} + '@oxlint/binding-linux-riscv64-musl@1.75.0': + resolution: {integrity: sha512-95hrAvriAlI+pekSomTFIn0+bawMDlDwTNVmdjsFusTHyL2JWh7TWvRNG/Lkim72uN8OiCcO9wcaC6omLP5E3w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxlint/binding-linux-s390x-gnu@1.71.0': - resolution: {integrity: sha512-4NJLxBs1ujISCt3L/1FcywLs73PWtJuw+piD6feK2V6h6OS6P7xu9/sWt1DTRLibe6QCzmfZzmM/2HPORoV/Lg==} + '@oxlint/binding-linux-s390x-gnu@1.75.0': + resolution: {integrity: sha512-4b6f2+FrtruAESrCqIKcrarzfrSx+wk2QNcp+RT91/Prc+pMQMAfyZ1rG1c3tFQNl8Bc616tx40uNXyxNBRPbQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-gnu@1.71.0': - resolution: {integrity: sha512-cFDaiR8L3430qp88tfZnvFlt3KotFhR/DlbIL0nHOMMYiG/9Wy4l+6f7t8G8pTa9bd8Lt8+M0y/qjRQ/xcB74g==} + '@oxlint/binding-linux-x64-gnu@1.75.0': + resolution: {integrity: sha512-nshAhrUvXFUWOvqQ2soIw7HFNWvpvEV4o0cYSqPtzLiPF5gKyYTDOOTJ6Rn8g8K/iGvPIrbDA4v8+5MvnjJrrg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-musl@1.71.0': - resolution: {integrity: sha512-orfixdt76KlpNly9z0PkWBBNfwjKz+JFVLP/7wnVchlKNU9Dpt9InU/ZggeSej6fC7qwHmHNOGlhLnQXcYoGuA==} + '@oxlint/binding-linux-x64-musl@1.75.0': + resolution: {integrity: sha512-e4jNxLKnxLC6sYBQRxrI2pgIIxnmMtF8U/VwNYcjTT/CLS+spH624cYVnj07bTKwaEWT37/e025isOs6j/0xqA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxlint/binding-openharmony-arm64@1.71.0': - resolution: {integrity: sha512-9emQu2lAp6yhPB3XuI+++vR+l/o6JR1X+EpxwcumPdQXBWXEPAsquPGL7l158EqU8SebQMXTUa/S5zN98juyHw==} + '@oxlint/binding-openharmony-arm64@1.75.0': + resolution: {integrity: sha512-hZ2lH+1qLf/DiEP9UWuQTK2JWj/BgvMB4jhIV4SmNU1wfEiYYX4TynQyAZXx0j9X4qRYizAL042SKaV+8ynh4w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxlint/binding-win32-arm64-msvc@1.71.0': - resolution: {integrity: sha512-bd5kI8spYwTm3BILDtGhi73zoup5dw8MlPQNT8YB3BD5UIsjNe3K9/4ctrzQMX4SZMoK5HgzVLkLJzacEXB7fA==} + '@oxlint/binding-win32-arm64-msvc@1.75.0': + resolution: {integrity: sha512-Ilj6PNzGDS3bCU0MSJH7Msh0NhH+T/mRp2shwg+q+GHeVlPwP5LEboW96aW+3kVKFk6zYZy1Xi5pZkqZh6X8KQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxlint/binding-win32-ia32-msvc@1.71.0': - resolution: {integrity: sha512-W4HvOHGzVLHcrmFu+bMrJlho+/yrlX5ZNdJZqGe8MEldkQG+RHYhxxad9P4jvWAYFmIqUA5i9DQ8QsJqSU9GIw==} + '@oxlint/binding-win32-ia32-msvc@1.75.0': + resolution: {integrity: sha512-QVit2nOEOiPhkmsrksPSkoGCdnZRNkspt8fwoYyP09te1VEbnSj4LAxua4rc8FKTmWkySVe05j8iz9GXYfF1AQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxlint/binding-win32-x64-msvc@1.71.0': - resolution: {integrity: sha512-D2kyEIPHk/G/wiZLnwTVC/sVst+T/lKldVOjAFpgTIBUAOlry72e5OiapDbDBF4LfJLkN5ypJb/8Eu6yJzkveQ==} + '@oxlint/binding-win32-x64-msvc@1.75.0': + resolution: {integrity: sha512-DSxnNkBUAYARPwJtR12Ig3deWr8w0H997xP6jy33i+e0SyYJw8FKuz4+cZtpmPEhQmvlPJE3X/2vNxDmLkd/rA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -5135,6 +5269,10 @@ packages: outvariant@1.4.3: resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + oxc-parser@0.141.0: + resolution: {integrity: sha512-uFkGGr1KMWd6aWv9UAqooYrN78trw8MWWmoPvgWokfBEUq1+eiIQ+qfj3wokhy0fxtZWZk+0dHoS7/yRTJtd6w==} + engines: {node: ^20.19.0 || >=22.12.0} + oxfmt@0.52.0: resolution: {integrity: sha512-nJlYM35F64zTDMecCNhoHNkf+D/eHv7xcjj9XDSj+bFAVtN93m7v8DQMdHd6nDG6Akf/kEYYHmDUBs2Dz27Sug==} engines: {node: ^20.19.0 || >=22.12.0} @@ -5148,20 +5286,20 @@ packages: vite-plus: optional: true - oxlint-plugin-react-doctor@0.2.10: - resolution: {integrity: sha512-n36QdOLz4k9EEWod+vhki9/h29x/PL4nS91nWSk6AIr7HAL+rc6fkwUcVLgg3NhUVlaL/VOfYiIm4cVxyIIdGg==} - engines: {node: ^20.19.0 || >=22.12.0} + oxlint-plugin-react-doctor@0.9.1: + resolution: {integrity: sha512-yCW8USbiuszbVsUMN4fL1iU7mRu3Ae3w96+k/xqCyWvW6bF6DzCMtLy5N/w6WLRX78iQsWxVqW/SEgzRBXLfsA==} + engines: {node: ^20.19.0 || >=22.13.0} - oxlint-tsgolint@0.23.0: - resolution: {integrity: sha512-3mBv3CoPbh8dFbzfDGIWa2ytZjn2v+3EX4aKRXjIhsoGFzG8GCjfRirz3rwZf1wYbZzsNLTSgpw8VjQuWdp/jA==} + oxlint-tsgolint@7.0.2001: + resolution: {integrity: sha512-KjK/XLcXr1DSyonKhsuFqJRiuKqcyG9j3LJ8nkOsrLzGvodBPqzHOKauy10asLMDI0sUpvb+1sxlzff3udZvfg==} hasBin: true - oxlint@1.71.0: - resolution: {integrity: sha512-U1m1X+C0vDj7DC1e13IoZULzEcPczE7UOMTs8VlZGHUEIUaSTZKo5qkPsQEfzpgnQ29Pea/w3Xntk62UCecxZw==} + oxlint@1.75.0: + resolution: {integrity: sha512-m9WzjRcRYA/uqIZDa9tclrieoPJ/ln1QYTKdFx6NUOs8uY5DiHlIwRQoCrHT6OM6O3ww3l2skY5gO7G7ZphE7g==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: - oxlint-tsgolint: '>=0.22.1' + oxlint-tsgolint: '>=7.0.2001' vite-plus: '*' peerDependenciesMeta: oxlint-tsgolint: @@ -6432,8 +6570,8 @@ packages: zod@4.4.3: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} - zustand@5.0.13: - resolution: {integrity: sha512-efI2tVaVQPqtOh114loML/Z80Y4NP3yc+Ff0fYiZJPauNeWZeIp/bRFD7I9bfmCOYBh/PHxlglQ9+wvlwnPikQ==} + zustand@5.0.14: + resolution: {integrity: sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==} engines: {node: '>=12.20.0'} peerDependencies: '@types/react': '>=18.0.0' @@ -7159,10 +7297,79 @@ snapshots: '@open-draft/until@2.1.0': optional: true + '@opentelemetry/api@1.9.1': + optional: true + + '@oxc-parser/binding-android-arm-eabi@0.141.0': + optional: true + + '@oxc-parser/binding-android-arm64@0.141.0': + optional: true + + '@oxc-parser/binding-darwin-arm64@0.141.0': + optional: true + + '@oxc-parser/binding-darwin-x64@0.141.0': + optional: true + + '@oxc-parser/binding-freebsd-x64@0.141.0': + optional: true + + '@oxc-parser/binding-linux-arm-gnueabihf@0.141.0': + optional: true + + '@oxc-parser/binding-linux-arm-musleabihf@0.141.0': + optional: true + + '@oxc-parser/binding-linux-arm64-gnu@0.141.0': + optional: true + + '@oxc-parser/binding-linux-arm64-musl@0.141.0': + optional: true + + '@oxc-parser/binding-linux-ppc64-gnu@0.141.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-gnu@0.141.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-musl@0.141.0': + optional: true + + '@oxc-parser/binding-linux-s390x-gnu@0.141.0': + optional: true + + '@oxc-parser/binding-linux-x64-gnu@0.141.0': + optional: true + + '@oxc-parser/binding-linux-x64-musl@0.141.0': + optional: true + + '@oxc-parser/binding-openharmony-arm64@0.141.0': + optional: true + + '@oxc-parser/binding-wasm32-wasi@0.141.0': + dependencies: + '@emnapi/core': 1.11.2 + '@emnapi/runtime': 1.11.2 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2) + optional: true + + '@oxc-parser/binding-win32-arm64-msvc@0.141.0': + optional: true + + '@oxc-parser/binding-win32-ia32-msvc@0.141.0': + optional: true + + '@oxc-parser/binding-win32-x64-msvc@0.141.0': + optional: true + '@oxc-project/runtime@0.101.0': {} '@oxc-project/types@0.101.0': {} + '@oxc-project/types@0.141.0': {} + '@oxfmt/binding-android-arm-eabi@0.52.0': optional: true @@ -7220,79 +7427,79 @@ snapshots: '@oxfmt/binding-win32-x64-msvc@0.52.0': optional: true - '@oxlint-tsgolint/darwin-arm64@0.23.0': + '@oxlint-tsgolint/darwin-arm64@7.0.2001': optional: true - '@oxlint-tsgolint/darwin-x64@0.23.0': + '@oxlint-tsgolint/darwin-x64@7.0.2001': optional: true - '@oxlint-tsgolint/linux-arm64@0.23.0': + '@oxlint-tsgolint/linux-arm64@7.0.2001': optional: true - '@oxlint-tsgolint/linux-x64@0.23.0': + '@oxlint-tsgolint/linux-x64@7.0.2001': optional: true - '@oxlint-tsgolint/win32-arm64@0.23.0': + '@oxlint-tsgolint/win32-arm64@7.0.2001': optional: true - '@oxlint-tsgolint/win32-x64@0.23.0': + '@oxlint-tsgolint/win32-x64@7.0.2001': optional: true - '@oxlint/binding-android-arm-eabi@1.71.0': + '@oxlint/binding-android-arm-eabi@1.75.0': optional: true - '@oxlint/binding-android-arm64@1.71.0': + '@oxlint/binding-android-arm64@1.75.0': optional: true - '@oxlint/binding-darwin-arm64@1.71.0': + '@oxlint/binding-darwin-arm64@1.75.0': optional: true - '@oxlint/binding-darwin-x64@1.71.0': + '@oxlint/binding-darwin-x64@1.75.0': optional: true - '@oxlint/binding-freebsd-x64@1.71.0': + '@oxlint/binding-freebsd-x64@1.75.0': optional: true - '@oxlint/binding-linux-arm-gnueabihf@1.71.0': + '@oxlint/binding-linux-arm-gnueabihf@1.75.0': optional: true - '@oxlint/binding-linux-arm-musleabihf@1.71.0': + '@oxlint/binding-linux-arm-musleabihf@1.75.0': optional: true - '@oxlint/binding-linux-arm64-gnu@1.71.0': + '@oxlint/binding-linux-arm64-gnu@1.75.0': optional: true - '@oxlint/binding-linux-arm64-musl@1.71.0': + '@oxlint/binding-linux-arm64-musl@1.75.0': optional: true - '@oxlint/binding-linux-ppc64-gnu@1.71.0': + '@oxlint/binding-linux-ppc64-gnu@1.75.0': optional: true - '@oxlint/binding-linux-riscv64-gnu@1.71.0': + '@oxlint/binding-linux-riscv64-gnu@1.75.0': optional: true - '@oxlint/binding-linux-riscv64-musl@1.71.0': + '@oxlint/binding-linux-riscv64-musl@1.75.0': optional: true - '@oxlint/binding-linux-s390x-gnu@1.71.0': + '@oxlint/binding-linux-s390x-gnu@1.75.0': optional: true - '@oxlint/binding-linux-x64-gnu@1.71.0': + '@oxlint/binding-linux-x64-gnu@1.75.0': optional: true - '@oxlint/binding-linux-x64-musl@1.71.0': + '@oxlint/binding-linux-x64-musl@1.75.0': optional: true - '@oxlint/binding-openharmony-arm64@1.71.0': + '@oxlint/binding-openharmony-arm64@1.75.0': optional: true - '@oxlint/binding-win32-arm64-msvc@1.71.0': + '@oxlint/binding-win32-arm64-msvc@1.75.0': optional: true - '@oxlint/binding-win32-ia32-msvc@1.71.0': + '@oxlint/binding-win32-ia32-msvc@1.75.0': optional: true - '@oxlint/binding-win32-x64-msvc@1.71.0': + '@oxlint/binding-win32-x64-msvc@1.75.0': optional: true '@parcel/watcher-android-arm64@2.5.6': @@ -11417,6 +11624,31 @@ snapshots: outvariant@1.4.3: optional: true + oxc-parser@0.141.0: + dependencies: + '@oxc-project/types': 0.141.0 + optionalDependencies: + '@oxc-parser/binding-android-arm-eabi': 0.141.0 + '@oxc-parser/binding-android-arm64': 0.141.0 + '@oxc-parser/binding-darwin-arm64': 0.141.0 + '@oxc-parser/binding-darwin-x64': 0.141.0 + '@oxc-parser/binding-freebsd-x64': 0.141.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.141.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.141.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.141.0 + '@oxc-parser/binding-linux-arm64-musl': 0.141.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.141.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.141.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.141.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.141.0 + '@oxc-parser/binding-linux-x64-gnu': 0.141.0 + '@oxc-parser/binding-linux-x64-musl': 0.141.0 + '@oxc-parser/binding-openharmony-arm64': 0.141.0 + '@oxc-parser/binding-wasm32-wasi': 0.141.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.141.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.141.0 + '@oxc-parser/binding-win32-x64-msvc': 0.141.0 + oxfmt@0.52.0: dependencies: tinypool: 2.1.0 @@ -11441,43 +11673,44 @@ snapshots: '@oxfmt/binding-win32-ia32-msvc': 0.52.0 '@oxfmt/binding-win32-x64-msvc': 0.52.0 - oxlint-plugin-react-doctor@0.2.10: + oxlint-plugin-react-doctor@0.9.1: dependencies: '@typescript-eslint/types': 8.60.0 eslint-scope: 9.1.2 eslint-visitor-keys: 5.0.1 + oxc-parser: 0.141.0 - oxlint-tsgolint@0.23.0: + oxlint-tsgolint@7.0.2001: optionalDependencies: - '@oxlint-tsgolint/darwin-arm64': 0.23.0 - '@oxlint-tsgolint/darwin-x64': 0.23.0 - '@oxlint-tsgolint/linux-arm64': 0.23.0 - '@oxlint-tsgolint/linux-x64': 0.23.0 - '@oxlint-tsgolint/win32-arm64': 0.23.0 - '@oxlint-tsgolint/win32-x64': 0.23.0 + '@oxlint-tsgolint/darwin-arm64': 7.0.2001 + '@oxlint-tsgolint/darwin-x64': 7.0.2001 + '@oxlint-tsgolint/linux-arm64': 7.0.2001 + '@oxlint-tsgolint/linux-x64': 7.0.2001 + '@oxlint-tsgolint/win32-arm64': 7.0.2001 + '@oxlint-tsgolint/win32-x64': 7.0.2001 - oxlint@1.71.0(oxlint-tsgolint@0.23.0): + oxlint@1.75.0(oxlint-tsgolint@7.0.2001): optionalDependencies: - '@oxlint/binding-android-arm-eabi': 1.71.0 - '@oxlint/binding-android-arm64': 1.71.0 - '@oxlint/binding-darwin-arm64': 1.71.0 - '@oxlint/binding-darwin-x64': 1.71.0 - '@oxlint/binding-freebsd-x64': 1.71.0 - '@oxlint/binding-linux-arm-gnueabihf': 1.71.0 - '@oxlint/binding-linux-arm-musleabihf': 1.71.0 - '@oxlint/binding-linux-arm64-gnu': 1.71.0 - '@oxlint/binding-linux-arm64-musl': 1.71.0 - '@oxlint/binding-linux-ppc64-gnu': 1.71.0 - '@oxlint/binding-linux-riscv64-gnu': 1.71.0 - '@oxlint/binding-linux-riscv64-musl': 1.71.0 - '@oxlint/binding-linux-s390x-gnu': 1.71.0 - '@oxlint/binding-linux-x64-gnu': 1.71.0 - '@oxlint/binding-linux-x64-musl': 1.71.0 - '@oxlint/binding-openharmony-arm64': 1.71.0 - '@oxlint/binding-win32-arm64-msvc': 1.71.0 - '@oxlint/binding-win32-ia32-msvc': 1.71.0 - '@oxlint/binding-win32-x64-msvc': 1.71.0 - oxlint-tsgolint: 0.23.0 + '@oxlint/binding-android-arm-eabi': 1.75.0 + '@oxlint/binding-android-arm64': 1.75.0 + '@oxlint/binding-darwin-arm64': 1.75.0 + '@oxlint/binding-darwin-x64': 1.75.0 + '@oxlint/binding-freebsd-x64': 1.75.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.75.0 + '@oxlint/binding-linux-arm-musleabihf': 1.75.0 + '@oxlint/binding-linux-arm64-gnu': 1.75.0 + '@oxlint/binding-linux-arm64-musl': 1.75.0 + '@oxlint/binding-linux-ppc64-gnu': 1.75.0 + '@oxlint/binding-linux-riscv64-gnu': 1.75.0 + '@oxlint/binding-linux-riscv64-musl': 1.75.0 + '@oxlint/binding-linux-s390x-gnu': 1.75.0 + '@oxlint/binding-linux-x64-gnu': 1.75.0 + '@oxlint/binding-linux-x64-musl': 1.75.0 + '@oxlint/binding-openharmony-arm64': 1.75.0 + '@oxlint/binding-win32-arm64-msvc': 1.75.0 + '@oxlint/binding-win32-ia32-msvc': 1.75.0 + '@oxlint/binding-win32-x64-msvc': 1.75.0 + oxlint-tsgolint: 7.0.2001 p-cancelable@2.1.1: {} @@ -12713,7 +12946,7 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vitest@4.1.5(@types/node@25.9.5)(happy-dom@20.9.0)(msw@2.14.3(@types/node@25.9.5)(typescript@7.0.2))(rolldown-vite@7.3.1(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(@types/node@25.9.5)(jiti@2.7.0)(yaml@2.8.4)): + vitest@4.1.5(@opentelemetry/api@1.9.1)(@types/node@25.9.5)(happy-dom@20.9.0)(msw@2.14.3(@types/node@25.9.5)(typescript@7.0.2))(rolldown-vite@7.3.1(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(@types/node@25.9.5)(jiti@2.7.0)(yaml@2.8.4)): dependencies: '@vitest/expect': 4.1.5 '@vitest/mocker': 4.1.5(msw@2.14.3(@types/node@25.9.5)(typescript@7.0.2))(rolldown-vite@7.3.1(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(@types/node@25.9.5)(jiti@2.7.0)(yaml@2.8.4)) @@ -12736,6 +12969,7 @@ snapshots: vite: rolldown-vite@7.3.1(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(@types/node@25.9.5)(jiti@2.7.0)(yaml@2.8.4) why-is-node-running: 2.3.0 optionalDependencies: + '@opentelemetry/api': 1.9.1 '@types/node': 25.9.5 happy-dom: 20.9.0 transitivePeerDependencies: @@ -12877,7 +13111,7 @@ snapshots: zod@4.4.3: {} - zustand@5.0.13(@types/react@19.2.17)(react@19.2.7)(use-sync-external-store@1.6.0(react@19.2.7)): + zustand@5.0.14(@types/react@19.2.17)(react@19.2.7)(use-sync-external-store@1.6.0(react@19.2.7)): optionalDependencies: '@types/react': 19.2.17 react: 19.2.7 diff --git a/src/renderer/src/components/sidebar/WorktreeCard.tsx b/src/renderer/src/components/sidebar/WorktreeCard.tsx index 887fa64a9..86f02f7cf 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.tsx @@ -1114,14 +1114,15 @@ const WorktreeCard = React.memo(function WorktreeCard({ }, [hoverReview, openTaskPage, repo] ) + const hoverReviewProvider = hoverReview?.provider const hasExplicitLinkedReview = - (hoverReview?.provider === 'github' && worktree.linkedPR !== null) || - (hoverReview?.provider === 'gitlab' && linkedGitLabMR !== null) || - (hoverReview?.provider === 'bitbucket' && linkedBitbucketPR !== null) || - (hoverReview?.provider === 'azure-devops' && linkedAzureDevOpsPR !== null) || - (hoverReview?.provider === 'gitea' && linkedGiteaPR !== null) + (hoverReviewProvider === 'github' && worktree.linkedPR !== null) || + (hoverReviewProvider === 'gitlab' && linkedGitLabMR !== null) || + (hoverReviewProvider === 'bitbucket' && linkedBitbucketPR !== null) || + (hoverReviewProvider === 'azure-devops' && linkedAzureDevOpsPR !== null) || + (hoverReviewProvider === 'gitea' && linkedGiteaPR !== null) const handleUnlinkReview = useCallback(() => { - switch (hoverReview?.provider) { + switch (hoverReviewProvider) { case 'github': void updateWorktreeMeta(worktree.id, { linkedPR: null }) return @@ -1136,12 +1137,12 @@ const WorktreeCard = React.memo(function WorktreeCard({ return case 'gitea': void updateWorktreeMeta(worktree.id, { linkedGiteaPR: null }) - return + break case 'unsupported': case undefined: break } - }, [hoverReview?.provider, updateWorktreeMeta, worktree.id]) + }, [hoverReviewProvider, updateWorktreeMeta, worktree.id]) const handleOpenLinearIssueInOrca = useCallback( (e: React.MouseEvent) => { e.stopPropagation()