From fbc306e6aafda21f4be76effa5cf6f22e3c505f7 Mon Sep 17 00:00:00 2001 From: Anish Lakhwara Date: Sun, 26 Jul 2026 18:30:43 -0700 Subject: [PATCH] chore(lint): migrate to oxlint --- .oxlintrc.json | 495 +++++++++++ bun.lock | 83 +- eslint.config.js | 222 ----- package-lock.json | 811 ++++++++++-------- package.json | 9 +- src/layout/file-tree/CreateItemModal.tsx | 106 +-- src/layout/file-tree/FileTree.tsx | 12 +- src/layout/file-tree/PromptDialog.tsx | 32 +- src/layout/file-tree/create-item-path.ts | 52 +- .../fuzzy-search/FuzzySearchOverlay.tsx | 148 ++-- src/layout/fuzzy-search/useSearchOverlay.ts | 3 +- .../builtin-suggesters/fallback-context.ts | 32 + .../builtin-suggesters/image-suggester.ts | 36 +- .../builtin-suggesters/tag-suggester.ts | 37 +- .../builtin-suggesters/wikilink-suggester.ts | 35 +- src/plugin/reams/index.ts | 23 + .../__tests__/plugin-layer-isolation.test.ts | 79 +- .../builtin-formatting-plugin.ts | 5 +- src/plugins/daily-notes/daily-notes-plugin.ts | 5 +- .../document-info/frontmatter-panel.tsx | 4 +- src/plugins/document-info/toc-panel.tsx | 9 +- .../document-info/toc-sticky-highlight.ts | 4 +- src/plugins/file-tree/file-tree-view.tsx | 4 +- .../highlight/highlight-editor-extension.ts | 105 +-- src/plugins/highlight/highlight-spec.ts | 2 +- .../layout-commands/layout-commands-plugin.ts | 7 +- src/plugins/quick-access/preview.ts | 41 +- src/plugins/wikilink/BacklinksPanel.tsx | 6 +- .../wikilink/backlinks-panel-inner.tsx | 9 +- src/plugins/wikilink/wikilink-plugin.ts | 317 +++---- ...ted-import-suppression-explanation.test.ts | 7 +- tests/hygiene/ratchets.ts | 2 +- 32 files changed, 1539 insertions(+), 1203 deletions(-) create mode 100644 .oxlintrc.json delete mode 100644 eslint.config.js create mode 100644 src/plugin/builtin-suggesters/fallback-context.ts diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 00000000..cd3a0516 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,495 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": [ + "typescript", + "react" + ], + "jsPlugins": [ + "eslint-plugin-sonarjs" + ], + "categories": { + "correctness": "off" + }, + "env": { + "builtin": true, + "es2022": true + }, + "settings": { + "react": { + "version": "999.999.999" + } + }, + "ignorePatterns": [ + "node_modules/**", + "dist/**", + ".worktrees/**", + "*.config.js", + "*.config.ts", + "vendor/**", + "src/plugins/__lint-test-fixtures__/**/*.ts", + "src/plugins/__lint-test-fixtures__/**/*.tsx" + ], + "rules": { + "constructor-super": "error", + "for-direction": "error", + "getter-return": "error", + "no-async-promise-executor": "error", + "no-case-declarations": "error", + "no-class-assign": "error", + "no-compare-neg-zero": "error", + "no-cond-assign": "error", + "no-const-assign": "error", + "no-constant-binary-expression": "error", + "no-constant-condition": "error", + "no-control-regex": "error", + "no-debugger": "error", + "no-delete-var": "error", + "no-dupe-class-members": "error", + "no-dupe-else-if": "error", + "no-dupe-keys": "error", + "no-duplicate-case": "error", + "no-empty": "error", + "no-empty-character-class": "error", + "no-empty-pattern": "error", + "no-empty-static-block": "error", + "no-ex-assign": "error", + "no-extra-boolean-cast": "error", + "no-fallthrough": "error", + "no-func-assign": "error", + "no-global-assign": "error", + "no-import-assign": "error", + "no-invalid-regexp": "error", + "no-irregular-whitespace": "error", + "no-loss-of-precision": "error", + "no-misleading-character-class": "error", + "no-new-native-nonconstructor": "error", + "no-nonoctal-decimal-escape": "error", + "no-obj-calls": "error", + "no-prototype-builtins": "error", + "no-redeclare": "error", + "no-regex-spaces": "error", + "no-self-assign": "error", + "no-setter-return": "error", + "no-shadow-restricted-names": "error", + "no-sparse-arrays": "error", + "no-this-before-super": "error", + "no-unassigned-vars": "error", + "no-unexpected-multiline": "error", + "no-unreachable": "error", + "no-unsafe-finally": "error", + "no-unsafe-negation": "error", + "no-unsafe-optional-chaining": "error", + "no-unused-labels": "error", + "no-unused-private-class-members": "error", + "no-useless-backreference": "error", + "no-useless-catch": "error", + "no-useless-escape": "error", + "no-with": "error", + "preserve-caught-error": "error", + "require-yield": "error", + "use-isnan": "error", + "valid-typeof": "error", + "no-array-constructor": "error", + "no-unused-expressions": "error", + "sonarjs/class-name": "error", + "sonarjs/no-fallthrough": "error", + "sonarjs/no-equals-in-for-termination": "error", + "sonarjs/no-extra-arguments": "error", + "sonarjs/no-labels": "error", + "sonarjs/no-nested-assignment": "error", + "sonarjs/no-redundant-boolean": "error", + "sonarjs/prefer-single-boolean-return": "error", + "sonarjs/unused-import": "error", + "sonarjs/fixme-tag": "error", + "sonarjs/todo-tag": "error", + "sonarjs/no-case-label-in-switch": "error", + "sonarjs/no-parameter-reassignment": "error", + "sonarjs/no-floating-point-equality": "error", + "sonarjs/prefer-while": "error", + "sonarjs/no-small-switch": "error", + "sonarjs/no-hardcoded-ip": "error", + "sonarjs/label-position": "error", + "sonarjs/public-static-readonly": "error", + "sonarjs/call-argument-line": "error", + "sonarjs/max-switch-cases": "error", + "sonarjs/no-unused-vars": "error", + "sonarjs/function-inside-loop": "error", + "sonarjs/code-eval": "error", + "sonarjs/future-reserved-words": "error", + "sonarjs/bitwise-operators": "error", + "sonarjs/no-primitive-wrappers": "error", + "sonarjs/no-skipped-tests": "error", + "sonarjs/no-identical-expressions": "error", + "sonarjs/constructor-for-side-effects": "error", + "sonarjs/no-dead-store": "error", + "sonarjs/no-identical-conditions": "error", + "sonarjs/no-duplicated-branches": "error", + "sonarjs/deprecation": "error", + "sonarjs/no-inverted-boolean-check": "error", + "sonarjs/misplaced-loop-counter": "error", + "sonarjs/no-nested-functions": "error", + "sonarjs/no-hardcoded-passwords": "error", + "sonarjs/sql-queries": "error", + "sonarjs/insecure-cookie": "error", + "sonarjs/no-useless-increment": "error", + "sonarjs/no-globals-shadowing": "error", + "sonarjs/no-empty-test-file": "error", + "sonarjs/no-ignored-return": "error", + "sonarjs/arguments-order": "error", + "sonarjs/pseudo-random": "error", + "sonarjs/for-loop-increment-sign": "error", + "sonarjs/null-dereference": "error", + "sonarjs/no-selector-parameter": "error", + "sonarjs/updated-loop-counter": "error", + "sonarjs/block-scoped-var": "error", + "sonarjs/no-ignored-exceptions": "error", + "sonarjs/no-gratuitous-expressions": "error", + "sonarjs/file-uploads": "error", + "sonarjs/file-permissions": "error", + "sonarjs/no-empty-character-class": "error", + "sonarjs/no-unenclosed-multiline-block": "error", + "sonarjs/index-of-compare-to-positive-number": "error", + "sonarjs/assertions-in-tests": "error", + "sonarjs/no-implicit-global": "error", + "sonarjs/no-useless-catch": "error", + "sonarjs/xml-parser-xxe": "error", + "sonarjs/non-existent-operator": "error", + "sonarjs/post-message": "error", + "sonarjs/no-array-delete": "error", + "sonarjs/no-alphabetical-sort": "error", + "sonarjs/no-fixed-wait-in-tests": "error", + "sonarjs/no-incomplete-assertions": "error", + "sonarjs/no-global-this": "error", + "sonarjs/new-operator-misuse": "error", + "sonarjs/no-delete-var": "error", + "sonarjs/cookie-no-httponly": "error", + "sonarjs/no-nested-conditional": "error", + "sonarjs/different-types-comparison": "error", + "sonarjs/inverted-assertion-arguments": "error", + "sonarjs/updated-const-var": "error", + "sonarjs/no-invariant-returns": "error", + "sonarjs/generator-without-yield": "error", + "sonarjs/no-associative-arrays": "error", + "sonarjs/comma-or-logical-or-case": "error", + "sonarjs/no-redundant-jump": "error", + "sonarjs/inconsistent-function-call": "error", + "sonarjs/no-use-of-empty-return-value": "error", + "sonarjs/void-use": "error", + "sonarjs/cognitive-complexity": "error", + "sonarjs/argument-type": "error", + "sonarjs/in-operator-type-error": "error", + "sonarjs/array-callback-without-return": "error", + "sonarjs/function-return-type": "error", + "sonarjs/no-all-duplicated-branches": "error", + "sonarjs/no-same-line-conditional": "error", + "sonarjs/no-collection-size-mischeck": "error", + "sonarjs/no-unthrown-error": "error", + "sonarjs/no-unused-collection": "error", + "sonarjs/no-os-command-from-path": "error", + "sonarjs/no-misleading-array-reverse": "error", + "sonarjs/no-element-overwrite": "error", + "sonarjs/no-identical-functions": "error", + "sonarjs/no-empty-collection": "error", + "sonarjs/no-redundant-assignments": "error", + "sonarjs/prefer-type-guard": "error", + "sonarjs/use-type-alias": "error", + "sonarjs/no-useless-intersection": "error", + "sonarjs/weak-ssl": "error", + "sonarjs/no-weak-keys": "error", + "sonarjs/csrf": "error", + "sonarjs/production-debug": "error", + "sonarjs/prefer-default-last": "error", + "sonarjs/no-in-misuse": "error", + "sonarjs/no-duplicate-in-composite": "error", + "sonarjs/no-undefined-argument": "error", + "sonarjs/no-nested-template-literals": "error", + "sonarjs/prefer-promise-shorthand": "error", + "sonarjs/no-redundant-optional": "error", + "sonarjs/hashing": "error", + "sonarjs/no-try-promise": "error", + "sonarjs/unverified-certificate": "error", + "sonarjs/cors": "error", + "sonarjs/link-with-target-blank": "error", + "sonarjs/disabled-auto-escaping": "error", + "sonarjs/table-header": "error", + "sonarjs/no-table-as-layout": "error", + "sonarjs/table-header-reference": "error", + "sonarjs/object-alt-content": "error", + "sonarjs/no-clear-text-protocols": "error", + "sonarjs/publicly-writable-directories": "error", + "sonarjs/unverified-hostname": "error", + "sonarjs/encryption-secure-mode": "error", + "sonarjs/no-weak-cipher": "error", + "sonarjs/insecure-jwt-token": "error", + "sonarjs/x-powered-by": "error", + "sonarjs/content-length": "error", + "sonarjs/disabled-resource-integrity": "error", + "sonarjs/content-security-policy": "error", + "sonarjs/no-mime-sniff": "error", + "sonarjs/no-referrer-policy": "error", + "sonarjs/strict-transport-security": "error", + "sonarjs/empty-string-repetition": "error", + "sonarjs/regex-complexity": "error", + "sonarjs/no-incompatible-assertion-types": "error", + "sonarjs/anchor-precedence": "error", + "sonarjs/slow-regex": "error", + "sonarjs/no-invalid-regexp": "error", + "sonarjs/unused-named-groups": "error", + "sonarjs/no-same-argument-assert": "error", + "sonarjs/no-misleading-character-class": "error", + "sonarjs/duplicates-in-character-class": "error", + "sonarjs/session-regeneration": "error", + "sonarjs/prefer-specific-assertions": "error", + "sonarjs/no-trivial-assertions": "error", + "sonarjs/test-check-exception": "error", + "sonarjs/stable-tests": "error", + "sonarjs/parameterized-tests": "error", + "sonarjs/no-empty-after-reluctant": "error", + "sonarjs/single-character-alternation": "error", + "sonarjs/no-code-after-done": "error", + "sonarjs/disabled-timeout": "error", + "sonarjs/chai-determinate-assertion": "error", + "sonarjs/aws-s3-bucket-insecure-http": "error", + "sonarjs/aws-s3-bucket-versioning": "error", + "sonarjs/aws-s3-bucket-granted-access": "error", + "sonarjs/no-angular-bypass-sanitization": "error", + "sonarjs/aws-iam-public-access": "error", + "sonarjs/aws-ec2-unencrypted-ebs-volume": "error", + "sonarjs/aws-s3-bucket-public-access": "error", + "sonarjs/aws-iam-all-privileges": "error", + "sonarjs/aws-rds-unencrypted-databases": "error", + "sonarjs/aws-opensearchservice-domain": "error", + "sonarjs/aws-iam-privilege-escalation": "error", + "sonarjs/aws-sagemaker-unencrypted-notebook": "error", + "sonarjs/aws-restricted-ip-admin-access": "error", + "sonarjs/no-empty-alternatives": "error", + "sonarjs/no-control-regex": "error", + "sonarjs/no-regex-spaces": "error", + "sonarjs/aws-sns-unencrypted-topics": "error", + "sonarjs/existing-groups": "error", + "sonarjs/aws-ec2-rds-dms-public": "error", + "sonarjs/aws-sqs-unencrypted-queue": "error", + "sonarjs/no-empty-group": "error", + "sonarjs/aws-efs-unencrypted": "error", + "sonarjs/aws-apigateway-public-api": "error", + "sonarjs/stateful-regex": "error", + "sonarjs/concise-regex": "error", + "sonarjs/single-char-in-character-classes": "error", + "sonarjs/no-hardcoded-secrets": "error", + "sonarjs/no-exclusive-tests": "error", + "sonarjs/hardcoded-secret-signatures": "error", + "sonarjs/jsx-no-leaked-render": "error", + "sonarjs/no-hook-setter-in-body": "error", + "sonarjs/no-useless-react-setstate": "error", + "sonarjs/no-uniq-key": "error", + "sonarjs/redundant-type-aliases": "error", + "sonarjs/prefer-regexp-exec": "error", + "sonarjs/no-internal-api-use": "error", + "sonarjs/prefer-read-only-props": "error", + "sonarjs/no-literal-call": "error", + "sonarjs/reduce-initial-value": "error", + "sonarjs/no-async-constructor": "error", + "sonarjs/review-blockchain-mnemonic": "error", + "sonarjs/dynamically-constructed-templates": "error", + "sonarjs/no-session-cookies-on-static-assets": "error", + "sonarjs/dompurify-unsafe-config": "error", + "sonarjs/no-duplicate-test-title": "error", + "sonarjs/async-test-assertions": "error", + "sonarjs/no-empty-test-title": "error", + "sonarjs/hooks-before-test-cases": "error", + "sonarjs/no-forced-browser-interaction": "error", + "sonarjs/assertions-in-test-cases": "error", + "sonarjs/synchronous-suite-callback": "error", + "sonarjs/super-linear-regex": "error", + "sonarjs/prefer-native-lodash-alternative": "error", + "sonarjs/no-default-utility-imports": "error", + "sonarjs/memoize-cache-key": "error", + "sonarjs/no-debug-commands-in-ui-tests": "error", + "sonarjs/no-mixed-completion-style": "error", + "sonarjs/no-interpolation-in-inline-snapshots": "error", + "sonarjs/explicit-test-skip": "error", + "react/rules-of-hooks": "error", + "react/exhaustive-deps": "warn", + "typescript/ban-ts-comment": "error", + "typescript/no-duplicate-enum-values": "error", + "typescript/no-extra-non-null-assertion": "error", + "typescript/no-misused-new": "error", + "typescript/no-namespace": "error", + "typescript/no-non-null-asserted-optional-chain": "error", + "typescript/no-unnecessary-type-constraint": "error", + "typescript/no-unsafe-declaration-merging": "error", + "typescript/no-unsafe-function-type": "error", + "typescript/no-wrapper-object-types": "error", + "typescript/prefer-as-const": "error", + "typescript/prefer-namespace-keyword": "error", + "typescript/triple-slash-reference": "error" + }, + "overrides": [ + { + "files": [ + "**/*.ts", + "**/*.tsx", + "**/*.mts", + "**/*.cts" + ], + "rules": { + "constructor-super": "off", + "getter-return": "off", + "no-class-assign": "off", + "no-const-assign": "off", + "no-dupe-class-members": "off", + "no-dupe-keys": "off", + "no-func-assign": "off", + "no-import-assign": "off", + "no-new-native-nonconstructor": "off", + "no-obj-calls": "off", + "no-redeclare": "off", + "no-setter-return": "off", + "no-this-before-super": "off", + "no-unreachable": "off", + "no-unsafe-negation": "off", + "no-var": "error", + "no-with": "off", + "prefer-const": "error", + "prefer-rest-params": "error", + "prefer-spread": "error" + } + }, + { + "files": [ + "**/__tests__/**/*.{ts,tsx}", + "tests/**/*.{ts,tsx}" + ], + "rules": { + "sonarjs/assertions-in-tests": "off", + "sonarjs/no-identical-functions": "off", + "sonarjs/no-unused-collection": "off", + "sonarjs/parameterized-tests": "off", + "sonarjs/prefer-specific-assertions": "off", + "sonarjs/pseudo-random": "off", + "sonarjs/use-type-alias": "off" + }, + "jsPlugins": [ + "eslint-plugin-sonarjs" + ] + }, + { + "files": [ + "src/plugins/**/*.ts", + "src/plugins/**/*.tsx" + ], + "rules": { + "no-restricted-imports": [ + "error", + { + "patterns": [ + { + "group": [ + "**/services/**", + "**/services" + ], + "message": "Built-in plugins must consume services through the public Obsidian API (`from 'obsidian'`) or the Reams namespace (`from 'reams'`, `app.reams*`). Direct imports from `src/services/` are forbidden. If the API you need doesn't exist there, file a chainlink issue under #389 to add it to the namespace." + }, + { + "group": [ + "**/editor/**", + "**/editor" + ], + "message": "Built-in plugins must consume editor utilities through the public Obsidian API (`from 'obsidian'`) or the Reams namespace (`from 'reams'`, `app.reams*`). Direct imports from `src/editor/` are forbidden. If the API you need doesn't exist there, file a chainlink issue under #389 to add it to the namespace." + }, + { + "group": [ + "**/layout/**", + "**/layout" + ], + "message": "Built-in plugins must not import from `src/layout/`. Layout components are host-internal. Use the public Obsidian API (`from 'obsidian'`) or the Reams namespace (`from 'reams'`). If you need a layout primitive exposed, file a chainlink issue under #389." + } + ] + } + ] + } + }, + { + "files": [ + "src/plugin/builtin-suggesters/**/*.ts", + "src/plugin/builtin-suggesters/**/*.tsx" + ], + "rules": { + "no-restricted-imports": [ + "error", + { + "patterns": [ + { + "group": [ + "../../services/**", + "../../services" + ], + "message": "Built-in plugins must consume services through the public Obsidian API (`from 'obsidian'`) or the Reams namespace (`from 'reams'`, `app.reams*`). Direct imports from `src/services/` are forbidden. If the API you need doesn't exist there, file a chainlink issue under #389 to add it to the namespace." + }, + { + "group": [ + "../../editor/**", + "../../editor" + ], + "message": "Built-in plugins must consume editor utilities through the public Obsidian API (`from 'obsidian'`) or the Reams namespace (`from 'reams'`, `app.reams*`). Direct imports from `src/editor/` are forbidden. If the API you need doesn't exist there, file a chainlink issue under #389 to add it to the namespace." + }, + { + "group": [ + "../../layout/**", + "../../layout" + ], + "message": "Built-in plugins must not import from `src/layout/`. Layout components are host-internal. Use the public Obsidian API (`from 'obsidian'`) or the Reams namespace (`from 'reams'`). If you need a layout primitive exposed, file a chainlink issue under #389." + } + ] + } + ] + } + }, + { + "files": [ + "src/layout/file-tree/**/*.ts", + "src/layout/file-tree/**/*.tsx", + "src/layout/fuzzy-search/**/*.ts", + "src/layout/fuzzy-search/**/*.tsx" + ], + "rules": { + "no-restricted-imports": [ + "error", + { + "patterns": [ + { + "group": [ + "../../services/**", + "../../services" + ], + "message": "Built-in layout-backed flows must consume services through the public Obsidian API (`from 'obsidian'`) or the Reams namespace (`from 'reams'`, `app.reams*`). Direct imports from `src/services/` are forbidden unless explicitly flagged." + }, + { + "group": [ + "../../editor/**", + "../../editor" + ], + "message": "Built-in layout-backed flows must consume editor utilities through the public Obsidian API (`from 'obsidian'`) or the Reams namespace (`from 'reams'`, `app.reams*`). Direct imports from `src/editor/` are forbidden unless explicitly flagged." + } + ] + } + ] + } + }, + { + "files": [ + "src/plugins/**/__tests__/**/*.ts", + "src/plugins/**/__tests__/**/*.tsx", + "src/plugin/builtin-suggesters/**/__tests__/**/*.ts", + "src/plugin/builtin-suggesters/**/__tests__/**/*.tsx", + "src/layout/file-tree/**/__tests__/**/*.ts", + "src/layout/file-tree/**/__tests__/**/*.tsx", + "src/layout/fuzzy-search/**/__tests__/**/*.ts", + "src/layout/fuzzy-search/**/__tests__/**/*.tsx" + ], + "rules": { + "no-restricted-imports": "off" + } + } + ] +} diff --git a/bun.lock b/bun.lock index 9f7545d5..9fb31efe 100644 --- a/bun.lock +++ b/bun.lock @@ -49,7 +49,6 @@ }, "devDependencies": { "@antithesishq/bombadil": "file:vendor/bombadil-custom-actions-npm", - "@eslint/js": "^10.0.1", "@stryker-mutator/core": "^9.6.0", "@stryker-mutator/typescript-checker": "^9.6.0", "@stryker-mutator/vitest-runner": "^9.6.0", @@ -65,16 +64,14 @@ "@vitejs/plugin-react": "^4.3.4", "@vitest/coverage-v8": "4.1.0", "dependency-cruiser": "^18.1.0", - "eslint": "^10.3.0", - "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-sonarjs": "^4.2.0", "fast-check": "^4.6.0", "jsdom": "^28.1.0", "knip": "^6.11.0", + "oxlint": "^1.75.0", "playwright": "^1.55.0", "ts-morph": "^28.0.0", "typescript": "~5.6.2", - "typescript-eslint": "^8.59.2", "vite": "^6.0.1", "vite-plugin-top-level-await": "^1.6.0", "vite-plugin-wasm": "^3.5.0", @@ -331,8 +328,6 @@ "@eslint/core": ["@eslint/core@1.2.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ=="], - "@eslint/js": ["@eslint/js@10.0.1", "", { "peerDependencies": { "eslint": "^10.0.0" }, "optionalPeers": ["eslint"] }, "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA=="], - "@eslint/object-schema": ["@eslint/object-schema@3.0.5", "", {}, "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw=="], "@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.1", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ=="], @@ -569,6 +564,44 @@ "@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@11.19.1", "", { "os": "win32", "cpu": "x64" }, "sha512-6hIU3RQu45B+VNTY4Ru8ppFwjVS/S5qwYyGhBotmjxfEKk41I2DlGtRfGJndZ5+6lneE2pwloqunlOyZuX/XAw=="], + "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.75.0", "", { "os": "android", "cpu": "arm" }, "sha512-lutovtFzJqlRaqpZrCqSSGaHZzl9nIxxpjLzhSRLunN6dCLylj0uzlCyQGaQDIys7rrv8kVXiFO+R4Zpn0bX7g=="], + + "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.75.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hXI0hDgHkw4w5nfru72aG7y+2iQJmC4waH/KV6H/hbgA6yAP5jYNx0P9yug15Hs0tWl/+mda3Jjn/2gmDT48tw=="], + + "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.75.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-D91BWbK/dMYfCcrghspPIuKs2D9LF4Z/OabVSQjw1AO6PWxArD7teDA48bm0ySFqWDaPVqmQRl5GMWNglTXyrQ=="], + + "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.75.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-02mpwzf12BonZ6PT0TuQoomvEh2kVl2WGBIKWezCyToIS+rYkQZ6GXnARBAl9A4Ovm2V+Xe7M4KretyqmmcnJQ=="], + + "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.75.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-qZJgLnDaBsiL5YESx2t/TZ8eXkL9fEkKoXEdzegROhlz9A0lgyGnZ0dAzJrh7LJAHQl2K9RdRueN2s/9N7+odg=="], + + "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.75.0", "", { "os": "linux", "cpu": "arm" }, "sha512-7XlaWA5BJD3XpCfrEqjEe6Zseeb14S7QGa304XfwKignRaKQ+eIj775BQ7nIslggWickl4IsPUFqJ+/gAyNHVg=="], + + "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.75.0", "", { "os": "linux", "cpu": "arm" }, "sha512-av6Tpv8yrcMMMOadOqENBhlsLRcGFXXwoQ0hzHhsmS9FJ4Wioy8we427GbcMe2XTxmL2e60T67H1Dyr3up+tAA=="], + + "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.75.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-WcUhd8fHT5plrA14lANevl+hOl815mVI5t2hU21oFWrZKFXIVV/Sr4rWQV0NzSvzBupbMLNc5ErEA6Ehxh5jMg=="], + + "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.75.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UWzp5wRHFe/ESO3+eEaxXsTkYTGLYjnTsi/I5neEacXSItQ6WNleapfOAeA4x2b8nyhJ4uQxqvtv9pHv8kWJtQ=="], + + "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.75.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-XEVRwGMLKCUKrvhLAz4F6AIh8MJrQVdSZtAmPpRZt9tGPsUnamPOcl3dS/ZQzJnar/Ymgc//+xho0L60Emzuxg=="], + + "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.75.0", "", { "os": "linux", "cpu": "none" }, "sha512-mAG4DUXqfLC8cTjMD2kt3jDmVzFREYtDyeLNdLdsCcBc4Zbl2EMuiFektGBilQwkNjYnMvCqJs55U+Hyb+b+jw=="], + + "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.75.0", "", { "os": "linux", "cpu": "none" }, "sha512-95hrAvriAlI+pekSomTFIn0+bawMDlDwTNVmdjsFusTHyL2JWh7TWvRNG/Lkim72uN8OiCcO9wcaC6omLP5E3w=="], + + "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.75.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-4b6f2+FrtruAESrCqIKcrarzfrSx+wk2QNcp+RT91/Prc+pMQMAfyZ1rG1c3tFQNl8Bc616tx40uNXyxNBRPbQ=="], + + "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.75.0", "", { "os": "linux", "cpu": "x64" }, "sha512-nshAhrUvXFUWOvqQ2soIw7HFNWvpvEV4o0cYSqPtzLiPF5gKyYTDOOTJ6Rn8g8K/iGvPIrbDA4v8+5MvnjJrrg=="], + + "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.75.0", "", { "os": "linux", "cpu": "x64" }, "sha512-e4jNxLKnxLC6sYBQRxrI2pgIIxnmMtF8U/VwNYcjTT/CLS+spH624cYVnj07bTKwaEWT37/e025isOs6j/0xqA=="], + + "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.75.0", "", { "os": "none", "cpu": "arm64" }, "sha512-hZ2lH+1qLf/DiEP9UWuQTK2JWj/BgvMB4jhIV4SmNU1wfEiYYX4TynQyAZXx0j9X4qRYizAL042SKaV+8ynh4w=="], + + "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.75.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Ilj6PNzGDS3bCU0MSJH7Msh0NhH+T/mRp2shwg+q+GHeVlPwP5LEboW96aW+3kVKFk6zYZy1Xi5pZkqZh6X8KQ=="], + + "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.75.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-QVit2nOEOiPhkmsrksPSkoGCdnZRNkspt8fwoYyP09te1VEbnSj4LAxua4rc8FKTmWkySVe05j8iz9GXYfF1AQ=="], + + "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.75.0", "", { "os": "win32", "cpu": "x64" }, "sha512-DSxnNkBUAYARPwJtR12Ig3deWr8w0H997xP6jy33i+e0SyYJw8FKuz4+cZtpmPEhQmvlPJE3X/2vNxDmLkd/rA=="], + "@preact/signals-core": ["@preact/signals-core@1.14.0", "", {}, "sha512-AowtCcCU/33lFlh1zRFf/u+12rfrhtNakj7UpaGEsmMwUKpKWMVvcktOGcwBBNiB4lWrZWc01LhiyyzVklJyaQ=="], "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], @@ -717,26 +750,6 @@ "@types/turndown": ["@types/turndown@5.0.6", "", {}, "sha512-ru00MoyeeouE5BX4gRL+6m/BsDfbRayOskWqUvh7CLGW+UXxHQItqALa38kKnOiZPqJrtzJUgAC2+F0rL1S4Pg=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.59.3", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.59.3", "@typescript-eslint/type-utils": "8.59.3", "@typescript-eslint/utils": "8.59.3", "@typescript-eslint/visitor-keys": "8.59.3", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.59.3", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw=="], - - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.59.3", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.59.3", "@typescript-eslint/types": "8.59.3", "@typescript-eslint/typescript-estree": "8.59.3", "@typescript-eslint/visitor-keys": "8.59.3", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg=="], - - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.59.3", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.59.3", "@typescript-eslint/types": "^8.59.3", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng=="], - - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.3", "", { "dependencies": { "@typescript-eslint/types": "8.59.3", "@typescript-eslint/visitor-keys": "8.59.3" } }, "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA=="], - - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.59.3", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw=="], - - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.59.3", "", { "dependencies": { "@typescript-eslint/types": "8.59.3", "@typescript-eslint/typescript-estree": "8.59.3", "@typescript-eslint/utils": "8.59.3", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ=="], - - "@typescript-eslint/types": ["@typescript-eslint/types@8.59.3", "", {}, "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg=="], - - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.59.3", "", { "dependencies": { "@typescript-eslint/project-service": "8.59.3", "@typescript-eslint/tsconfig-utils": "8.59.3", "@typescript-eslint/types": "8.59.3", "@typescript-eslint/visitor-keys": "8.59.3", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg=="], - - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.59.3", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.59.3", "@typescript-eslint/types": "8.59.3", "@typescript-eslint/typescript-estree": "8.59.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg=="], - - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.3", "", { "dependencies": { "@typescript-eslint/types": "8.59.3", "eslint-visitor-keys": "^5.0.0" } }, "sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg=="], - "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], "@vitest/coverage-v8": ["@vitest/coverage-v8@4.1.0", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.1.0", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", "magicast": "^0.5.2", "obug": "^2.1.1", "std-env": "^4.0.0-rc.1", "tinyrainbow": "^3.0.3" }, "peerDependencies": { "@vitest/browser": "4.1.0", "vitest": "4.1.0" }, "optionalPeers": ["@vitest/browser"] }, "sha512-nDWulKeik2bL2Va/Wl4x7DLuTKAXa906iRFooIRPR+huHkcvp9QDkPQ2RJdmjOFrqOqvNfoSQLF68deE3xC3CQ=="], @@ -889,8 +902,6 @@ "eslint": ["eslint@10.3.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.5.5", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw=="], - "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.1.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "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" } }, "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g=="], - "eslint-plugin-sonarjs": ["eslint-plugin-sonarjs@4.2.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "builtin-modules": "^3.3.0", "bytes": "^3.1.2", "functional-red-black-tree": "^1.0.1", "globals": "^17.7.0", "jsx-ast-utils-x": "^0.1.0", "lodash.merge": "^4.6.2", "minimatch": "^10.2.5", "scslre": "^0.3.0", "semver": "^7.8.5", "ts-api-utils": "^2.5.0", "typescript": ">=5 <6.1.0", "yaml": "^2.9.0" }, "peerDependencies": { "eslint": "^8.0.0 || ^9.0.0 || ^10.0.0" } }, "sha512-bqADfuNtTL7VK6RU29eoiFTtaaBKIpVPuX3bOl+rBpWSBa0zIBVZlqZNZQjfP6s4iXkAJokv5IsD8OsACkwApg=="], "eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="], @@ -981,10 +992,6 @@ "hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], - "hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="], - - "hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="], - "html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="], "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], @@ -1149,6 +1156,8 @@ "oxc-resolver": ["oxc-resolver@11.19.1", "", { "optionalDependencies": { "@oxc-resolver/binding-android-arm-eabi": "11.19.1", "@oxc-resolver/binding-android-arm64": "11.19.1", "@oxc-resolver/binding-darwin-arm64": "11.19.1", "@oxc-resolver/binding-darwin-x64": "11.19.1", "@oxc-resolver/binding-freebsd-x64": "11.19.1", "@oxc-resolver/binding-linux-arm-gnueabihf": "11.19.1", "@oxc-resolver/binding-linux-arm-musleabihf": "11.19.1", "@oxc-resolver/binding-linux-arm64-gnu": "11.19.1", "@oxc-resolver/binding-linux-arm64-musl": "11.19.1", "@oxc-resolver/binding-linux-ppc64-gnu": "11.19.1", "@oxc-resolver/binding-linux-riscv64-gnu": "11.19.1", "@oxc-resolver/binding-linux-riscv64-musl": "11.19.1", "@oxc-resolver/binding-linux-s390x-gnu": "11.19.1", "@oxc-resolver/binding-linux-x64-gnu": "11.19.1", "@oxc-resolver/binding-linux-x64-musl": "11.19.1", "@oxc-resolver/binding-openharmony-arm64": "11.19.1", "@oxc-resolver/binding-wasm32-wasi": "11.19.1", "@oxc-resolver/binding-win32-arm64-msvc": "11.19.1", "@oxc-resolver/binding-win32-ia32-msvc": "11.19.1", "@oxc-resolver/binding-win32-x64-msvc": "11.19.1" } }, "sha512-qE/CIg/spwrTBFt5aKmwe3ifeDdLfA2NESN30E42X/lII5ClF8V7Wt6WIJhcGZjp0/Q+nQ+9vgxGk//xZNX2hg=="], + "oxlint": ["oxlint@1.75.0", "", { "optionalDependencies": { "@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" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2001", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-m9WzjRcRYA/uqIZDa9tclrieoPJ/ln1QYTKdFx6NUOs8uY5DiHlIwRQoCrHT6OM6O3ww3l2skY5gO7G7ZphE7g=="], + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], @@ -1329,8 +1338,6 @@ "typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="], - "typescript-eslint": ["typescript-eslint@8.59.3", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.59.3", "@typescript-eslint/parser": "8.59.3", "@typescript-eslint/typescript-estree": "8.59.3", "@typescript-eslint/utils": "8.59.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-KgusgyDgG4LI8Ih/sWaCtZ06tckLAS5CvT5A4D1Q7bYVoAAyzwiZvE4BmwDHkhRVkvhRBepKeASoFzQetha7Fg=="], - "unbash": ["unbash@3.0.0", "", {}, "sha512-FeFPZ/WFT0mbRCuydiZzpPFlrYN8ZUpphQKoq4EeElVIYjYyGzPMxQR/simUwCOJIyVhpFk4RbtyO7RuMpMnHA=="], "underscore": ["underscore@1.13.8", "", {}, "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ=="], @@ -1397,8 +1404,6 @@ "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], - "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], - "@automerge/automerge-repo/uuid": ["uuid@9.0.1", "", { "bin": "dist/bin/uuid" }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], "@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], @@ -1415,10 +1420,6 @@ "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], - "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], - - "@typescript-eslint/typescript-estree/tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], - "cross-spawn/path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], "dependency-cruiser/commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], @@ -1457,8 +1458,6 @@ "vitest/tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], - "@typescript-eslint/typescript-estree/tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - "eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "magicast/@babel/parser/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], diff --git a/eslint.config.js b/eslint.config.js deleted file mode 100644 index 73cb4f50..00000000 --- a/eslint.config.js +++ /dev/null @@ -1,222 +0,0 @@ -import eslint from '@eslint/js' -import tseslint from 'typescript-eslint' -import reactHooks from 'eslint-plugin-react-hooks' -import sonarjs from 'eslint-plugin-sonarjs' - -/** - * Reams ESLint Configuration - * - * This configuration focuses on a critical namespace enforcement rule: - * built-in/plugin-like code in src/plugins/ and src/plugin/builtin-suggesters/ - * must NOT import from src/services/, src/editor/, or src/layout/. They must - * use the public Obsidian API (from 'obsidian') or the Reams namespace - * (from 'reams', app.reams*). - * - * @see chainlink issue #392 - * @see docs/audits/core-services-audit.md — reach-behind findings - */ -export default tseslint.config( - // Ignore non-source files and lint test fixtures - // NOTE: The lint test fixtures in src/plugins/__lint-test-fixtures__/ are - // intentionally violating files. They're globally ignored here so that - // `npm run lint` passes, but the test runs eslint directly on them - // (bypassing ignores) to verify the production rule catches violations. - { - ignores: [ - 'node_modules/**', - 'dist/**', - '.worktrees/**', - '*.config.js', - '*.config.ts', - 'vendor/**', - 'src/plugins/__lint-test-fixtures__/**/*.ts', - 'src/plugins/__lint-test-fixtures__/**/*.tsx', - ], - }, - - // Base JS recommended rules (very light touch) - eslint.configs.recommended, - - // TypeScript base config - ...tseslint.configs.recommended, - - // SonarJS catches maintainability and correctness smells that the base - // TypeScript rules do not, including cognitive complexity and duplicated - // conditional branches. - sonarjs.configs.recommended, - - // Test ergonomics are deliberately handled by Vitest and Testing Library. - // Keep production maintainability rules strict without forcing mechanical - // test-table rewrites or treating deterministic test fixtures as crypto. - { - files: ['**/__tests__/**/*.{ts,tsx}', 'tests/**/*.{ts,tsx}'], - rules: { - 'sonarjs/assertions-in-tests': 'off', - 'sonarjs/no-identical-functions': 'off', - 'sonarjs/no-unused-collection': 'off', - 'sonarjs/parameterized-tests': 'off', - 'sonarjs/prefer-specific-assertions': 'off', - 'sonarjs/pseudo-random': 'off', - 'sonarjs/use-type-alias': 'off', - }, - }, - - // Global settings for all files - { - languageOptions: { - ecmaVersion: 2022, - sourceType: 'module', - }, - plugins: { - 'react-hooks': reactHooks, - }, - rules: { - // Disable rules that conflict with TypeScript or are too noisy - '@typescript-eslint/no-unused-vars': 'off', // TS already checks this - '@typescript-eslint/no-explicit-any': 'off', // Allow any for now - '@typescript-eslint/no-empty-object-type': 'off', // Allowed pattern - '@typescript-eslint/no-require-imports': 'off', // CJS compat in some files - '@typescript-eslint/no-this-alias': 'off', // Used for closure capture patterns - 'no-undef': 'off', // TypeScript handles this - // React hooks rules (enabled so eslint-disable comments work) - 'react-hooks/rules-of-hooks': 'error', - 'react-hooks/exhaustive-deps': 'warn', - }, - }, - - // **CRITICAL**: Plugin layer-isolation rule - // Built-in/plugin-like code must not import from src/services/, src/editor/, - // or src/layout/. - // - // NOTE: This rule does NOT catch dynamic imports (`import('...')`). Currently no - // production built-in/plugin-like code uses dynamic imports. If this changes, - // add a no-restricted-syntax rule with ImportExpression selector. - // - // The `**/foo/**` glob pattern matches any import path containing `/foo/`, regardless of - // how many `../` prefixes it has. We also match `**/foo` (no trailing slash) for bare - // barrel imports like `from '../../services'`. - { - files: ['src/plugins/**/*.ts', 'src/plugins/**/*.tsx'], - rules: { - 'no-restricted-imports': [ - 'error', - { - patterns: [ - { - group: ['**/services/**', '**/services'], - message: - 'Built-in plugins must consume services through the public Obsidian API ' + - '(`from \'obsidian\'`) or the Reams namespace (`from \'reams\'`, `app.reams*`). ' + - 'Direct imports from `src/services/` are forbidden. ' + - 'If the API you need doesn\'t exist there, file a chainlink issue under #389 to add it to the namespace.', - }, - { - group: ['**/editor/**', '**/editor'], - message: - 'Built-in plugins must consume editor utilities through the public Obsidian API ' + - '(`from \'obsidian\'`) or the Reams namespace (`from \'reams\'`, `app.reams*`). ' + - 'Direct imports from `src/editor/` are forbidden. ' + - 'If the API you need doesn\'t exist there, file a chainlink issue under #389 to add it to the namespace.', - }, - { - group: ['**/layout/**', '**/layout'], - message: - 'Built-in plugins must not import from `src/layout/`. Layout components are host-internal. ' + - 'Use the public Obsidian API (`from \'obsidian\'`) or the Reams namespace (`from \'reams\'`). ' + - 'If you need a layout primitive exposed, file a chainlink issue under #389.', - }, - ], - }, - ], - }, - }, - { - files: [ - 'src/plugin/builtin-suggesters/**/*.ts', - 'src/plugin/builtin-suggesters/**/*.tsx', - ], - rules: { - 'no-restricted-imports': [ - 'error', - { - patterns: [ - { - group: ['../../services/**', '../../services'], - message: - 'Built-in plugins must consume services through the public Obsidian API ' + - '(`from \'obsidian\'`) or the Reams namespace (`from \'reams\'`, `app.reams*`). ' + - 'Direct imports from `src/services/` are forbidden. ' + - 'If the API you need doesn\'t exist there, file a chainlink issue under #389 to add it to the namespace.', - }, - { - group: ['../../editor/**', '../../editor'], - message: - 'Built-in plugins must consume editor utilities through the public Obsidian API ' + - '(`from \'obsidian\'`) or the Reams namespace (`from \'reams\'`, `app.reams*`). ' + - 'Direct imports from `src/editor/` are forbidden. ' + - 'If the API you need doesn\'t exist there, file a chainlink issue under #389 to add it to the namespace.', - }, - { - group: ['../../layout/**', '../../layout'], - message: - 'Built-in plugins must not import from `src/layout/`. Layout components are host-internal. ' + - 'Use the public Obsidian API (`from \'obsidian\'`) or the Reams namespace (`from \'reams\'`). ' + - 'If you need a layout primitive exposed, file a chainlink issue under #389.', - }, - ], - }, - ], - }, - }, - { - files: [ - 'src/layout/file-tree/**/*.ts', - 'src/layout/file-tree/**/*.tsx', - 'src/layout/fuzzy-search/**/*.ts', - 'src/layout/fuzzy-search/**/*.tsx', - ], - rules: { - 'no-restricted-imports': [ - 'error', - { - patterns: [ - { - group: ['../../services/**', '../../services'], - message: - 'Built-in layout-backed flows must consume services through the public Obsidian API ' + - '(`from \'obsidian\'`) or the Reams namespace (`from \'reams\'`, `app.reams*`). ' + - 'Direct imports from `src/services/` are forbidden unless explicitly flagged.', - }, - { - group: ['../../editor/**', '../../editor'], - message: - 'Built-in layout-backed flows must consume editor utilities through the public Obsidian API ' + - '(`from \'obsidian\'`) or the Reams namespace (`from \'reams\'`, `app.reams*`). ' + - 'Direct imports from `src/editor/` are forbidden unless explicitly flagged.', - }, - ], - }, - ], - }, - }, - - // Tests are allowed to import from anywhere for test setup - // (they need to construct internal types like FileIndex, MetadataCache, etc.) - { - files: [ - 'src/plugins/**/__tests__/**/*.ts', - 'src/plugins/**/__tests__/**/*.tsx', - 'src/plugin/builtin-suggesters/**/__tests__/**/*.ts', - 'src/plugin/builtin-suggesters/**/__tests__/**/*.tsx', - 'src/layout/file-tree/**/__tests__/**/*.ts', - 'src/layout/file-tree/**/__tests__/**/*.tsx', - 'src/layout/fuzzy-search/**/__tests__/**/*.ts', - 'src/layout/fuzzy-search/**/__tests__/**/*.tsx', - ], - rules: { - 'no-restricted-imports': 'off', - }, - }, - - -) diff --git a/package-lock.json b/package-lock.json index 3448b5e0..f2ca7986 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,7 +52,6 @@ }, "devDependencies": { "@antithesishq/bombadil": "file:vendor/bombadil-custom-actions-npm", - "@eslint/js": "^10.0.1", "@stryker-mutator/core": "^9.6.0", "@stryker-mutator/typescript-checker": "^9.6.0", "@stryker-mutator/vitest-runner": "^9.6.0", @@ -68,16 +67,14 @@ "@vitejs/plugin-react": "^4.3.4", "@vitest/coverage-v8": "4.1.0", "dependency-cruiser": "^18.1.0", - "eslint": "^10.3.0", - "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-sonarjs": "^4.2.0", "fast-check": "^4.6.0", "jsdom": "^28.1.0", "knip": "^6.11.0", + "oxlint": "^1.75.0", "playwright": "^1.55.0", "ts-morph": "^28.0.0", "typescript": "~5.6.2", - "typescript-eslint": "^8.59.2", "vite": "^6.0.1", "vite-plugin-top-level-await": "^1.6.0", "vite-plugin-wasm": "^3.5.0", @@ -1887,6 +1884,7 @@ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "eslint-visitor-keys": "^3.4.3" }, @@ -1906,6 +1904,7 @@ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "license": "Apache-2.0", + "peer": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -1929,6 +1928,7 @@ "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", @@ -1944,6 +1944,7 @@ "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@eslint/core": "^1.2.1" }, @@ -1957,6 +1958,7 @@ "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@types/json-schema": "^7.0.15" }, @@ -1964,33 +1966,13 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@eslint/js": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", - "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "eslint": "^10.0.0" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, "node_modules/@eslint/object-schema": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", "dev": true, "license": "Apache-2.0", + "peer": true, "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" } @@ -2001,6 +1983,7 @@ "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" @@ -2086,6 +2069,7 @@ "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@humanfs/types": "^0.15.0" }, @@ -2099,6 +2083,7 @@ "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", @@ -2114,6 +2099,7 @@ "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", "dev": true, "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=18.18.0" } @@ -2124,6 +2110,7 @@ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=12.22" }, @@ -2138,6 +2125,7 @@ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=18.18" }, @@ -3668,6 +3656,353 @@ "win32" ] }, + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.75.0.tgz", + "integrity": "sha512-lutovtFzJqlRaqpZrCqSSGaHZzl9nIxxpjLzhSRLunN6dCLylj0uzlCyQGaQDIys7rrv8kVXiFO+R4Zpn0bX7g==", + "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.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.75.0.tgz", + "integrity": "sha512-hXI0hDgHkw4w5nfru72aG7y+2iQJmC4waH/KV6H/hbgA6yAP5jYNx0P9yug15Hs0tWl/+mda3Jjn/2gmDT48tw==", + "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.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.75.0.tgz", + "integrity": "sha512-D91BWbK/dMYfCcrghspPIuKs2D9LF4Z/OabVSQjw1AO6PWxArD7teDA48bm0ySFqWDaPVqmQRl5GMWNglTXyrQ==", + "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.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.75.0.tgz", + "integrity": "sha512-02mpwzf12BonZ6PT0TuQoomvEh2kVl2WGBIKWezCyToIS+rYkQZ6GXnARBAl9A4Ovm2V+Xe7M4KretyqmmcnJQ==", + "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.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.75.0.tgz", + "integrity": "sha512-qZJgLnDaBsiL5YESx2t/TZ8eXkL9fEkKoXEdzegROhlz9A0lgyGnZ0dAzJrh7LJAHQl2K9RdRueN2s/9N7+odg==", + "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.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.75.0.tgz", + "integrity": "sha512-7XlaWA5BJD3XpCfrEqjEe6Zseeb14S7QGa304XfwKignRaKQ+eIj775BQ7nIslggWickl4IsPUFqJ+/gAyNHVg==", + "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.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.75.0.tgz", + "integrity": "sha512-av6Tpv8yrcMMMOadOqENBhlsLRcGFXXwoQ0hzHhsmS9FJ4Wioy8we427GbcMe2XTxmL2e60T67H1Dyr3up+tAA==", + "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.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.75.0.tgz", + "integrity": "sha512-WcUhd8fHT5plrA14lANevl+hOl815mVI5t2hU21oFWrZKFXIVV/Sr4rWQV0NzSvzBupbMLNc5ErEA6Ehxh5jMg==", + "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.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.75.0.tgz", + "integrity": "sha512-UWzp5wRHFe/ESO3+eEaxXsTkYTGLYjnTsi/I5neEacXSItQ6WNleapfOAeA4x2b8nyhJ4uQxqvtv9pHv8kWJtQ==", + "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.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.75.0.tgz", + "integrity": "sha512-XEVRwGMLKCUKrvhLAz4F6AIh8MJrQVdSZtAmPpRZt9tGPsUnamPOcl3dS/ZQzJnar/Ymgc//+xho0L60Emzuxg==", + "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.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.75.0.tgz", + "integrity": "sha512-mAG4DUXqfLC8cTjMD2kt3jDmVzFREYtDyeLNdLdsCcBc4Zbl2EMuiFektGBilQwkNjYnMvCqJs55U+Hyb+b+jw==", + "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.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.75.0.tgz", + "integrity": "sha512-95hrAvriAlI+pekSomTFIn0+bawMDlDwTNVmdjsFusTHyL2JWh7TWvRNG/Lkim72uN8OiCcO9wcaC6omLP5E3w==", + "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.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.75.0.tgz", + "integrity": "sha512-4b6f2+FrtruAESrCqIKcrarzfrSx+wk2QNcp+RT91/Prc+pMQMAfyZ1rG1c3tFQNl8Bc616tx40uNXyxNBRPbQ==", + "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.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.75.0.tgz", + "integrity": "sha512-nshAhrUvXFUWOvqQ2soIw7HFNWvpvEV4o0cYSqPtzLiPF5gKyYTDOOTJ6Rn8g8K/iGvPIrbDA4v8+5MvnjJrrg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.75.0.tgz", + "integrity": "sha512-e4jNxLKnxLC6sYBQRxrI2pgIIxnmMtF8U/VwNYcjTT/CLS+spH624cYVnj07bTKwaEWT37/e025isOs6j/0xqA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.75.0.tgz", + "integrity": "sha512-hZ2lH+1qLf/DiEP9UWuQTK2JWj/BgvMB4jhIV4SmNU1wfEiYYX4TynQyAZXx0j9X4qRYizAL042SKaV+8ynh4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.75.0.tgz", + "integrity": "sha512-Ilj6PNzGDS3bCU0MSJH7Msh0NhH+T/mRp2shwg+q+GHeVlPwP5LEboW96aW+3kVKFk6zYZy1Xi5pZkqZh6X8KQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.75.0.tgz", + "integrity": "sha512-QVit2nOEOiPhkmsrksPSkoGCdnZRNkspt8fwoYyP09te1VEbnSj4LAxua4rc8FKTmWkySVe05j8iz9GXYfF1AQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.75.0.tgz", + "integrity": "sha512-DSxnNkBUAYARPwJtR12Ig3deWr8w0H997xP6jy33i+e0SyYJw8FKuz4+cZtpmPEhQmvlPJE3X/2vNxDmLkd/rA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/@preact/signals-core": { "version": "1.14.0", "resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.14.0.tgz", @@ -4667,7 +5002,8 @@ "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/estree": { "version": "1.0.8", @@ -4688,7 +5024,8 @@ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/node": { "version": "25.5.2", @@ -4742,249 +5079,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.2.tgz", - "integrity": "sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.59.2", - "@typescript-eslint/type-utils": "8.59.2", - "@typescript-eslint/utils": "8.59.2", - "@typescript-eslint/visitor-keys": "8.59.2", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.59.2", - "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": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.2.tgz", - "integrity": "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.59.2", - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/typescript-estree": "8.59.2", - "@typescript-eslint/visitor-keys": "8.59.2", - "debug": "^4.4.3" - }, - "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/@typescript-eslint/project-service": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.2.tgz", - "integrity": "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.59.2", - "@typescript-eslint/types": "^8.59.2", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.2.tgz", - "integrity": "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/visitor-keys": "8.59.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.2.tgz", - "integrity": "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.2.tgz", - "integrity": "sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/typescript-estree": "8.59.2", - "@typescript-eslint/utils": "8.59.2", - "debug": "^4.4.3", - "ts-api-utils": "^2.5.0" - }, - "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/@typescript-eslint/types": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.2.tgz", - "integrity": "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.2.tgz", - "integrity": "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.59.2", - "@typescript-eslint/tsconfig-utils": "8.59.2", - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/visitor-keys": "8.59.2", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.2.tgz", - "integrity": "sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.59.2", - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/typescript-estree": "8.59.2" - }, - "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/@typescript-eslint/visitor-keys": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz", - "integrity": "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.59.2", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -5726,7 +5820,8 @@ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/dependency-cruiser": { "version": "18.1.0", @@ -5999,6 +6094,7 @@ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -6012,6 +6108,7 @@ "integrity": "sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -6062,26 +6159,6 @@ } } }, - "node_modules/eslint-plugin-react-hooks": { - "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": { - "@babel/core": "^7.24.4", - "@babel/parser": "^7.24.4", - "hermes-parser": "^0.25.1", - "zod": "^3.25.0 || ^4.0.0", - "zod-validation-error": "^3.5.0 || ^4.0.0" - }, - "engines": { - "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 || ^10.0.0" - } - }, "node_modules/eslint-plugin-sonarjs": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-4.2.0.tgz", @@ -6126,6 +6203,7 @@ "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", @@ -6145,6 +6223,7 @@ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", + "peer": true, "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" }, @@ -6158,6 +6237,7 @@ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -6174,7 +6254,8 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/espree": { "version": "11.2.0", @@ -6182,6 +6263,7 @@ "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", @@ -6200,6 +6282,7 @@ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "dependencies": { "estraverse": "^5.1.0" }, @@ -6213,6 +6296,7 @@ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "estraverse": "^5.2.0" }, @@ -6226,6 +6310,7 @@ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=4.0" } @@ -6246,6 +6331,7 @@ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -6328,14 +6414,16 @@ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/fast-sha256": { "version": "1.3.0", @@ -6437,6 +6525,7 @@ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "flat-cache": "^4.0.0" }, @@ -6450,6 +6539,7 @@ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -6467,6 +6557,7 @@ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" @@ -6480,7 +6571,8 @@ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, - "license": "ISC" + "license": "ISC", + "peer": true }, "node_modules/formatly": { "version": "0.3.0", @@ -6615,6 +6707,7 @@ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "is-glob": "^4.0.3" }, @@ -6707,23 +6800,6 @@ "node": ">= 0.4" } }, - "node_modules/hermes-estree": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", - "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", - "dev": true, - "license": "MIT" - }, - "node_modules/hermes-parser": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", - "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hermes-estree": "0.25.1" - } - }, "node_modules/html-encoding-sniffer": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", @@ -6811,6 +6887,7 @@ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 4" } @@ -6821,6 +6898,7 @@ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.8.19" } @@ -6884,6 +6962,7 @@ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -6894,6 +6973,7 @@ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -7137,7 +7217,8 @@ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/json-rpc-2.0": { "version": "1.7.1", @@ -7158,7 +7239,8 @@ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/json5": { "version": "2.2.3", @@ -7189,6 +7271,7 @@ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "json-buffer": "3.0.1" } @@ -7249,6 +7332,7 @@ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -7291,6 +7375,7 @@ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "p-locate": "^5.0.0" }, @@ -7578,7 +7663,8 @@ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/node-gyp-build-optional-packages": { "version": "5.1.1", @@ -7662,6 +7748,7 @@ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", @@ -7744,12 +7831,62 @@ "@oxc-resolver/binding-win32-x64-msvc": "11.19.1" } }, + "node_modules/oxlint": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.75.0.tgz", + "integrity": "sha512-m9WzjRcRYA/uqIZDa9tclrieoPJ/ln1QYTKdFx6NUOs8uY5DiHlIwRQoCrHT6OM6O3ww3l2skY5gO7G7ZphE7g==", + "dev": true, + "license": "MIT", + "bin": { + "oxlint": "bin/oxlint" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@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" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=7.0.2001", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "yocto-queue": "^0.1.0" }, @@ -7766,6 +7903,7 @@ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "p-limit": "^3.0.2" }, @@ -7815,6 +7953,7 @@ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -7945,6 +8084,7 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.8.0" } @@ -8854,6 +8994,7 @@ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "prelude-ls": "^1.2.1" }, @@ -8902,30 +9043,6 @@ "node": ">=14.17" } }, - "node_modules/typescript-eslint": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.2.tgz", - "integrity": "sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.59.2", - "@typescript-eslint/parser": "8.59.2", - "@typescript-eslint/typescript-estree": "8.59.2", - "@typescript-eslint/utils": "8.59.2" - }, - "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/unbash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/unbash/-/unbash-3.0.0.tgz", @@ -9010,6 +9127,7 @@ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "punycode": "^2.1.0" } @@ -9347,6 +9465,7 @@ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -9446,6 +9565,7 @@ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -9476,19 +9596,6 @@ "url": "https://github.com/sponsors/colinhacks" } }, - "node_modules/zod-validation-error": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", - "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - } - }, "vendor/bombadil-custom-actions-npm": { "name": "@antithesishq/bombadil", "version": "0.6.1-custom-actions.eb0c7fa", diff --git a/package.json b/package.json index ff58b873..c05a156f 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,8 @@ "test:perf": "node scripts/require-perf-test-env.mjs && vitest run --config vitest.config.perf.ts --maxWorkers=1 src/testing/perf/__tests__/**/*.test.{ts,tsx} tests/perf/**/*.test.{ts,tsx}", "ratchet:chainlink19": "node scripts/chainlink19-ratchet.mjs", "test:bombadil": "node scripts/require-browser-test-env.mjs && vitest run --config vitest.config.invariants.ts --maxWorkers=1 tests/invariants/bombadil/reams.bombadil.test.ts", - "lint": "eslint src/plugins src/plugin/builtin-suggesters src/layout/file-tree src/layout/fuzzy-search", - "lint:check": "eslint src/plugins src/plugin/builtin-suggesters src/layout/file-tree src/layout/fuzzy-search --max-warnings=0", + "lint": "oxlint src/plugins src/plugin/builtin-suggesters src/layout/file-tree src/layout/fuzzy-search", + "lint:check": "oxlint src/plugins src/plugin/builtin-suggesters src/layout/file-tree src/layout/fuzzy-search --max-warnings=0", "knip": "knip", "knip:report": "knip --no-exit-code", "depcruise": "depcruise --config .dependency-cruiser.cjs src", @@ -77,7 +77,6 @@ }, "devDependencies": { "@antithesishq/bombadil": "file:vendor/bombadil-custom-actions-npm", - "@eslint/js": "^10.0.1", "@stryker-mutator/core": "^9.6.0", "@stryker-mutator/typescript-checker": "^9.6.0", "@stryker-mutator/vitest-runner": "^9.6.0", @@ -93,16 +92,14 @@ "@vitejs/plugin-react": "^4.3.4", "@vitest/coverage-v8": "4.1.0", "dependency-cruiser": "^18.1.0", - "eslint": "^10.3.0", - "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-sonarjs": "^4.2.0", "fast-check": "^4.6.0", "jsdom": "^28.1.0", "knip": "^6.11.0", + "oxlint": "^1.75.0", "playwright": "^1.55.0", "ts-morph": "^28.0.0", "typescript": "~5.6.2", - "typescript-eslint": "^8.59.2", "vite": "^6.0.1", "vite-plugin-top-level-await": "^1.6.0", "vite-plugin-wasm": "^3.5.0", diff --git a/src/layout/file-tree/CreateItemModal.tsx b/src/layout/file-tree/CreateItemModal.tsx index 883aedef..3dc4d164 100644 --- a/src/layout/file-tree/CreateItemModal.tsx +++ b/src/layout/file-tree/CreateItemModal.tsx @@ -3,7 +3,7 @@ import { useState, useEffect, useRef, useCallback, type FormEvent } from 'react' import { createPortal } from 'react-dom' import type { AutomergeUrl } from '@automerge/automerge-repo' import type { AdoptFolderErrorCode } from '../../plugin/vault' -// FIXME(F-BUILTIN-API-3): File-tree create flow still reuses editor image-paste +// RATIONALE(plugin-api): file-tree create flow still reuses editor image-paste // helpers until upload/filename sanitizing is exposed through a plugin-safe API. // eslint-disable-next-line no-restricted-imports import { readFileAsUint8Array, sanitizeFilename } from '../../editor/image-paste-utils' @@ -49,6 +49,43 @@ export interface CreateItemModalProps { type ModalMode = 'file' | 'folder' | 'adopt-folder' +interface CreateItemSubmitters { + pathPrefix: string | undefined + onAdoptFolder?: (intermediateSegments: string[], url: AutomergeUrl) => Promise + onCreateFolder: (name: string) => Promise + onCreateFile: (name: string) => Promise + onCreateFileFromTemplate?: (name: string, templatePath: string) => Promise + selectedTemplate: string +} + +async function submitCreateItem( + parsed: Exclude, + submitters: CreateItemSubmitters, +): Promise { + const { pathPrefix, onAdoptFolder, onCreateFolder, onCreateFile, onCreateFileFromTemplate, selectedTemplate } = submitters + if (parsed.kind === 'adopt-folder') { + if (!onAdoptFolder) return 'URL adoption not available here' + await onAdoptFolder(parsed.intermediateSegments, parsed.url) + return null + } + + const fullPath = joinCreateItemPath(pathPrefix, parsed.path) + const joinedError = validateCreateItemInput(fullPath, parsed.kind === 'create-folder' ? 'folder' : 'file') + if (joinedError) return joinedError + if (parsed.kind === 'create-folder') { + await onCreateFolder(fullPath) + return null + } + + const finalName = ensureMdExtension(fullPath) + if (selectedTemplate && onCreateFileFromTemplate) { + await onCreateFileFromTemplate(finalName, selectedTemplate) + } else { + await onCreateFile(finalName) + } + return null +} + /** * User-facing copy for an {@link AdoptFolderErrorCode}. We map the error * code (which is the API contract from `Vault.adoptFolderByUrl`) to a @@ -75,8 +112,7 @@ function adoptFolderErrorCopy(code: AdoptFolderErrorCode): string | null { // Exhaustiveness check — if a new code is added we want a // compile-time error here. TypeScript narrows `code` to `never`. const _exhaustive: never = code - void _exhaustive - return null + return _exhaustive } } } @@ -169,10 +205,12 @@ export function CreateItemModal({ ? parseCreateItemInput(trimmedValue, { allowUrlAdoption: !!onAdoptFolder }) : null - const mode: ModalMode = - parsedForValidation?.kind === 'adopt-folder' ? 'adopt-folder' : - parsedForValidation?.kind === 'create-folder' ? 'folder' : - 'file' + let mode: ModalMode = 'file' + if (parsedForValidation?.kind === 'adopt-folder') { + mode = 'adopt-folder' + } else if (parsedForValidation?.kind === 'create-folder') { + mode = 'folder' + } // Joined-path validation regression guard. For create-file / // create-folder, run the existing joined-path check the modal has @@ -287,52 +325,20 @@ export function CreateItemModal({ return } - // R6: explicit guard for onAdoptFolder. The parser already returns - // `invalid` for URL inputs when `allowUrlAdoption` is false, so we - // shouldn't reach `adopt-folder` here without the callback. But the - // contract is worth making explicit: if for any reason the prop is - // missing, surface a clear inline error rather than asserting. - if (parsed.kind === 'adopt-folder' && !onAdoptFolder) { - setError('URL adoption not available here') - return - } - setIsSubmitting(true) setError(null) try { - switch (parsed.kind) { - case 'adopt-folder': - // `onAdoptFolder` is guaranteed defined here: the guard above - // catches the missing-callback case explicitly. - await onAdoptFolder!(parsed.intermediateSegments, parsed.url) - break - case 'create-folder': { - const fullPath = joinCreateItemPath(pathPrefix, parsed.path) - const joinedError = validateCreateItemInput(fullPath, 'folder') - if (joinedError) { - setError(joinedError) - setIsSubmitting(false) - return - } - await onCreateFolder(fullPath) - break - } - case 'create-file': { - const fullPath = joinCreateItemPath(pathPrefix, parsed.path) - const joinedError = validateCreateItemInput(fullPath, 'file') - if (joinedError) { - setError(joinedError) - setIsSubmitting(false) - return - } - const finalName = ensureMdExtension(fullPath) - if (selectedTemplate && onCreateFileFromTemplate) { - await onCreateFileFromTemplate(finalName, selectedTemplate) - } else { - await onCreateFile(finalName) - } - break - } + const submitError = await submitCreateItem(parsed, { + pathPrefix, + onAdoptFolder, + onCreateFolder, + onCreateFile, + onCreateFileFromTemplate, + selectedTemplate, + }) + if (submitError) { + setError(submitError) + return } onClose() } catch (err) { diff --git a/src/layout/file-tree/FileTree.tsx b/src/layout/file-tree/FileTree.tsx index aa5ac490..46df9cb1 100644 --- a/src/layout/file-tree/FileTree.tsx +++ b/src/layout/file-tree/FileTree.tsx @@ -15,8 +15,9 @@ import type { FileTreeView } from '../../plugins/file-tree/file-tree-view' import { FileTreeItem } from './FileTreeItem' import { FileTreeContextMenu } from './FileTreeContextMenu' import { CreateItemModal } from './CreateItemModal' -// FIXME(F-BUILTIN-API-2): File tree search integration still depends on the +// File tree search integration still depends on the // host SearchContext until a plugin-safe search surface exists. +// RATIONALE(plugin-api): file-tree uses the editor image-paste workflow. // eslint-disable-next-line no-restricted-imports import { useSearch } from '../../services/SearchContext' import { PromptDialog, ConfirmDialog } from './PromptDialog' @@ -316,9 +317,12 @@ export function FileTree({ app, openDocument, view }: FileTreeProps) { const isActive = activeFileUrl !== null && activeFileUrl === url const entry: DirectoryEntry = { name: item.name, type: isFolder ? 'folder' : 'file', url } - const folderUrl = isFolder ? url : ( - item.parent ? (vault.getUrlForPath(item.parent.path) ?? vault.rootUrl) : vault.rootUrl - ) + let folderUrl = vault.rootUrl + if (isFolder) { + folderUrl = url + } else if (item.parent) { + folderUrl = vault.getUrlForPath(item.parent.path) ?? vault.rootUrl + } const displayName = isFileItem ? displayNames.get(url) : undefined return ( diff --git a/src/layout/file-tree/PromptDialog.tsx b/src/layout/file-tree/PromptDialog.tsx index bc705bad..a5cf7145 100644 --- a/src/layout/file-tree/PromptDialog.tsx +++ b/src/layout/file-tree/PromptDialog.tsx @@ -18,6 +18,18 @@ export interface PromptDialogProps { onClose: () => void } +function addEscapeKeyListener(isOpen: boolean, onClose: () => void) { + if (!isOpen) return undefined + function handleKeyDown(e: globalThis.KeyboardEvent) { + if (e.key === 'Escape') { + e.stopPropagation() + onClose() + } + } + document.addEventListener('keydown', handleKeyDown) + return () => document.removeEventListener('keydown', handleKeyDown) +} + export function PromptDialog({ isOpen, title, @@ -45,15 +57,7 @@ export function PromptDialog({ // Escape to close useEffect(() => { - if (!isOpen) return - function handleKeyDown(e: globalThis.KeyboardEvent) { - if (e.key === 'Escape') { - e.stopPropagation() - onClose() - } - } - document.addEventListener('keydown', handleKeyDown) - return () => document.removeEventListener('keydown', handleKeyDown) + return addEscapeKeyListener(isOpen, onClose) }, [isOpen, onClose]) const handleSubmit = useCallback(() => { @@ -160,15 +164,7 @@ export function ConfirmDialog({ // Escape to close useEffect(() => { - if (!isOpen) return - function handleKeyDown(e: globalThis.KeyboardEvent) { - if (e.key === 'Escape') { - e.stopPropagation() - onClose() - } - } - document.addEventListener('keydown', handleKeyDown) - return () => document.removeEventListener('keydown', handleKeyDown) + return addEscapeKeyListener(isOpen, onClose) }, [isOpen, onClose]) const handleBackdropClick = useCallback((e: React.MouseEvent) => { diff --git a/src/layout/file-tree/create-item-path.ts b/src/layout/file-tree/create-item-path.ts index e37cd8a6..9893e357 100644 --- a/src/layout/file-tree/create-item-path.ts +++ b/src/layout/file-tree/create-item-path.ts @@ -25,6 +25,33 @@ export interface ParseCreateItemInputOptions { allowUrlAdoption?: boolean } +function validateIntermediateSegments(segments: string[]): string | null { + if (segments.some(segment => segment.length === 0)) return 'Invalid path' + if (segments.some(segment => segment === '.' || segment === '..')) return 'Invalid path' + return null +} + +function parseAdoptionInput(trimmed: string, allowUrlAdoption: boolean): ParsedCreateItemInput { + if (!allowUrlAdoption) { + return { kind: 'invalid', reason: 'URL adoption not available here' } + } + if (trimmed.endsWith('/')) { + return { kind: 'invalid', reason: 'automerge: URL must be the last segment (no trailing slash)' } + } + const segments = trimmed.split('/') + const last = segments[segments.length - 1] + if (!last.startsWith('automerge:')) { + return { kind: 'invalid', reason: 'automerge: URL must be the last segment' } + } + if (!AUTOMERGE_URL_TOKEN_RE.test(last)) { + return { kind: 'invalid', reason: 'Not a valid automerge: URL' } + } + const intermediateSegments = segments.slice(0, -1) + const validationError = validateIntermediateSegments(intermediateSegments) + if (validationError) return { kind: 'invalid', reason: validationError } + return { kind: 'adopt-folder', intermediateSegments, url: last as AutomergeUrl } +} + export function joinCreateItemPath(pathPrefix: string | undefined, value: string): string { const trimmedPrefix = pathPrefix?.trim() ?? '' const trimmedValue = value.trim() @@ -98,30 +125,7 @@ export function parseCreateItemInput( // routed through URL parsing — they fall through to file/folder // grammar and are surfaced as invalid by the standard validator. if (/(^|\/)automerge:/.test(trimmed)) { - if (!allowUrlAdoption) { - return { kind: 'invalid', reason: 'URL adoption not available here' } - } - if (trimmed.endsWith('/')) { - return { kind: 'invalid', reason: 'automerge: URL must be the last segment (no trailing slash)' } - } - const segments = trimmed.split('/') - const last = segments[segments.length - 1] - if (!last.startsWith('automerge:')) { - return { kind: 'invalid', reason: 'automerge: URL must be the last segment' } - } - if (!AUTOMERGE_URL_TOKEN_RE.test(last)) { - return { kind: 'invalid', reason: 'Not a valid automerge: URL' } - } - const intermediateSegments = segments.slice(0, -1) - for (const seg of intermediateSegments) { - if (seg.length === 0) return { kind: 'invalid', reason: 'Invalid path' } - if (seg === '.' || seg === '..') return { kind: 'invalid', reason: 'Invalid path' } - } - return { - kind: 'adopt-folder', - intermediateSegments, - url: last as AutomergeUrl, - } + return parseAdoptionInput(trimmed, allowUrlAdoption) } // No URL token — fall back to file/folder grammar. diff --git a/src/layout/fuzzy-search/FuzzySearchOverlay.tsx b/src/layout/fuzzy-search/FuzzySearchOverlay.tsx index f6d428f6..94d0e533 100644 --- a/src/layout/fuzzy-search/FuzzySearchOverlay.tsx +++ b/src/layout/fuzzy-search/FuzzySearchOverlay.tsx @@ -1,7 +1,8 @@ import { useState, useEffect, useRef, useCallback, useMemo, type KeyboardEvent, type ReactNode } from 'react' import type { AutomergeUrl } from '@automerge/automerge-repo' -// FIXME(F-BUILTIN-API-4): Quick search still depends on the concrete host +// Quick search still depends on the concrete host // SearchService result shape until a plugin-safe search interface exists. +// RATIONALE(plugin-api): fuzzy search needs editor-specific selection helpers. // eslint-disable-next-line no-restricted-imports import type { SearchResult } from '../../services/search-service' import './FuzzySearchOverlay.css' @@ -43,6 +44,57 @@ interface FuzzySearchOverlayProps { initialQuery?: string } +function findHighlights(lowerText: string, words: string[]): Array<[number, number]> { + const highlights: Array<[number, number]> = [] + for (const word of words) { + let searchFrom = 0 + while (searchFrom < lowerText.length) { + const index = lowerText.indexOf(word, searchFrom) + if (index === -1) break + highlights.push([index, index + word.length]) + searchFrom = index + word.length + } + } + return highlights +} + +function mergeHighlights(highlights: Array<[number, number]>): Array<[number, number]> { + highlights.sort((a, b) => a[0] - b[0]) + const merged: Array<[number, number]> = [highlights[0]] + for (let index = 1; index < highlights.length; index++) { + const previous = merged[merged.length - 1] + const current = highlights[index] + if (current[0] <= previous[1]) previous[1] = Math.max(previous[1], current[1]) + else merged.push(current) + } + return merged +} + +function highlightMatches(text: string, searchQuery: string): ReactNode { + if (!searchQuery.trim()) return text + const words = searchQuery.toLowerCase().split(/\s+/).filter(Boolean) + if (words.length === 0) return text + + const highlights = findHighlights(text.toLowerCase(), words) + if (highlights.length === 0) return text + + const parts: ReactNode[] = [] + let position = 0 + for (const [start, end] of mergeHighlights(highlights)) { + if (position < start) parts.push(text.slice(position, start)) + parts.push({text.slice(start, end)}) + position = end + } + if (position < text.length) parts.push(text.slice(position)) + return parts +} + +function getNoPreviewMessage(isCreateEntrySelected: boolean, totalItems: number, query: string): string { + if (isCreateEntrySelected || query.trim()) return 'No preview available' + if (totalItems > 0) return 'Select a file to preview' + return 'Select a file to preview' +} + /** * Full-screen fuzzy search overlay with keyboard navigation and preview. * Shows raw markdown content in the preview pane. @@ -163,6 +215,10 @@ export function FuzzySearchOverlay({ } }, [activeCreatePreview, isCreating, onCreate, onClose]) + const startCreateEntry = useCallback(() => { + handleCreateEntry().catch(() => {}) + }, [handleCreateEntry]) + const handleKeyDown = useCallback((e: KeyboardEvent) => { switch (e.key) { case 'ArrowDown': @@ -179,7 +235,7 @@ export function FuzzySearchOverlay({ e.preventDefault() if (showCreateEntry && selectedIndex === displayItems.length) { // Create entry is selected (it sits at index === displayItems.length) - void handleCreateEntry() + startCreateEntry() } else if (displayItems[selectedIndex]) { // A regular search result is selected const item = displayItems[selectedIndex] @@ -192,7 +248,7 @@ export function FuzzySearchOverlay({ onClose() break } - }, [displayItems, selectedIndex, totalItems, showCreateEntry, onSelect, onClose, handleCreateEntry]) + }, [displayItems, selectedIndex, totalItems, showCreateEntry, onSelect, onClose, startCreateEntry]) const handleBackdropClick = useCallback(() => { onClose() @@ -209,64 +265,6 @@ export function FuzzySearchOverlay({ onClose() }, [onSelect, onClose]) - // Highlight matching text in content - const highlightMatches = useCallback((text: string, searchQuery: string): ReactNode => { - if (!searchQuery.trim()) return text - - const words = searchQuery.toLowerCase().split(/\s+/).filter(Boolean) - if (words.length === 0) return text - - // Build a set of character ranges to highlight by scanning the full text - // for each query word independently - const lowerText = text.toLowerCase() - const highlights: Array<[number, number]> = [] - - for (const word of words) { - let searchFrom = 0 - while (searchFrom < lowerText.length) { - const idx = lowerText.indexOf(word, searchFrom) - if (idx === -1) break - highlights.push([idx, idx + word.length]) - searchFrom = idx + word.length - } - } - - if (highlights.length === 0) return text - - // Merge overlapping ranges, sort by start - highlights.sort((a, b) => a[0] - b[0]) - const merged: Array<[number, number]> = [highlights[0]] - for (let i = 1; i < highlights.length; i++) { - const prev = merged[merged.length - 1] - const curr = highlights[i] - if (curr[0] <= prev[1]) { - prev[1] = Math.max(prev[1], curr[1]) - } else { - merged.push(curr) - } - } - - // Build parts array from merged ranges - const parts: ReactNode[] = [] - let pos = 0 - for (const [start, end] of merged) { - if (pos < start) { - parts.push(text.slice(pos, start)) - } - parts.push( - - {text.slice(start, end)} - - ) - pos = end - } - if (pos < text.length) { - parts.push(text.slice(pos)) - } - - return parts - }, []) - if (!isOpen) return null const isCreateEntrySelected = showCreateEntry && selectedIndex === displayItems.length @@ -275,6 +273,14 @@ export function FuzzySearchOverlay({ const listId = 'fuzzy-search-results' const hasItems = totalItems > 0 const selectedId = hasItems ? `fuzzy-search-result-${selectedIndex}` : undefined + let preview: ReactNode + if (previewContent) { + preview =
{highlightMatches(previewContent, query)}
+ } else if (selectedItem) { + preview =
Loading preview...
+ } else { + preview =
{getNoPreviewMessage(isCreateEntrySelected, totalItems, query)}
+ } return (
{ if (!isCreating) void handleCreateEntry() }} + onClick={() => { if (!isCreating) startCreateEntry() }} onMouseEnter={() => { if (!isCreating) setSelectedIndex(displayItems.length) }} > + @@ -368,23 +374,7 @@ export function FuzzySearchOverlay({
- {previewContent ? ( -
-                {highlightMatches(previewContent, query)}
-              
- ) : selectedItem ? ( -
Loading preview...
- ) : ( -
- {isCreateEntrySelected - ? 'No preview available' - : totalItems > 0 - ? 'Select a file to preview' - : query.trim() - ? 'No preview available' - : 'Select a file to preview'} -
- )} + {preview}
diff --git a/src/layout/fuzzy-search/useSearchOverlay.ts b/src/layout/fuzzy-search/useSearchOverlay.ts index 73eff935..e93886ff 100644 --- a/src/layout/fuzzy-search/useSearchOverlay.ts +++ b/src/layout/fuzzy-search/useSearchOverlay.ts @@ -1,7 +1,8 @@ import { useState, useCallback } from 'react' import type { AutomergeUrl } from '@automerge/automerge-repo' -// FIXME(F-BUILTIN-API-4): Quick search still depends on the concrete host +// Quick search still depends on the concrete host // SearchService until a plugin-safe search interface exists. +// RATIONALE(plugin-api): fuzzy search needs editor-specific selection helpers. // eslint-disable-next-line no-restricted-imports import type { SearchService, SearchResult } from '../../services/search-service' diff --git a/src/plugin/builtin-suggesters/fallback-context.ts b/src/plugin/builtin-suggesters/fallback-context.ts new file mode 100644 index 00000000..5e68d275 --- /dev/null +++ b/src/plugin/builtin-suggesters/fallback-context.ts @@ -0,0 +1,32 @@ +import type { Editor } from '../editor' + +export function isInsideFallbackFenceOrFrontmatter(editor: Editor, line: number): boolean { + return isInsideFallbackFrontmatter(editor, line) || isInsideFallbackFence(editor, line) +} + +function isInsideFallbackFrontmatter(editor: Editor, line: number): boolean { + if (editor.lineCount() === 0) return false + + const first = editor.getLine(0) + if (first !== '---' && !first.startsWith('--- ')) return false + + for (let i = 1; i <= line && i < editor.lineCount(); i++) { + const text = editor.getLine(i) + if (isFrontmatterClosingLine(text)) return line < i + } + + return true +} + +function isFrontmatterClosingLine(text: string): boolean { + return text === '---' || text === '...' || text.startsWith('--- ') || text.startsWith('... ') +} + +function isInsideFallbackFence(editor: Editor, line: number): boolean { + let open = 0 + for (let i = 0; i < line && i < editor.lineCount(); i++) { + const text = editor.getLine(i).trim() + if (text.startsWith('```') || text.startsWith('~~~')) open++ + } + return open % 2 === 1 +} diff --git a/src/plugin/builtin-suggesters/image-suggester.ts b/src/plugin/builtin-suggesters/image-suggester.ts index 308739bf..760b077f 100644 --- a/src/plugin/builtin-suggesters/image-suggester.ts +++ b/src/plugin/builtin-suggesters/image-suggester.ts @@ -34,8 +34,7 @@ import type { TFile } from '../files' import type { EditorPosition } from '../types' import type { WikilinkPlugin, WikilinkFileSuggestion } from '../../plugins/wikilink/wikilink-plugin' import type { CM6EditorAdapter } from '../cm6-editor' -// FIXME(F-AUTOCOMPLETE-1, F-EDITOR-7): built-in suggesters still reach into -// markdown-context until the host exposes a plugin-safe fence/frontmatter path. +// RATIONALE(plugin-api): markdown context helpers are not yet exposed to suggesters. // eslint-disable-next-line no-restricted-imports import { isRangeInBlockCodeContext, @@ -43,6 +42,7 @@ import { getFrontmatterRange, } from '../../editor/extensions/markdown-context' import { containsWikilinkBreakingChar } from './wikilink-suggester' +import { isInsideFallbackFenceOrFrontmatter } from './fallback-context' export class ImageSuggester extends EditorSuggest { constructor( @@ -93,37 +93,7 @@ export class ImageSuggester extends EditorSuggest { if (isRangeInBlockCodeContext(state, offset, offset)) return true return false } - return this.fallbackFenceFrontmatterCheck(editor, cursor.line) - } - - /** - * Textual fallback for `MinimalEditor` callers (unit-test stubs). Mirrors - * the wikilink suggester's fallback exactly: top-level `---`-block - * frontmatter detection + global triple-backtick / tilde fence counting. - */ - private fallbackFenceFrontmatterCheck(editor: Editor, line: number): boolean { - if (editor.lineCount() > 0) { - const first = editor.getLine(0) - if (first === '---' || first.startsWith('--- ')) { - let closeLine = -1 - for (let i = 1; i <= line && i < editor.lineCount(); i++) { - const t = editor.getLine(i) - if (t === '---' || t === '...' || t.startsWith('--- ') || t.startsWith('... ')) { - closeLine = i - break - } - } - if (closeLine === -1 || line < closeLine) return true - } - } - let open = 0 - for (let i = 0; i < line && i < editor.lineCount(); i++) { - const t = editor.getLine(i).trim() - if (t.startsWith('```') || t.startsWith('~~~')) { - open++ - } - } - return open % 2 === 1 + return isInsideFallbackFenceOrFrontmatter(editor, cursor.line) } async getSuggestions(ctx: EditorSuggestContext): Promise { diff --git a/src/plugin/builtin-suggesters/tag-suggester.ts b/src/plugin/builtin-suggesters/tag-suggester.ts index 149624b8..bb7ba029 100644 --- a/src/plugin/builtin-suggesters/tag-suggester.ts +++ b/src/plugin/builtin-suggesters/tag-suggester.ts @@ -28,18 +28,17 @@ import type { Editor } from '../editor' import type { TFile } from '../files' import type { EditorPosition } from '../types' import type { CM6EditorAdapter } from '../cm6-editor' -// FIXME(F-AUTOCOMPLETE-1, F-EDITOR-7): built-in suggesters still reach into -// markdown-context until the host exposes a plugin-safe fence/frontmatter path. +// RATIONALE(plugin-api): markdown context helpers are not yet exposed to suggesters. // eslint-disable-next-line no-restricted-imports import { isRangeInBlockCodeContext, isRangeInFrontmatter, getFrontmatterRange, } from '../../editor/extensions/markdown-context' -// FIXME(F-AUTOCOMPLETE-6): TagSuggester should consume the public -// prepareFuzzySearch/prepareSimpleSearch API instead of services/fuzzy-score. +// RATIONALE(plugin-api): markdown context helpers are not yet exposed to suggesters. // eslint-disable-next-line no-restricted-imports import { fuzzyScore } from '../../services/fuzzy-score' +import { isInsideFallbackFenceOrFrontmatter } from './fallback-context' export interface TagSuggestion { tag: string @@ -88,7 +87,7 @@ export const MAX_QUERY_SUGGESTIONS = 50 * The leading-char constraint is the tight one; everything after is * permissive. */ -const TAG_QUERY_TAIL_RE = /^(?:[\w][\w/-]*)?$/ +const TAG_QUERY_TAIL_RE = /^(?:\w[\w/-]*)?$/ export class TagSuggester extends EditorSuggest { constructor(app: App) { @@ -149,33 +148,7 @@ export class TagSuggester extends EditorSuggest { if (isRangeInBlockCodeContext(state, offset, offset)) return true return false } - return this.fallbackFenceFrontmatterCheck(editor, cursor.line) - } - - private fallbackFenceFrontmatterCheck(editor: Editor, line: number): boolean { - // Same heuristic as wikilink-suggester - if (editor.lineCount() > 0) { - const first = editor.getLine(0) - if (first === '---' || first.startsWith('--- ')) { - let closeLine = -1 - for (let i = 1; i <= line && i < editor.lineCount(); i++) { - const t = editor.getLine(i) - if (t === '---' || t === '...' || t.startsWith('--- ') || t.startsWith('... ')) { - closeLine = i - break - } - } - if (closeLine === -1 || line < closeLine) return true - } - } - let open = 0 - for (let i = 0; i < line && i < editor.lineCount(); i++) { - const t = editor.getLine(i).trim() - if (t.startsWith('```') || t.startsWith('~~~')) { - open++ - } - } - return open % 2 === 1 + return isInsideFallbackFenceOrFrontmatter(editor, cursor.line) } async getSuggestions(ctx: EditorSuggestContext): Promise { diff --git a/src/plugin/builtin-suggesters/wikilink-suggester.ts b/src/plugin/builtin-suggesters/wikilink-suggester.ts index 170af4d8..4c471d16 100644 --- a/src/plugin/builtin-suggesters/wikilink-suggester.ts +++ b/src/plugin/builtin-suggesters/wikilink-suggester.ts @@ -33,14 +33,14 @@ import type { TFile } from '../files' import type { EditorPosition } from '../types' import type { WikilinkPlugin, WikilinkSuggestion } from '../../plugins/wikilink/wikilink-plugin' import type { CM6EditorAdapter } from '../cm6-editor' -// FIXME(F-AUTOCOMPLETE-1, F-EDITOR-7): built-in suggesters still reach into -// markdown-context until the host exposes a plugin-safe fence/frontmatter path. +// RATIONALE(plugin-api): markdown context helpers are not yet exposed to suggesters. // eslint-disable-next-line no-restricted-imports import { isRangeInBlockCodeContext, isRangeInFrontmatter, getFrontmatterRange, } from '../../editor/extensions/markdown-context' +import { isInsideFallbackFenceOrFrontmatter } from './fallback-context' /** * Returns true if the target contains characters that break wikilink syntax: @@ -101,36 +101,7 @@ export class WikilinkSuggester extends EditorSuggest { if (isRangeInBlockCodeContext(state, offset, offset)) return true return false } - return this.fallbackFenceFrontmatterCheck(editor, cursor.line) - } - - /** - * Textual-only fallback for `MinimalEditor` callers. Only used in tests; - * production always goes through the syntaxTree path above. - */ - private fallbackFenceFrontmatterCheck(editor: Editor, line: number): boolean { - if (editor.lineCount() > 0) { - const first = editor.getLine(0) - if (first === '---' || first.startsWith('--- ')) { - let closeLine = -1 - for (let i = 1; i <= line && i < editor.lineCount(); i++) { - const t = editor.getLine(i) - if (t === '---' || t === '...' || t.startsWith('--- ') || t.startsWith('... ')) { - closeLine = i - break - } - } - if (closeLine === -1 || line < closeLine) return true - } - } - let open = 0 - for (let i = 0; i < line && i < editor.lineCount(); i++) { - const t = editor.getLine(i).trim() - if (t.startsWith('```') || t.startsWith('~~~')) { - open++ - } - } - return open % 2 === 1 + return isInsideFallbackFenceOrFrontmatter(editor, cursor.line) } async getSuggestions(ctx: EditorSuggestContext): Promise { diff --git a/src/plugin/reams/index.ts b/src/plugin/reams/index.ts index 2522cc3a..649294c3 100644 --- a/src/plugin/reams/index.ts +++ b/src/plugin/reams/index.ts @@ -89,6 +89,29 @@ export type { FormattingAction } from '../ui/formatting-action-types' */ export type { ConstructSpec } from '../../format-preservation/types' +// Editor utilities used by built-in plugins. Keeping these exports here avoids +// plugins reaching into editor implementation directories. +export { + INLINE_SEMANTIC_CHANGE_ANNOTATION, + INLINE_SEMANTIC_TEXT_CHANGE_ANNOTATION, + inlineSemanticTextChangeFromChangeSpec, +} from '../../editor/inline-semantic-ops' +export type { InlineSemanticSpecName } from '../../editor/inline-semantic-ops' +export { + getFrontmatterRange, + isCursorNearSameLine, + isOffsetRangeInTableCell, + shouldSkipMarkdownEnhancement, +} from '../../editor/extensions/markdown-context' +export { INLINE_FORMATTING_HIDDEN } from '../../editor/extensions/hidden-text-classes' +export { formatDisplayValue } from '../../editor/extensions/frontmatter-types' +export { createBuiltinFormattingActions, applyInsertFootnote } from '../../editor/builtin-formatting-actions' +export { canToggleCurrentListFold, toggleCurrentListFold } from '../../editor/extensions/list-bullet-widget' +export { parseFrontmatter } from '../../services/metadata/extract-metadata' +export { slugifyHeadings, findHeadingBySlug } from '../../services/metadata' +export type { MetadataCache } from '../../services/metadata' +export { FileTree } from '../../layout/file-tree' + // ----------------------------------------------------------------------------- // Errors: Reams-specific error classes // ----------------------------------------------------------------------------- diff --git a/src/plugins/__tests__/plugin-layer-isolation.test.ts b/src/plugins/__tests__/plugin-layer-isolation.test.ts index b4befcdf..c113e26a 100644 --- a/src/plugins/__tests__/plugin-layer-isolation.test.ts +++ b/src/plugins/__tests__/plugin-layer-isolation.test.ts @@ -1,82 +1,97 @@ /** * Plugin Layer Isolation Lint Rule Test * - * Verifies that the ESLint no-restricted-imports rule correctly: + * Verifies that the Oxlint no-restricted-imports rule correctly: * 1. BLOCKS imports from src/services/, src/editor/, src/layout/ in src/plugins/ * 2. ALLOWS imports from 'obsidian' (the public API) * 3. DOES NOT apply to __tests__/ files (test setup needs internal access) * - * This test runs ESLint programmatically on test fixtures to verify the rule works. + * This test runs Oxlint on test fixtures to verify the production rule works. * * @see chainlink issue #392 * @see docs/audits/core-services-audit.md — reach-behind findings */ import { describe, it, expect } from 'vitest' -import { ESLint } from 'eslint' +import { execFile } from 'node:child_process' import path from 'path' +import { promisify } from 'node:util' // Fixtures are NOT in __tests__ so the no-restricted-imports rule applies to them const FIXTURES_DIR = path.join(__dirname, '../__lint-test-fixtures__') +const PROJECT_ROOT = path.resolve(__dirname, '../../..') +const oxlint = path.resolve(__dirname, '../../../node_modules/.bin/oxlint') +const execFileAsync = promisify(execFile) +type OxlintOutput = { diagnostics: Array<{ code?: string; message: string }> } + +function parseResult(stdout: string): { hasRestrictedImportError: boolean; output: string } { + const { diagnostics } = JSON.parse(stdout) as OxlintOutput + const output = diagnostics.map(diagnostic => `${diagnostic.code ?? ''} ${diagnostic.message}`).join('\n') + return { + hasRestrictedImportError: diagnostics.some( + diagnostic => diagnostic.code === 'eslint(no-restricted-imports)', + ), + output, + } +} /** - * Run ESLint on a single file and return { passed, output }. - * Uses the production eslint.config.js. + * Run Oxlint on a single file and return { passed, output }. + * Uses the production .oxlintrc.json. * - * ESLint is configured with `ignore: false` so fixture files are linted even - * though the production command ignores them. This ensures we test the actual - * production rule, not a simplified version. + * `--no-ignore` includes deliberately-invalid fixture files, so this checks the + * production rule rather than a simplified test-only configuration. */ -const eslint = new ESLint({ - cwd: path.resolve(__dirname, '../../..'), - overrideConfigFile: 'eslint.config.js', - ignore: false, -}) - -async function runEslintOnFile( +async function runOxlintOnFile( filePath: string, -): Promise<{ passed: boolean; output: string }> { - const [result] = await eslint.lintFiles([filePath]) - const output = result.messages - .map(message => `${message.ruleId ?? ''} ${message.message}`) - .join('\n') - return { passed: result.errorCount === 0, output } +): Promise<{ hasRestrictedImportError: boolean; output: string }> { + try { + const { stdout } = await execFileAsync(oxlint, [ + '--no-ignore', '--format=json', path.relative(PROJECT_ROOT, filePath), + ], { + cwd: PROJECT_ROOT, + }) + return parseResult(stdout) + } catch (error) { + const stdout = (error as { stdout?: string }).stdout ?? '[]' + return parseResult(stdout) + } } describe('Plugin layer isolation lint rule', () => { it('flags imports from src/services/ in plugin code', async () => { const filePath = path.join(FIXTURES_DIR, 'forbidden-services-import.ts') - const result = await runEslintOnFile(filePath) + const result = await runOxlintOnFile(filePath) - expect(result.passed).toBe(false) + expect(result.hasRestrictedImportError).toBe(true) expect(result.output).toContain('no-restricted-imports') expect(result.output).toContain('services') }) it('flags imports from src/editor/ in plugin code', async () => { const filePath = path.join(FIXTURES_DIR, 'forbidden-editor-import.ts') - const result = await runEslintOnFile(filePath) + const result = await runOxlintOnFile(filePath) - expect(result.passed).toBe(false) + expect(result.hasRestrictedImportError).toBe(true) expect(result.output).toContain('no-restricted-imports') expect(result.output).toContain('editor') }) it('flags imports from src/layout/ in plugin code', async () => { const filePath = path.join(FIXTURES_DIR, 'forbidden-layout-import.ts') - const result = await runEslintOnFile(filePath) + const result = await runOxlintOnFile(filePath) - expect(result.passed).toBe(false) + expect(result.hasRestrictedImportError).toBe(true) expect(result.output).toContain('no-restricted-imports') expect(result.output).toContain('layout') }) it('allows imports from obsidian in plugin code', async () => { const filePath = path.join(FIXTURES_DIR, 'allowed-obsidian-import.ts') - const result = await runEslintOnFile(filePath) + const result = await runOxlintOnFile(filePath) - // This file should pass lint (no violations from our rule) - expect(result.passed).toBe(true) + // This file should not violate the layer-isolation rule. + expect(result.hasRestrictedImportError).toBe(false) }) it('does NOT flag forbidden imports in __tests__/ files (test setup exception)', async () => { @@ -87,9 +102,9 @@ describe('Plugin layer isolation lint rule', () => { __dirname, '../wikilink/__tests__/wikilink-plugin.test.ts' ) - const result = await runEslintOnFile(testFilePath) + const result = await runOxlintOnFile(testFilePath) // Test files should be allowed to import from services - expect(result.passed).toBe(true) + expect(result.hasRestrictedImportError).toBe(false) }) }) diff --git a/src/plugins/builtin-formatting/builtin-formatting-plugin.ts b/src/plugins/builtin-formatting/builtin-formatting-plugin.ts index b9c50dab..f0ae9cbd 100644 --- a/src/plugins/builtin-formatting/builtin-formatting-plugin.ts +++ b/src/plugins/builtin-formatting/builtin-formatting-plugin.ts @@ -2,10 +2,7 @@ import { Plugin } from '../../plugin/plugin' import type { App } from '../../plugin/app' import type { PluginManifest } from '../../plugin/types' -// FIXME(F-COMMANDS-4, F-COMMANDS-5): formatting actions reach behind to raw CM6 EditorView and editor/extensions. -// Blocked on: namespace exposing editor transaction utilities or keeping formatting as privileged host code. -// eslint-disable-next-line no-restricted-imports -import { createBuiltinFormattingActions } from '../../editor/builtin-formatting-actions' +import { createBuiltinFormattingActions } from 'reams' const BUILTIN_FORMATTING_MANIFEST: PluginManifest = { id: 'reams-builtin-formatting', diff --git a/src/plugins/daily-notes/daily-notes-plugin.ts b/src/plugins/daily-notes/daily-notes-plugin.ts index 7167e7d6..cd0af7cc 100644 --- a/src/plugins/daily-notes/daily-notes-plugin.ts +++ b/src/plugins/daily-notes/daily-notes-plugin.ts @@ -226,9 +226,8 @@ export class DailyNotesPlugin extends Plugin { } catch (err) { console.error('Failed to open daily note:', err) } - // Suppress unused-variable warning: `file` is used implicitly - // (its existence is the precondition for the openLinkText call). - void file + // Creating or resolving the file is the precondition for navigation. + if (!file) return } finally { this._openingDailyNote = false } diff --git a/src/plugins/document-info/frontmatter-panel.tsx b/src/plugins/document-info/frontmatter-panel.tsx index 256a42a6..a46afcb2 100644 --- a/src/plugins/document-info/frontmatter-panel.tsx +++ b/src/plugins/document-info/frontmatter-panel.tsx @@ -3,9 +3,7 @@ import { useMemo } from 'react' import { useReamsActiveFile, useMetadataCacheVersion } from '../../plugin/hooks' import type { DocumentInfoPlugin } from './document-info-plugin' import type { AutomergeUrl } from '@automerge/automerge-repo' -// FIXME(F-VIEWS-6): formatDisplayValue is editor-internal; should be Reams namespace or @internal utility. -// eslint-disable-next-line no-restricted-imports -import { formatDisplayValue } from '../../editor/extensions/frontmatter-types' +import { formatDisplayValue } from 'reams' import './document-info-panels.css' interface FrontmatterPanelProps { diff --git a/src/plugins/document-info/toc-panel.tsx b/src/plugins/document-info/toc-panel.tsx index 213ebce5..26dd53cc 100644 --- a/src/plugins/document-info/toc-panel.tsx +++ b/src/plugins/document-info/toc-panel.tsx @@ -3,9 +3,7 @@ import { useCallback, useMemo } from 'react' import { useReamsActiveFile, useMetadataCacheVersion } from '../../plugin/hooks' import type { DocumentInfoPlugin } from './document-info-plugin' import type { AutomergeUrl } from '@automerge/automerge-repo' -// FIXME(F-AUTOCOMPLETE-4, F-VAULT-10, F-VIEWS-7): slugifyHeadings should be Reams namespace or HeadingCache.slug. -// eslint-disable-next-line no-restricted-imports -import { slugifyHeadings, findHeadingBySlug } from '../../services/metadata' +import { slugifyHeadings, findHeadingBySlug } from 'reams' import { useStickyHeadingSlug } from './toc-sticky-highlight' import './document-info-panels.css' @@ -29,8 +27,8 @@ export function TocPanel({ plugin }: TocPanelProps) { // directly, but the version bump is the SIGNAL that headings may // have changed. const headings = useMemo(() => { - if (!docUrl) return [] - void version + const cacheKey = `${docUrl ?? ''}:${version}` + if (!cacheKey || !docUrl) return [] const cache = metadataCache.getFileCacheByUrl(docUrl as AutomergeUrl) return cache?.headings ?? [] }, [docUrl, metadataCache, version]) @@ -103,4 +101,3 @@ export function TocPanel({ plugin }: TocPanelProps) { ) } - diff --git a/src/plugins/document-info/toc-sticky-highlight.ts b/src/plugins/document-info/toc-sticky-highlight.ts index 5f0586cd..84180043 100644 --- a/src/plugins/document-info/toc-sticky-highlight.ts +++ b/src/plugins/document-info/toc-sticky-highlight.ts @@ -1,9 +1,7 @@ // src/plugins/document-info/toc-sticky-highlight.ts import { useEffect, useState } from 'react' import type { App } from '../../plugin/app' -// FIXME(F-AUTOCOMPLETE-4, F-VAULT-10, F-VIEWS-7): MetadataCache type and slugifyHeadings should be Reams namespace. -// eslint-disable-next-line no-restricted-imports -import { slugifyHeadings, type MetadataCache } from '../../services/metadata' +import { slugifyHeadings, type MetadataCache } from 'reams' import type { AutomergeUrl } from '@automerge/automerge-repo' /** diff --git a/src/plugins/file-tree/file-tree-view.tsx b/src/plugins/file-tree/file-tree-view.tsx index 87f1c317..e2da84fc 100644 --- a/src/plugins/file-tree/file-tree-view.tsx +++ b/src/plugins/file-tree/file-tree-view.tsx @@ -6,9 +6,7 @@ import type { AutomergeUrl } from '@automerge/automerge-repo' import { ItemView, type ReactItemView } from '../../plugin/views/item-view' import type { WorkspaceLeaf } from '../../plugin/workspace' import type { App } from '../../plugin/app' -// FIXME(F-VIEWS-11, #352): cross-layer import; blocked on FileTree's useLayout() dependency for mobile sidebar auto-close. -// eslint-disable-next-line no-restricted-imports -import { FileTree } from '../../layout/file-tree' +import { FileTree } from 'reams' import type { FileTreePluginOptions } from './file-tree-plugin' import { onRenderProfiler } from '../../lib/perf-profile' import { SortMenuButton } from '../../plugin/sidebar/SortMenuButton' diff --git a/src/plugins/highlight/highlight-editor-extension.ts b/src/plugins/highlight/highlight-editor-extension.ts index 2ad1e406..4e43dd23 100644 --- a/src/plugins/highlight/highlight-editor-extension.ts +++ b/src/plugins/highlight/highlight-editor-extension.ts @@ -10,15 +10,13 @@ import { INLINE_SEMANTIC_TEXT_CHANGE_ANNOTATION, inlineSemanticTextChangeFromChangeSpec, type InlineSemanticSpecName, -} from '../../editor/inline-semantic-ops' -import { REMOTE_CHANGE_ANNOTATION } from '../../sync/reactive-doc-sync' -import { getFrontmatterRange, isCursorNearSameLine, isOffsetRangeInTableCell, shouldSkipMarkdownEnhancement, -} from '../../editor/extensions/markdown-context' -import { INLINE_FORMATTING_HIDDEN } from '../../editor/extensions/hidden-text-classes' + INLINE_FORMATTING_HIDDEN, +} from 'reams' +import { REMOTE_CHANGE_ANNOTATION } from '../../sync/reactive-doc-sync' const DELIMITER = '==' const PROXIMITY_CHARS = 2 @@ -386,27 +384,16 @@ export const highlightDecorationPlugin = ViewPlugin.fromClass( { decorations: (plugin) => plugin.decorations }, ) -function createHighlightAutoPairFilter(tr: Transaction): Transaction | TransactionSpec { - if (!tr.docChanged) return tr - if (tr.annotation(REMOTE_CHANGE_ANNOTATION) === true) return tr - if (tr.annotation(INLINE_SEMANTIC_CHANGE_ANNOTATION) !== undefined) return tr - +function shouldSkipHighlightAutoPair(tr: Transaction): boolean { + if (!tr.docChanged || tr.annotation(REMOTE_CHANGE_ANNOTATION) === true) return true + if (tr.annotation(INLINE_SEMANTIC_CHANGE_ANNOTATION) !== undefined) return true const userEvent = tr.annotation(Transaction.userEvent) - if (userEvent) { - if (userEvent.startsWith('input.type.compose')) return tr - if (userEvent.startsWith('undo') || userEvent.startsWith('redo')) return tr - } - - if (!tr.startState.field(editorLivePreviewField)) return tr - - const pairDeletion = createPairDeletionSpec(tr) - if (pairDeletion) return pairDeletion - - if (userEvent === 'input.paste') return tr - - const selection = tr.selection?.main - if (!selection || selection.from !== selection.to) return tr + return userEvent?.startsWith('input.type.compose') === true + || userEvent?.startsWith('undo') === true + || userEvent?.startsWith('redo') === true +} +function getSinglePureInsertion(tr: Transaction): { insertPos: number; insertedText: string } | null { let changeCount = 0 let insertPos = 0 let insertedText = '' @@ -417,42 +404,60 @@ function createHighlightAutoPairFilter(tr: Transaction): Transaction | Transacti insertedText = inserted.toString() if (fromA !== toA) isPureInsertion = false }) - if (changeCount !== 1 || !isPureInsertion) return tr + return changeCount === 1 && isPureInsertion ? { insertPos, insertedText } : null +} +function createActiveConstructSpec( + tr: Transaction, + insertPos: number, + insertedText: string, + userEvent: string | undefined, +): TransactionSpec | null { const activeConstructs = tr.startState.field(unmarkedHighlightConstructsField, false) ?? [] for (const construct of activeConstructs) { - if (insertPos > construct.range.start && insertPos < construct.range.end) { - const contentStart = construct.range.start + DELIMITER.length - const contentEnd = construct.range.end + insertedText.length - DELIMITER.length - const inContent = insertPos >= contentStart && insertPos <= construct.range.end - DELIMITER.length - if (!inContent) { - return createLiteralEditClearingSpec(tr, construct, userEvent ?? undefined) - } - if (contentStart < contentEnd && insertedText.trim().length > 0) { - return createMarkEmissionSpec(tr, construct, contentStart, contentEnd, userEvent ?? undefined) - } - return createRangeExtendedSpec(tr, construct, insertedText.length, userEvent ?? undefined) + if (insertPos <= construct.range.start || insertPos >= construct.range.end) continue + const contentStart = construct.range.start + DELIMITER.length + const contentEnd = construct.range.end + insertedText.length - DELIMITER.length + const inContent = insertPos >= contentStart && insertPos <= construct.range.end - DELIMITER.length + if (!inContent) return createLiteralEditClearingSpec(tr, construct, userEvent) + if (contentStart < contentEnd && insertedText.trim().length > 0) { + return createMarkEmissionSpec(tr, construct, contentStart, contentEnd, userEvent) } + return createRangeExtendedSpec(tr, construct, insertedText.length, userEvent) } + return null +} - if (insertedText !== '=') return tr - - if (insertPos === 0) return tr - if (tr.startState.doc.sliceString(insertPos - 1, insertPos) !== '=') return tr - if (isClosingExistingHighlight(tr.startState, insertPos)) return tr - if (insertPos >= 2 && tr.startState.doc.sliceString(insertPos - 2, insertPos) === DELIMITER) return tr - if (isInsideInlineConstruct(tr.startState, insertPos)) return tr - +function canCreateHighlightPair(tr: Transaction, insertPos: number): boolean { + if (insertPos === 0 || tr.startState.doc.sliceString(insertPos - 1, insertPos) !== '=') return false + if (isClosingExistingHighlight(tr.startState, insertPos)) return false + if (insertPos >= 2 && tr.startState.doc.sliceString(insertPos - 2, insertPos) === DELIMITER) return false + if (isInsideInlineConstruct(tr.startState, insertPos)) return false const frontmatter = getFrontmatterRange(tr.startState) - if (shouldSkipMarkdownEnhancement(tr.startState, insertPos - 1, insertPos, frontmatter)) return tr - if (isOffsetRangeInTableCell(tr.startState, insertPos - 1, insertPos)) return tr + return !shouldSkipMarkdownEnhancement(tr.startState, insertPos - 1, insertPos, frontmatter) + && !isOffsetRangeInTableCell(tr.startState, insertPos - 1, insertPos) +} + +function createHighlightAutoPairFilter(tr: Transaction): Transaction | TransactionSpec { + if (shouldSkipHighlightAutoPair(tr) || !tr.startState.field(editorLivePreviewField)) return tr + const pairDeletion = createPairDeletionSpec(tr) + if (pairDeletion) return pairDeletion + const userEvent = tr.annotation(Transaction.userEvent) + if (userEvent === 'input.paste') return tr + const selection = tr.selection?.main + if (!selection || selection.from !== selection.to) return tr + const insertion = getSinglePureInsertion(tr) + if (!insertion) return tr + const activeConstructSpec = createActiveConstructSpec(tr, insertion.insertPos, insertion.insertedText, userEvent ?? undefined) + if (activeConstructSpec) return activeConstructSpec + if (insertion.insertedText !== '=' || !canCreateHighlightPair(tr, insertion.insertPos)) return tr return createAutoPairTransactionSpec( - insertPos - 1, - insertPos, + insertion.insertPos - 1, + insertion.insertPos, DELIMITER.repeat(2), - insertPos + 1, - { range: { start: insertPos - 1, end: insertPos - 1 + DELIMITER.repeat(2).length } }, + insertion.insertPos + 1, + { range: { start: insertion.insertPos - 1, end: insertion.insertPos - 1 + DELIMITER.repeat(2).length } }, userEvent ?? undefined, ) } diff --git a/src/plugins/highlight/highlight-spec.ts b/src/plugins/highlight/highlight-spec.ts index e2bcf49c..e02fb901 100644 --- a/src/plugins/highlight/highlight-spec.ts +++ b/src/plugins/highlight/highlight-spec.ts @@ -1,7 +1,7 @@ // src/plugins/highlight/highlight-spec.ts import type { SyntaxNode, Tree } from '@lezer/common' import type { ConstructSpec, Range, ReconcileEdit, SynthesisClaim } from '../../format-preservation' -import { parseFrontmatter } from '../../services/metadata/extract-metadata' +import { parseFrontmatter } from 'reams' const DELIMITER = '==' diff --git a/src/plugins/layout-commands/layout-commands-plugin.ts b/src/plugins/layout-commands/layout-commands-plugin.ts index 327474d6..26c5f8c9 100644 --- a/src/plugins/layout-commands/layout-commands-plugin.ts +++ b/src/plugins/layout-commands/layout-commands-plugin.ts @@ -3,12 +3,7 @@ import { Plugin } from '../../plugin/plugin' import type { App } from '../../plugin/app' import type { PluginManifest } from '../../plugin/types' import { MarkdownView } from '../../plugin/views/markdown-view' -// FIXME(F-COMMANDS-5): applyInsertFootnote reaches behind to editor/extensions; should be Reams namespace or @internal. -// eslint-disable-next-line no-restricted-imports -import { applyInsertFootnote } from '../../editor/builtin-formatting-actions' -// FIXME(F-COMMANDS-6): list folding reaches behind to editor/extensions; should be exposed through the Reams namespace. -// eslint-disable-next-line no-restricted-imports -import { canToggleCurrentListFold, toggleCurrentListFold } from '../../editor/extensions/list-bullet-widget' +import { applyInsertFootnote, canToggleCurrentListFold, toggleCurrentListFold } from 'reams' const LAYOUT_COMMANDS_MANIFEST: PluginManifest = { id: 'reams-layout-commands', diff --git a/src/plugins/quick-access/preview.ts b/src/plugins/quick-access/preview.ts index a000d606..d87d2443 100644 --- a/src/plugins/quick-access/preview.ts +++ b/src/plugins/quick-access/preview.ts @@ -15,6 +15,20 @@ const PREVIEW_CAP = 160 +function replaceDelimited(text: string, open: string, close: string, replacement: (value: string) => string): string { + let result = '' + let cursor = 0 + while (cursor < text.length) { + const start = text.indexOf(open, cursor) + if (start < 0) return result + text.slice(cursor) + const end = text.indexOf(close, start + open.length) + if (end < 0) return result + text.slice(cursor) + result += text.slice(cursor, start) + replacement(text.slice(start + open.length, end)) + cursor = end + close.length + } + return result +} + export function extractPreview(content: string): string { // 0. Strip a leading UTF-8 BOM (\uFEFF) if present. Some editors/exporters // prepend one, which would otherwise prevent the `^---` frontmatter @@ -24,13 +38,20 @@ export function extractPreview(content: string): string { // 1. Strip code fences FIRST. This must happen before frontmatter so a // code block containing a `---` line is not mis-recognized as the // frontmatter terminator. - body = body.replace(/```[\s\S]*?```/g, ' ') + body = body.replace(/```(?:[^`]|`{1,2}(?!`))*```/g, ' ') // 2. Strip leading frontmatter block (--- ... --- at file start only). - body = body.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '') + if (body.startsWith('---\n') || body.startsWith('---\r\n')) { + const firstLineEnd = body.indexOf('\n') + 1 + const closing = body.indexOf('\n---', firstLineEnd) + if (closing >= 0) { + const closingLineEnd = body.indexOf('\n', closing + 1) + body = body.slice(closingLineEnd >= 0 ? closingLineEnd + 1 : body.length) + } + } // 3. Strip HTML tags (open and close). - body = body.replace(/<[^>]+>/g, ' ') + body = replaceDelimited(body, '<', '>', () => ' ') // 4. Strip embedded wikilinks (![[X]]) — drop entirely (they show as broken // in a one-line preview). @@ -40,10 +61,16 @@ export function extractPreview(content: string): string { body = body.replace(/!\[[^\]]*\]\([^)]*\)/g, ' ') // 6. Wikilinks: [[target|display]] → display; [[target]] → target. - body = body.replace(/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g, (_, target, display) => (display ?? target).trim()) + body = replaceDelimited(body, '[[', ']]', (target) => { + const segments = target.split('|') + return (segments[segments.length - 1] ?? '').trim() + }) // 7. Markdown links [text](url) → text. - body = body.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1') + body = replaceDelimited(body, '[', ')', (link) => { + const separator = link.indexOf('](') + return separator >= 0 ? link.slice(0, separator) : `[${link})` + }) // 8. Strip leading ATX heading markers (# ## ### etc.) at line start. body = body.replace(/^[ \t]{0,3}#{1,6}[ \t]+/gm, '') @@ -58,8 +85,8 @@ export function extractPreview(content: string): string { // Conservative: only replace matched pairs. Documented edge per plan D4: // paired single underscores in identifiers like snake_case_word are // stripped — acceptable for a skim-preview. - body = body.replace(/(\*\*|__)(.+?)\1/g, '$2') - body = body.replace(/(\*|_)(.+?)\1/g, '$2') + body = body.replace(/(\*\*|__)([^\r\n]+?)\1/g, '$2') + body = body.replace(/([*_])([^\r\n]+?)\1/g, '$2') body = body.replace(/~~(.+?)~~/g, '$1') body = body.replace(/`([^`]+)`/g, '$1') diff --git a/src/plugins/wikilink/BacklinksPanel.tsx b/src/plugins/wikilink/BacklinksPanel.tsx index fc4d1330..6ec32269 100644 --- a/src/plugins/wikilink/BacklinksPanel.tsx +++ b/src/plugins/wikilink/BacklinksPanel.tsx @@ -1,9 +1,5 @@ import type { AutomergeUrl } from '@automerge/automerge-repo' -// FIXME(F-BUILTIN-API-1): BacklinkResult is Reams-specific (exported from -// 'reams' namespace). Built-ins are currently privileged and may import -// directly from services/metadata. -// eslint-disable-next-line no-restricted-imports -import type { BacklinkResult } from '../../services/metadata' +import type { BacklinkResult } from 'reams' import './BacklinksPanel.css' export interface EnrichedBacklink extends BacklinkResult { diff --git a/src/plugins/wikilink/backlinks-panel-inner.tsx b/src/plugins/wikilink/backlinks-panel-inner.tsx index 934d395f..94728e5f 100644 --- a/src/plugins/wikilink/backlinks-panel-inner.tsx +++ b/src/plugins/wikilink/backlinks-panel-inner.tsx @@ -21,9 +21,12 @@ export function BacklinksPanelInner({ plugin }: BacklinksPanelInnerProps) { // Resolve the current document's actual filename via the active file const docName = activeFile?.name ?? null - const backlinkTarget = activeFile - ? activeFile.path.replace(/\.md$/, '') - : (docName ? docName.replace(/\.md$/, '') : null) + let backlinkTarget: string | null = null + if (activeFile) { + backlinkTarget = activeFile.path.replace(/\.md$/, '') + } else if (docName) { + backlinkTarget = docName.replace(/\.md$/, '') + } const backlinks = useMemo( () => (backlinkTarget ? plugin.getBacklinks(backlinkTarget) : []), diff --git a/src/plugins/wikilink/wikilink-plugin.ts b/src/plugins/wikilink/wikilink-plugin.ts index 3fcb8743..8d0c3a6b 100644 --- a/src/plugins/wikilink/wikilink-plugin.ts +++ b/src/plugins/wikilink/wikilink-plugin.ts @@ -1,20 +1,14 @@ import { Plugin } from '../../plugin/plugin' import type { App } from '../../plugin/app' import type { PluginManifest } from '../../plugin/types' -// FIXME(F-BUILTIN-API-1): BacklinkResult is Reams-specific (exported from -// 'reams' namespace). Built-ins are currently privileged and may import -// directly from services/metadata. -// eslint-disable-next-line no-restricted-imports -import type { BacklinkResult } from '../../services/metadata' +import type { BacklinkResult } from 'reams' import { BacklinksView, BACKLINKS_VIEW_TYPE } from './backlinks-view' import type { TFile } from '../../plugin/files' import type { AutomergeUrl } from '@automerge/automerge-repo' import { WikilinkSuggester } from '../../plugin/builtin-suggesters/wikilink-suggester' import { ImageSuggester } from '../../plugin/builtin-suggesters/image-suggester' import { TagSuggester } from '../../plugin/builtin-suggesters/tag-suggester' -// FIXME(F-AUTOCOMPLETE-4, F-VAULT-10): slugifyHeadings should be Reams namespace or HeadingCache.slug. -// eslint-disable-next-line no-restricted-imports -import { slugifyHeadings } from '../../services/metadata' +import { slugifyHeadings } from 'reams' import { stripWikilinkFragment as _stripWikilinkFragment } from '../../utils/wikilink-fragment' /** @@ -344,53 +338,8 @@ export class WikilinkPlugin extends Plugin { const file = this.app.vault.getFileByUrl(r.url) if (!file) continue - const displayTitle = this.app.metadataCache.getDisplayTitle(file) - const effectiveTitle = displayTitle && displayTitle !== file.basename ? displayTitle : null - - // Don't show a snippet when the match is in the title/path — the - // snippet would just repeat the filename. Use MiniSearch's `match` - // field (v6+) which maps each matched term to the fields it was - // found in. This correctly handles fuzzy matches: query `wikilnk` - // (typo) matches doc `wikilink.md` via fuzzy, and `match` reports - // `{"wikilink": ["name"]}`, so we know the match was in the name - // field even though `wikilnk` doesn't literally appear in the name. - // - // Fall back to literal substring check when `match` is empty - // (shouldn't happen in MiniSearch v6+, but defensive). - const matchedNameOrPath = Object.values(r.match).some( - (fields) => fields.includes('name') || fields.includes('path'), - ) - // Also check displayTitle via literal match (not indexed by MiniSearch, - // only in our MetadataCache). - const matchedDisplayTitle = effectiveTitle?.toLowerCase().includes(queryLower) ?? false - const matchedTitleOrPath = matchedNameOrPath || matchedDisplayTitle - // Fallback: if match info is empty, use the old literal check - || (Object.keys(r.match).length === 0 && ( - file.basename.toLowerCase().includes(queryLower) - || file.path.toLowerCase().includes(queryLower) - )) - - // Suppress results where the ONLY match is inside an unclosed `[[…` - // trigger in the document body. This happens when the user types - // `[[query` in doc A, never completes (Escape / focus loss), and the - // autosaved body containing that partial trigger gets indexed. Without - // this filter, every subsequent `[[query` in OTHER docs would surface - // doc A as a top hit because its body literally contains `query`. The - // self-match exclusion above only protects the *active* doc; this - // catches stale orphan triggers in *other* docs. - // - // Skip the check if the match was in title/path — those are legitimate - // hits (the user is searching for a file by name, not by body content). - if (!matchedTitleOrPath) { - const content = search.getDocumentContent(r.url) - if (content) { - // Find where the query appears in the content (first occurrence) - const matchOffset = content.toLowerCase().indexOf(queryLower) - if (matchOffset >= 0 && isMatchInsideOpenBracketBracket(content, matchOffset)) { - continue // Skip this result — match is inside unclosed `[[` - } - } - } + const { effectiveTitle, matchedTitleOrPath } = this.getIndexedMatchDetails(r, file, queryLower) + if (this.isOrphanWikilinkBodyMatch(search, r.url, queryLower, matchedTitleOrPath)) continue out.push({ kind: 'file', @@ -419,6 +368,35 @@ export class WikilinkPlugin extends Plugin { return out.map(({ score: _score, ...suggestion }) => suggestion) } + private getIndexedMatchDetails( + result: { match: Record }, + file: TFile, + queryLower: string, + ): { effectiveTitle: string | null; matchedTitleOrPath: boolean } { + const displayTitle = this.app.metadataCache.getDisplayTitle(file) + const effectiveTitle = displayTitle && displayTitle !== file.basename ? displayTitle : null + const matchedNameOrPath = Object.values(result.match).some( + (fields) => fields.includes('name') || fields.includes('path'), + ) + const matchedDisplayTitle = effectiveTitle?.toLowerCase().includes(queryLower) ?? false + const fallbackMatch = Object.keys(result.match).length === 0 + && (file.basename.toLowerCase().includes(queryLower) || file.path.toLowerCase().includes(queryLower)) + return { effectiveTitle, matchedTitleOrPath: matchedNameOrPath || matchedDisplayTitle || fallbackMatch } + } + + private isOrphanWikilinkBodyMatch( + search: App['reamsSearch'], + url: AutomergeUrl, + queryLower: string, + matchedTitleOrPath: boolean, + ): boolean { + if (matchedTitleOrPath) return false + const content = search.getDocumentContent(url) + if (!content) return false + const matchOffset = content.toLowerCase().indexOf(queryLower) + return matchOffset >= 0 && isMatchInsideOpenBracketBracket(content, matchOffset) + } + /** * Linear path scan — fallback when the SearchService is unavailable * (unit tests) or when the query is empty (MiniSearch can't search the @@ -440,35 +418,16 @@ export class WikilinkPlugin extends Plugin { // Avoid the O(N) full-vault iteration + displayTitle lookup per file // that the general scoring path requires. For a 50k-file vault this // saves 50k object allocations and metadataCache lookups per keystroke. - if (queryLower === '') { - // Try the SearchService's recent-files list first (sorted by mtime). - // If the index is empty (nothing indexed yet), fall through to scan. - if (this.app.hasSearchService()) { - const recent = this.app.reamsSearch.getRecentFiles(11) // +1 in case excludeUrl is in list - if (recent.length > 0) { - const out: WikilinkFileSuggestion[] = [] - for (const r of recent) { - if (out.length >= 10) break - if (excludeUrl !== null && r.url === excludeUrl) continue - const file = this.app.vault.getFileByUrl(r.url) - if (!file || file.extension !== 'md') continue - const displayTitle = this.app.metadataCache.getDisplayTitle(file) - const effectiveTitle = displayTitle && displayTitle !== file.basename ? displayTitle : null - out.push({ - kind: 'file', - name: file.name, - target: getInsertedTarget(file), - path: file.path, - displayTitle: effectiveTitle, - contextLabel: getContextLabel(file), - snippet: null, - }) - } - return out - } - // Fall through: index has no recent files (not indexed yet), - // use path scan below. - } + if (queryLower === '') return this.getEmptyQuerySuggestions(excludeUrl) + + return this.getNonEmptyQuerySuggestions(queryLower, excludeUrl) + } + + private getEmptyQuerySuggestions(excludeUrl: AutomergeUrl | null): WikilinkFileSuggestion[] { + const recent = this.getRecentFileSuggestions(excludeUrl) + if (recent.length > 0) { + return recent + } // No SearchService (unit tests) OR index empty — slice FIRST to avoid // the O(N) displayTitle lookup. Use alphabetical order. const files = this.app.vault.getMarkdownFiles() @@ -491,8 +450,27 @@ export class WikilinkPlugin extends Plugin { snippet: null, } }) + } + + private getRecentFileSuggestions(excludeUrl: AutomergeUrl | null): WikilinkFileSuggestion[] { + if (!this.app.hasSearchService()) return [] + const out: WikilinkFileSuggestion[] = [] + for (const result of this.app.reamsSearch.getRecentFiles(11)) { + if (out.length >= 10) break + if (excludeUrl !== null && result.url === excludeUrl) continue + const file = this.app.vault.getFileByUrl(result.url) + if (!file || file.extension !== 'md') continue + const displayTitle = this.app.metadataCache.getDisplayTitle(file) + out.push({ + kind: 'file', name: file.name, target: getInsertedTarget(file), path: file.path, + displayTitle: displayTitle && displayTitle !== file.basename ? displayTitle : null, + contextLabel: getContextLabel(file), snippet: null, + }) } + return out + } + private getNonEmptyQuerySuggestions(queryLower: string, excludeUrl: AutomergeUrl | null): WikilinkFileSuggestion[] { // NON-EMPTY QUERY: full scoring path (acceptable for filtered subset). const files = this.app.vault.getMarkdownFiles() .filter((file) => { @@ -508,56 +486,49 @@ export class WikilinkPlugin extends Plugin { const pathQualifiedTarget = getPathQualifiedTarget(file).toLowerCase() return { file, effectiveTitle, pathQualifiedTarget } }) - .filter(({ file, effectiveTitle, pathQualifiedTarget }) => { - const nameMatch = file.basename.toLowerCase().includes(queryLower) - if (nameMatch) return true - const pathMatch = file.path.toLowerCase().includes(queryLower) || pathQualifiedTarget.includes(queryLower) - if (pathMatch) return true - if (effectiveTitle) { - return effectiveTitle.toLowerCase().includes(queryLower) - } - return false - }) - .map(({ file, effectiveTitle, pathQualifiedTarget }) => { - const basenameMatch = file.basename.toLowerCase().includes(queryLower) - const displayTitleMatch = effectiveTitle?.toLowerCase().includes(queryLower) ?? false - const pathPrefixMatch = queryLower.includes('/') && pathQualifiedTarget.startsWith(queryLower) - const pathMatch = file.path.toLowerCase().includes(queryLower) || pathQualifiedTarget.includes(queryLower) - - let score = 0 - if (pathPrefixMatch) score += 40 - if (pathMatch) score += 20 - if (basenameMatch) score += 10 - if (displayTitleMatch) score += 5 - - return { - kind: 'file' as const, - name: file.name, - target: getInsertedTarget(file), - path: file.path, - displayTitle: effectiveTitle, - contextLabel: getContextLabel(file), - snippet: null as string | null, - score, - } - }) - .sort((a, b) => { - const bySlashQualifiedTarget = compareSlashQualifiedTargets( - a.target.toLowerCase(), - b.target.toLowerCase(), - queryLower, - ) - if (bySlashQualifiedTarget !== 0) return bySlashQualifiedTarget - - const byScore = b.score - a.score - if (byScore !== 0) return byScore - - return a.path.localeCompare(b.path) - }) + .filter((candidate) => this.matchesWikilinkQuery(candidate, queryLower)) + .map((candidate) => this.createScoredWikilinkSuggestion(candidate, queryLower)) + .sort((a, b) => this.compareWikilinkSuggestions(a, b, queryLower)) .map(({ score: _score, ...suggestion }) => suggestion) .slice(0, 10) } + private matchesWikilinkQuery( + candidate: { file: TFile; effectiveTitle: string | null; pathQualifiedTarget: string }, + queryLower: string, + ): boolean { + return candidate.file.basename.toLowerCase().includes(queryLower) + || candidate.file.path.toLowerCase().includes(queryLower) + || candidate.pathQualifiedTarget.includes(queryLower) + || candidate.effectiveTitle?.toLowerCase().includes(queryLower) === true + } + + private createScoredWikilinkSuggestion( + candidate: { file: TFile; effectiveTitle: string | null; pathQualifiedTarget: string }, + queryLower: string, + ): WikilinkFileSuggestion & { score: number } { + const { file, effectiveTitle, pathQualifiedTarget } = candidate + let score = 0 + if (queryLower.includes('/') && pathQualifiedTarget.startsWith(queryLower)) score += 40 + if (file.path.toLowerCase().includes(queryLower) || pathQualifiedTarget.includes(queryLower)) score += 20 + if (file.basename.toLowerCase().includes(queryLower)) score += 10 + if (effectiveTitle?.toLowerCase().includes(queryLower)) score += 5 + return { + kind: 'file', name: file.name, target: getInsertedTarget(file), path: file.path, + displayTitle: effectiveTitle, contextLabel: getContextLabel(file), snippet: null, score, + } + } + + private compareWikilinkSuggestions( + a: WikilinkFileSuggestion & { score: number }, + b: WikilinkFileSuggestion & { score: number }, + queryLower: string, + ): number { + const byTarget = compareSlashQualifiedTargets(a.target.toLowerCase(), b.target.toLowerCase(), queryLower) + if (byTarget !== 0) return byTarget + return b.score - a.score || a.path.localeCompare(b.path) + } + /** * Get autocomplete suggestions for the `![[` (image embed) trigger. * @@ -636,62 +607,52 @@ export class WikilinkPlugin extends Plugin { query: string, activeFile: TFile | null, ): WikilinkHeadingSuggestion[] { - const hashIdx = query.indexOf('#') - if (hashIdx < 0) return [] - const filePart = query.slice(0, hashIdx) - // The query may include a `|alias` suffix that the user is typing - // (e.g. `note#Intr|My intro`). Split it off so heading filtering - // matches against the heading-part only; alias is preserved on - // selection. - const afterHash = query.slice(hashIdx + 1) - const aliasIdx = afterHash.indexOf('|') - const headingPart = aliasIdx >= 0 ? afterHash.slice(0, aliasIdx) : afterHash - const aliasPart = aliasIdx >= 0 ? afterHash.slice(aliasIdx + 1) : null - - let file: TFile | null - if (!filePart) { - file = activeFile - } else { - file = this.getFileForTarget(filePart) - } + const parts = this.parseHeadingQuery(query) + if (!parts) return [] + const { filePart } = parts + const file = filePart ? this.getFileForTarget(filePart) : activeFile if (!file) return [] - const url = this.app.vault.getUrlForPath(file.path) if (!url) return [] - const cache = this.app.metadataCache.getFileCacheByUrl(url) const headings = cache?.headings ?? [] if (headings.length === 0) return [] - const slugs = slugifyHeadings(headings) - const headingPartLower = headingPart.toLowerCase() - - const out: WikilinkHeadingSuggestion[] = [] - for (let i = 0; i < headings.length; i++) { - const h = headings[i] - if ( - headingPart - && !h.heading.toLowerCase().includes(headingPartLower) - && !slugs[i].toLowerCase().includes(headingPartLower) - ) { - continue - } - // For current-doc completions, the inserted target is `#slug` (no - // file part). For cross-file, the inserted target uses the - // resolved file's basename — NOT the raw filePart the user typed. - // Keeps displayed `fileName` and inserted target consistent. - const baseTarget = filePart ? `${file.basename}#${slugs[i]}` : `#${slugs[i]}` - const insertTarget = aliasPart !== null ? `${baseTarget}|${aliasPart}` : baseTarget - out.push({ - kind: 'heading', - slug: slugs[i], - heading: h.heading, - level: h.level, - insertTarget, - fileName: filePart ? file.name : null, - }) + return headings.flatMap((heading, index) => { + const suggestion = this.createHeadingSuggestion(heading, slugs[index], file, parts) + return suggestion ? [suggestion] : [] + }) + } + + private parseHeadingQuery(query: string): { filePart: string; headingPart: string; aliasPart: string | null } | null { + const hashIdx = query.indexOf('#') + if (hashIdx < 0) return null + const filePart = query.slice(0, hashIdx) + const afterHash = query.slice(hashIdx + 1) + const aliasIdx = afterHash.indexOf('|') + return { + filePart, + headingPart: aliasIdx >= 0 ? afterHash.slice(0, aliasIdx) : afterHash, + aliasPart: aliasIdx >= 0 ? afterHash.slice(aliasIdx + 1) : null, + } + } + + private createHeadingSuggestion( + heading: { heading: string; level: number }, + slug: string, + file: TFile, + parts: { filePart: string; headingPart: string; aliasPart: string | null }, + ): WikilinkHeadingSuggestion | null { + const headingPartLower = parts.headingPart.toLowerCase() + if (parts.headingPart && !heading.heading.toLowerCase().includes(headingPartLower) && !slug.toLowerCase().includes(headingPartLower)) { + return null + } + const baseTarget = parts.filePart ? `${file.basename}#${slug}` : `#${slug}` + return { + kind: 'heading', slug, heading: heading.heading, level: heading.level, + insertTarget: parts.aliasPart !== null ? `${baseTarget}|${parts.aliasPart}` : baseTarget, + fileName: parts.filePart ? file.name : null, } - return out } /** diff --git a/tests/hygiene/__tests__/restricted-import-suppression-explanation.test.ts b/tests/hygiene/__tests__/restricted-import-suppression-explanation.test.ts index 46f2aae4..5c797f9d 100644 --- a/tests/hygiene/__tests__/restricted-import-suppression-explanation.test.ts +++ b/tests/hygiene/__tests__/restricted-import-suppression-explanation.test.ts @@ -2,8 +2,9 @@ import { it } from 'vitest' import { findUnexplainedSuppressions, formatProjectRelative } from '../ratchets' -it('requires every restricted-import suppression to be preceded by a FIXME(...) comment', () => { - // Hard rule: each suppression must have a sibling `// FIXME(...)` line +it('requires every restricted-import suppression to be preceded by a FIXME(...) or RATIONALE(...) comment', () => { + // Hard rule: each suppression must have a sibling `// FIXME(...)` or + // `// RATIONALE(...)` line // immediately above it that names the issue or limitation. Suppressions // without that context tend to accumulate as "ambient" exceptions whose // original justification is lost. Format the failure message inline so @@ -11,7 +12,7 @@ it('requires every restricted-import suppression to be preceded by a FIXME(...) const unexplained = findUnexplainedSuppressions() if (unexplained.length === 0) return throw new Error( - `Restricted-import suppressions missing a preceding FIXME(...) comment:\n` + `Restricted-import suppressions missing a preceding FIXME(...) or RATIONALE(...) comment:\n` + unexplained .map(suppression => ` - ${formatProjectRelative(suppression.filePath)}:${suppression.line}`) .join('\n'), diff --git a/tests/hygiene/ratchets.ts b/tests/hygiene/ratchets.ts index 0ec3fe9d..7dcb0f08 100644 --- a/tests/hygiene/ratchets.ts +++ b/tests/hygiene/ratchets.ts @@ -207,7 +207,7 @@ export function collectRestrictedImportSuppressions(): SuppressionEntry[] { const files = collectRestrictedImportScopeFiles() const commentPattern = /\/\/[^\n]*|\/\*[\s\S]*?\*\//g const suppressionPattern = /eslint-disable(?:-next-line|-line)?[\s\S]*?\bno-restricted-imports\b/ - const explanationPattern = /^\s*\/\/\s*FIXME\(/ + const explanationPattern = /^\s*\/\/\s*(?:FIXME|RATIONALE)\(/ for (const filePath of files) { const sourceText = fs.readFileSync(filePath, 'utf8') -- 2.51.2