From 2173ea3d6ce34c1a0861b98efd678094fb1a6818 Mon Sep 17 00:00:00 2001 From: nyanrus <68762426+nyanrus@users.noreply.github.com> Date: Tue, 23 Jun 2026 13:58:10 +0900 Subject: [PATCH 1/4] Add oxlint plugin to @fedify/lint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Oxlint's JS plugin API can host the same rule set as the ESLint plugin, so projects that lint with oxlint can catch the same Fedify federation mistakes. This exposes the rules in oxlint's plugin shape under a new `@fedify/lint/oxlint` subpath export, configured via `.oxlintrc.json`'s `jsPlugins` field. Note that oxlint's JS plugin support is upstream alpha and may be unstable. Co-authored-by: Hong Minhee (洪 民憙) Assisted-by: Claude Code:claude-opus-4-8 --- .hongdown.toml | 1 + cspell.json | 2 + deno.lock | 125 +++++++++++++++- docs/manual/lint.md | 122 +++++++++++++++- packages/lint/README.md | 136 +++++++++++++++++- packages/lint/deno.json | 8 +- packages/lint/package.json | 11 +- packages/lint/src/oxlint.ts | 135 ++++++++++++++++++ packages/lint/tsdown.config.ts | 2 +- pnpm-lock.yaml | 251 +++++++++++++++++++++++++++++++-- pnpm-workspace.yaml | 2 + 11 files changed, 767 insertions(+), 28 deletions(-) create mode 100644 packages/lint/src/oxlint.ts diff --git a/.hongdown.toml b/.hongdown.toml index 399e1c73c..e5b6adc8b 100644 --- a/.hongdown.toml +++ b/.hongdown.toml @@ -90,6 +90,7 @@ proper_nouns = [ "Object Integrity Proofs", "OpenTelemetry", "OpenTelemetry Collector", + "Oxlint", "Piefed", "Pixelfed", "Pleroma", diff --git a/cspell.json b/cspell.json index b6a11b6bd..27ecb7644 100644 --- a/cspell.json +++ b/cspell.json @@ -83,6 +83,8 @@ "nuxt", "operatables", "optique", + "oxlint", + "oxlintrc", "phensley", "Pico", "Pixelfed", diff --git a/deno.lock b/deno.lock index c7c9ff265..601e31b62 100644 --- a/deno.lock +++ b/deno.lock @@ -150,6 +150,8 @@ "npm:miniflare@^4.20250523.0": "4.20250906.0", "npm:mysql2@^3.22.3": "3.22.3_@types+node@24.12.2", "npm:ora@^8.2.0": "8.2.0", + "npm:oxlint@*": "1.66.0", + "npm:oxlint@^1.66.0": "1.66.0", "npm:pkijs@^3.2.5": "3.4.0", "npm:pkijs@^3.3.3": "3.4.0", "npm:pngjs@7": "7.0.0", @@ -2572,6 +2574,101 @@ "@oxc-project/types@0.129.0": { "integrity": "sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg==" }, + "@oxlint/binding-android-arm-eabi@1.66.0": { + "integrity": "sha512-f7kq8N51T4phpzqfBpA2qaVTI/KrkCmNwaj3t/97I/WLTDI+UhlP5GL9eER+zVxBhtlx5rKXWByJU1/zDAvyaw==", + "os": ["android"], + "cpu": ["arm"] + }, + "@oxlint/binding-android-arm64@1.66.0": { + "integrity": "sha512-xu6QO71tdDS9mjmLZ3AqhtaVHBvdmsOKkYnReNNDgh+XiwnsipeQOIxbiYOOO0iAXycJ+GK0wdMSZP/2j/AmSg==", + "os": ["android"], + "cpu": ["arm64"] + }, + "@oxlint/binding-darwin-arm64@1.66.0": { + "integrity": "sha512-HZ24VimSOC7mxuEA99e0H2FS0C1yO3+iW13jPRAk+e2njsUs3QeAXsafCDyaIrV/MirdOVez+etQNQsJE43zNQ==", + "os": ["darwin"], + "cpu": ["arm64"] + }, + "@oxlint/binding-darwin-x64@1.66.0": { + "integrity": "sha512-awhj8ZvJrrRSnXj7V++rpZvTmnl99L6mi0B7gg7Cp7BN6cKpzuI481bHNLvXGA9GB1/oEgA3ponuyoAc6Md12A==", + "os": ["darwin"], + "cpu": ["x64"] + }, + "@oxlint/binding-freebsd-x64@1.66.0": { + "integrity": "sha512-KQF0oVV21/FjIqkRuL8Q1vh8ECsE5+ocdH5tcqTQ4ZnYuDVoYibQUNfqBjQaUsP6UIIda5Y75Wpm5p4RgQWiWw==", + "os": ["freebsd"], + "cpu": ["x64"] + }, + "@oxlint/binding-linux-arm-gnueabihf@1.66.0": { + "integrity": "sha512-9u1rgwZSEXWb30vbFZzQ78HVXBo0WCKNwJ3a2InRUTNMRng+PUDIoSFmA+m4HdUfBaIqftShq8J8qHc+eE/Vig==", + "os": ["linux"], + "cpu": ["arm"] + }, + "@oxlint/binding-linux-arm-musleabihf@1.66.0": { + "integrity": "sha512-Ynot2HR1bHxUaNWoC280MVTDfZuaWuP3XfSMRDhyuZrVjhzoaBCVFlw8h8qeZjWKVUBhPWFIxB7AQTlK8Z2WWg==", + "os": ["linux"], + "cpu": ["arm"] + }, + "@oxlint/binding-linux-arm64-gnu@1.66.0": { + "integrity": "sha512-xCbgzciGgo+A4aQZEknsNrNiIwY7sU5SfRuMmRjPIvZAgdF34cIHiKvwOsS5XRLjlTVSFwitmq6YclTtHTfU+g==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@oxlint/binding-linux-arm64-musl@1.66.0": { + "integrity": "sha512-hmo+ZB/lHkR1HdDmnziNpzSLmulnUSu10VEqX2Yex7OwvoBAbjJQLvy4gIBRV3AAwWnCvAxKp5Nv1GE6LU1QMg==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@oxlint/binding-linux-ppc64-gnu@1.66.0": { + "integrity": "sha512-2Invd4Uyy81mVooQC5FBtfxSNrvcX1OxbMlVQ6M2erRrNI2awFYF26YNW2yFxdVFZ4ffNOWKghtMjhnUPsXsVA==", + "os": ["linux"], + "cpu": ["ppc64"] + }, + "@oxlint/binding-linux-riscv64-gnu@1.66.0": { + "integrity": "sha512-s0iXPDQVdgayE3RGa/N2DZF7tjgg0TwEtD1sGoDxqPDGrIXgo45H0yHknT0f9A0yteASsweYZtDyTuVlM4aSag==", + "os": ["linux"], + "cpu": ["riscv64"] + }, + "@oxlint/binding-linux-riscv64-musl@1.66.0": { + "integrity": "sha512-OekL4XFiu7RPK0JIZi8VeHgtIXPREf42t8Cy/rKEsC+P3gcqDgNAAGiyuUOpdbG4wwbfue1q4CHcCO7spSve6w==", + "os": ["linux"], + "cpu": ["riscv64"] + }, + "@oxlint/binding-linux-s390x-gnu@1.66.0": { + "integrity": "sha512-Ga1D0kj1SFslm34ThA/BdkUlyAYEnTsXyRC4pF0C5agZSwtGdHYWMTQWemUfBGp4RCG4QWXgdO+HmmmKqOtlBg==", + "os": ["linux"], + "cpu": ["s390x"] + }, + "@oxlint/binding-linux-x64-gnu@1.66.0": { + "integrity": "sha512-p5jfP1wUZe/IC3qpQO84n9DRnf9g3lKRtLBlQq23ykyrDglHcVx7sWmVTlPuU6SBw8mNnPzyOn022G3XZHnlww==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@oxlint/binding-linux-x64-musl@1.66.0": { + "integrity": "sha512-vUB/sYlYZorDL1ZD+o9mRv7zbsykrrFRtmgS6R8musZqLtrPRQn1gc1eGpuX+sfdccz42STl/AqldY6XRb2upQ==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@oxlint/binding-openharmony-arm64@1.66.0": { + "integrity": "sha512-yde+6p/F59xRkGR9H1HfngWRif1QRJjynZK349l+UI0H6w9hL3G8/AVaTHFyTtLVQ56qtNbX2/5Dc77n1ovnOg==", + "os": ["openharmony"], + "cpu": ["arm64"] + }, + "@oxlint/binding-win32-arm64-msvc@1.66.0": { + "integrity": "sha512-O9GLucgoTdmOrbBX+EjzNe7o/Ze5TFOvXcib6bzUOtBOmj6cV+zw18NgB+cGKAkDw1Pdqs8vGkfHbbsLuDtXWg==", + "os": ["win32"], + "cpu": ["arm64"] + }, + "@oxlint/binding-win32-ia32-msvc@1.66.0": { + "integrity": "sha512-m3Pjwc2MfTcom4E4gOv7DyuGyt7OfGNCbmqDHd+N7EzXmP+ppHuudm2NjcA3AjV5TSeGxaguVF4SbTKHe1USYA==", + "os": ["win32"], + "cpu": ["ia32"] + }, + "@oxlint/binding-win32-x64-msvc@1.66.0": { + "integrity": "sha512-/DbBvw8UFBhja6PqudUjV4UtfsJr0Oa7jUjWVKB0g86lj/VwnPrkngn0sFql3c9RDA0O16dh7ozsXb6GjNAzBQ==", + "os": ["win32"], + "cpu": ["x64"] + }, "@parcel/watcher-android-arm64@2.5.6": { "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", "os": ["android"], @@ -7071,6 +7168,31 @@ "strip-ansi@7.2.0" ] }, + "oxlint@1.66.0": { + "integrity": "sha512-N4LLxYLd94KEBqXDMDM5f+2PUpItTjDLreXe2Gn5KhjhCK4Qp2YUXaBi8Yu325ryOgKwt22m45fpD7nPOn69Yw==", + "optionalDependencies": [ + "@oxlint/binding-android-arm-eabi", + "@oxlint/binding-android-arm64", + "@oxlint/binding-darwin-arm64", + "@oxlint/binding-darwin-x64", + "@oxlint/binding-freebsd-x64", + "@oxlint/binding-linux-arm-gnueabihf", + "@oxlint/binding-linux-arm-musleabihf", + "@oxlint/binding-linux-arm64-gnu", + "@oxlint/binding-linux-arm64-musl", + "@oxlint/binding-linux-ppc64-gnu", + "@oxlint/binding-linux-riscv64-gnu", + "@oxlint/binding-linux-riscv64-musl", + "@oxlint/binding-linux-s390x-gnu", + "@oxlint/binding-linux-x64-gnu", + "@oxlint/binding-linux-x64-musl", + "@oxlint/binding-openharmony-arm64", + "@oxlint/binding-win32-arm64-msvc", + "@oxlint/binding-win32-ia32-msvc", + "@oxlint/binding-win32-x64-msvc" + ], + "bin": true + }, "p-limit@3.1.0": { "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dependencies": [ @@ -9554,7 +9676,8 @@ "packages/lint": { "dependencies": [ "npm:@types/estree@^1.0.8", - "npm:eslint@9" + "npm:eslint@9", + "npm:oxlint@^1.66.0" ], "packageJson": { "dependencies": [ diff --git a/docs/manual/lint.md b/docs/manual/lint.md index 2772ebb98..e1a9695ef 100644 --- a/docs/manual/lint.md +++ b/docs/manual/lint.md @@ -1,8 +1,8 @@ --- description: >- - Fedify provides linting plugins for Deno Lint and ESLint to help you catch - common mistakes and enforce best practices when building federated server - apps. + Fedify provides linting plugins for Deno Lint, ESLint, and Oxlint to help you + catch common mistakes and enforce best practices when building federated + server apps. --- Linting @@ -15,8 +15,9 @@ _This package is available since Fedify 2.0.0._ > app to catch common mistakes early and enforce best practices. Fedify provides the [`@fedify/lint`] package, which includes lint rules -specifically designed for Fedify applications. It supports both [Deno Lint] and -[ESLint], so you can use it regardless of your JavaScript/TypeScript runtime. +specifically designed for Fedify applications. It supports [Deno Lint], +[ESLint], and [Oxlint], so you can use it regardless of your +JavaScript/TypeScript runtime. The plugin includes rules that check for: @@ -29,6 +30,7 @@ The plugin includes rules that check for: [`@fedify/lint`]: https://jsr.io/@fedify/lint [Deno Lint]: https://docs.deno.com/runtime/reference/lint_plugins/ [ESLint]: https://eslint.org/ +[Oxlint]: https://oxc.rs/docs/guide/usage/linter/ Installation @@ -262,6 +264,114 @@ bunx eslint . ::: +Oxlint +------ + +[Oxlint] is a fast Rust-based linter that supports ESLint-compatible JS +plugins. `@fedify/lint` exposes its rules through Oxlint's [JS plugin API] +via the `@fedify/lint/oxlint` subpath export. + +> [!NOTE] +> Oxlint's JS plugin API is currently in alpha and not subject to semver. + +[JS plugin API]: https://oxc.rs/docs/guide/usage/linter/writing-js-plugins.html + +### Basic setup + +Add the plugin to your _.oxlintrc.json_ via the `jsPlugins` field, then enable +the rules you want: + +~~~~ json +{ + "$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json", + "jsPlugins": ["@fedify/lint/oxlint"], + "rules": { + "@fedify/lint/actor-id-required": "error", + "@fedify/lint/actor-id-mismatch": "error" + } +} +~~~~ + +Rule IDs are namespaced under `@fedify/lint/`, matching the ESLint preset. + +### Custom configuration + +Each rule accepts `"error"`, `"warn"`, or `"off"`. Enable any subset listed in +the [*Rules* section](#rules) below: + +~~~~ json +{ + "jsPlugins": ["@fedify/lint/oxlint"], + "rules": { + "@fedify/lint/actor-id-required": "error", + "@fedify/lint/actor-id-mismatch": "error", + "@fedify/lint/actor-inbox-property-required": "warn", + "@fedify/lint/actor-outbox-property-required": "warn", + "@fedify/lint/actor-followers-property-required": "warn", + "@fedify/lint/actor-public-key-required": "warn", + "@fedify/lint/actor-assertion-method-required": "warn", + "@fedify/lint/collection-filtering-not-implemented": "warn" + } +} +~~~~ + +### Running Oxlint + +Add a script to _package.json_: + +~~~~ jsonc +{ + "scripts": { + "lint": "oxlint ." + } +} +~~~~ + +Then run the linter: + +::: code-group + +~~~~ sh [npm] +npm run lint +~~~~ + +~~~~ sh [pnpm] +pnpm lint +~~~~ + +~~~~ sh [Yarn] +yarn lint +~~~~ + +~~~~ sh [Bun] +bun lint +~~~~ + +::: + +Or invoke Oxlint directly: + +::: code-group + +~~~~ sh [npm] +npx oxlint . +~~~~ + +~~~~ sh [pnpm] +pnpx oxlint . +~~~~ + +~~~~ sh [Yarn] +yarn oxlint . +~~~~ + +~~~~ sh [Bun] +bunx oxlint . +~~~~ + +::: + + Rules ----- @@ -1217,10 +1327,12 @@ See also - [`@fedify/lint` on npm] - [Deno Lint plugins documentation] - [ESLint documentation] + - [Oxlint documentation] - [Example project] [`@fedify/lint` on JSR]: https://jsr.io/@fedify/lint [`@fedify/lint` on npm]: https://www.npmjs.com/package/@fedify/lint [Deno Lint plugins documentation]: https://docs.deno.com/runtime/reference/lint_plugins/ [ESLint documentation]: https://eslint.org/ +[Oxlint documentation]: https://oxc.rs/docs/guide/usage/linter/ [Example project]: https://github.com/fedify-dev/fedify/tree/main/examples/lint diff --git a/packages/lint/README.md b/packages/lint/README.md index 877f49272..89c678a9d 100644 --- a/packages/lint/README.md +++ b/packages/lint/README.md @@ -1,7 +1,7 @@ -@fedify/lint: ESLint plugin for Fedify -====================================== +@fedify/lint: Lint plugins for Fedify +===================================== [![JSR][JSR badge]][JSR] [![npm][npm badge]][npm] @@ -9,10 +9,10 @@ *This package is available since Fedify 2.0.0.* -This package provides [Deno Lint] and [ESLint] plugin with lint rules -specifically designed for [Fedify] applications. It helps you catch common -mistakes and enforce best practices when building federated server apps with -Fedify. +This package provides [Deno Lint], [ESLint], and [Oxlint] plugins with lint +rules specifically designed for [Fedify] applications. It helps you catch +common mistakes and enforce best practices when building federated server apps +with Fedify. The plugin includes rules that check for: @@ -30,6 +30,7 @@ The plugin includes rules that check for: [@fedify@hollo.social]: https://hollo.social/@fedify [Deno Lint]: https://docs.deno.com/runtime/reference/lint_plugins/ [ESLint]: https://eslint.org/ +[Oxlint]: https://oxc.rs/docs/guide/usage/linter/ [Fedify]: https://fedify.dev/ ### Deno Lint configuration example @@ -62,6 +63,21 @@ import fedifyLint from "@fedify/lint"; export default fedifyLint; ~~~~ +### Oxlint configuration example + +~~~~ jsonc +// .oxlintrc.json + +{ + "jsPlugins": ["@fedify/lint/oxlint"], + "rules": { + "@fedify/lint/actor-id-required": "error", + "@fedify/lint/actor-id-mismatch": "error", + "@fedify/lint/actor-inbox-property-required": "warn" + } +} +~~~~ + Features -------- @@ -403,6 +419,114 @@ bunx eslint . ::: +Usage (Oxlint) +-------------- + +[Oxlint] is a fast Rust-based linter that supports ESLint-compatible JS plugins. +The `@fedify/lint/oxlint` subpath export plugs the Fedify rules into Oxlint's +[JS plugin API]. + +> [!NOTE] +> Oxlint's JS plugin API is currently in alpha and not subject to semver. + +[JS plugin API]: https://oxc.rs/docs/guide/usage/linter/writing-js-plugins.html + +### Basic setup + +Add the plugin and the rules you want to enable to your *.oxlintrc.json*: + +~~~~ json +{ + "$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json", + "jsPlugins": ["@fedify/lint/oxlint"], + "rules": { + "@fedify/lint/actor-id-required": "error", + "@fedify/lint/actor-id-mismatch": "error" + } +} +~~~~ + +Rule IDs are namespaced under `@fedify/lint/`, matching the ESLint +configuration above. + +### Custom configuration + +Enable any subset of the rules listed in the *Features* section above. Each +rule can be set to `"error"`, `"warn"`, or `"off"`: + +~~~~ json +{ + "jsPlugins": ["@fedify/lint/oxlint"], + "rules": { + "@fedify/lint/actor-id-required": "error", + "@fedify/lint/actor-id-mismatch": "error", + "@fedify/lint/actor-inbox-property-required": "warn", + "@fedify/lint/actor-outbox-property-required": "warn", + "@fedify/lint/actor-followers-property-required": "warn", + "@fedify/lint/actor-public-key-required": "warn", + "@fedify/lint/actor-assertion-method-required": "warn", + "@fedify/lint/collection-filtering-not-implemented": "warn" + } +} +~~~~ + +### Running Oxlint + +Add a script to *package.json*: + +~~~~ jsonc +{ + "scripts": { + "lint": "oxlint ." + } +} +~~~~ + +Then run: + +::: code-group + +~~~~ sh [npm] +npm run lint +~~~~ + +~~~~ sh [pnpm] +pnpm lint +~~~~ + +~~~~ sh [Yarn] +yarn lint +~~~~ + +~~~~ sh [Bun] +bun lint +~~~~ + +::: + +Or invoke Oxlint directly: + +::: code-group + +~~~~ sh [npm] +npx oxlint . +~~~~ + +~~~~ sh [pnpm] +pnpx oxlint . +~~~~ + +~~~~ sh [Yarn] +yarn oxlint . +~~~~ + +~~~~ sh [Bun] +bunx oxlint . +~~~~ + +::: + + See also -------- diff --git a/packages/lint/deno.json b/packages/lint/deno.json index cf6a0fa27..a785cfda0 100644 --- a/packages/lint/deno.json +++ b/packages/lint/deno.json @@ -3,11 +3,13 @@ "version": "2.3.0", "license": "MIT", "exports": { - ".": "./src/mod.ts" + ".": "./src/mod.ts", + "./oxlint": "./src/oxlint.ts" }, "imports": { "eslint": "npm:eslint@^9.0.0", - "estree": "npm:@types/estree@^1.0.8" + "estree": "npm:@types/estree@^1.0.8", + "oxlint": "npm:oxlint@^1.66.0" }, "publish": { "exclude": [ @@ -16,6 +18,6 @@ ] }, "tasks": { - "test": "deno test --allow-env" + "test": "deno test --allow-env --allow-read --allow-run --allow-sys --allow-write" } } diff --git a/packages/lint/package.json b/packages/lint/package.json index 6bbca994f..da1449c12 100644 --- a/packages/lint/package.json +++ b/packages/lint/package.json @@ -8,7 +8,9 @@ "Fediverse", "Lint", "ESLint", - "ESLint Plugin" + "ESLint Plugin", + "oxlint", + "oxc" ], "author": { "name": "Chanhaeng Lee", @@ -38,6 +40,11 @@ "import": "./dist/index.js", "require": "./dist/index.cjs" }, + "./oxlint": { + "types": "./dist/oxlint.d.ts", + "import": "./dist/oxlint.js", + "require": "./dist/oxlint.cjs" + }, "./package.json": "./package.json" }, "files": [ @@ -60,9 +67,11 @@ "@typescript-eslint/utils": "^8.0.0" }, "devDependencies": { + "@fedify/fixture": "workspace:*", "@types/eslint": "^9.0.0", "@types/estree": "^1.0.9", "eslint": "^9.0.0", + "oxlint": "catalog:", "tsdown": "catalog:", "typescript": "catalog:" }, diff --git a/packages/lint/src/oxlint.ts b/packages/lint/src/oxlint.ts new file mode 100644 index 000000000..f9a0f0461 --- /dev/null +++ b/packages/lint/src/oxlint.ts @@ -0,0 +1,135 @@ +/** + * oxlint plugin for Fedify. + * + * Exposes the same rule set as the ESLint plugin in a shape suitable for + * oxlint's JS plugin API (https://oxc.rs/docs/guide/usage/linter/writing-js-plugins.html). + * + * Configure via `.oxlintrc.json`: + * + * ```json + * { + * "jsPlugins": ["@fedify/lint/oxlint"], + * "rules": { + * "@fedify/lint/actor-id-required": "error" + * } + * } + * ``` + */ +import type { Rule } from "eslint"; +import metadataRaw from "../deno.json" with { type: "json" }; +import { RULE_IDS } from "./lib/const.ts"; +import { + eslint as actorAssertionMethodRequired, +} from "./rules/actor-assertion-method-required.ts"; +import { + eslint as actorFeaturedPropertyMismatch, +} from "./rules/actor-featured-property-mismatch.ts"; +import { + eslint as actorFeaturedPropertyRequired, +} from "./rules/actor-featured-property-required.ts"; +import { + eslint as actorFeaturedTagsPropertyMismatch, +} from "./rules/actor-featured-tags-property-mismatch.ts"; +import { + eslint as actorFeaturedTagsPropertyRequired, +} from "./rules/actor-featured-tags-property-required.ts"; +import { + eslint as actorFollowersPropertyMismatch, +} from "./rules/actor-followers-property-mismatch.ts"; +import { + eslint as actorFollowersPropertyRequired, +} from "./rules/actor-followers-property-required.ts"; +import { + eslint as actorFollowingPropertyMismatch, +} from "./rules/actor-following-property-mismatch.ts"; +import { + eslint as actorFollowingPropertyRequired, +} from "./rules/actor-following-property-required.ts"; +import { eslint as actorIdMismatch } from "./rules/actor-id-mismatch.ts"; +import { eslint as actorIdRequired } from "./rules/actor-id-required.ts"; +import { + eslint as actorInboxPropertyMismatch, +} from "./rules/actor-inbox-property-mismatch.ts"; +import { + eslint as actorInboxPropertyRequired, +} from "./rules/actor-inbox-property-required.ts"; +import { + eslint as actorLikedPropertyMismatch, +} from "./rules/actor-liked-property-mismatch.ts"; +import { + eslint as actorLikedPropertyRequired, +} from "./rules/actor-liked-property-required.ts"; +import { + eslint as actorOutboxPropertyMismatch, +} from "./rules/actor-outbox-property-mismatch.ts"; +import { + eslint as actorOutboxPropertyRequired, +} from "./rules/actor-outbox-property-required.ts"; +import { + eslint as actorPublicKeyRequired, +} from "./rules/actor-public-key-required.ts"; +import { + eslint as actorSharedInboxPropertyMismatch, +} from "./rules/actor-shared-inbox-property-mismatch.ts"; +import { + eslint as actorSharedInboxPropertyRequired, +} from "./rules/actor-shared-inbox-property-required.ts"; +import { + eslint as collectionFiltering, +} from "./rules/collection-filtering-not-implemented.ts"; +import { + eslint as outboxListenerDeliveryRequired, +} from "./rules/outbox-listener-delivery-required.ts"; + +const rules: Record< + typeof RULE_IDS[keyof typeof RULE_IDS], + Rule.RuleModule +> = { + [RULE_IDS.actorIdMismatch]: actorIdMismatch, + [RULE_IDS.actorIdRequired]: actorIdRequired, + [RULE_IDS.actorFollowingPropertyRequired]: actorFollowingPropertyRequired, + [RULE_IDS.actorFollowingPropertyMismatch]: actorFollowingPropertyMismatch, + [RULE_IDS.actorFollowersPropertyRequired]: actorFollowersPropertyRequired, + [RULE_IDS.actorFollowersPropertyMismatch]: actorFollowersPropertyMismatch, + [RULE_IDS.actorOutboxPropertyRequired]: actorOutboxPropertyRequired, + [RULE_IDS.actorOutboxPropertyMismatch]: actorOutboxPropertyMismatch, + [RULE_IDS.actorLikedPropertyRequired]: actorLikedPropertyRequired, + [RULE_IDS.actorLikedPropertyMismatch]: actorLikedPropertyMismatch, + [RULE_IDS.actorFeaturedPropertyRequired]: actorFeaturedPropertyRequired, + [RULE_IDS.actorFeaturedPropertyMismatch]: actorFeaturedPropertyMismatch, + [RULE_IDS.actorFeaturedTagsPropertyRequired]: + actorFeaturedTagsPropertyRequired, + [RULE_IDS.actorFeaturedTagsPropertyMismatch]: + actorFeaturedTagsPropertyMismatch, + [RULE_IDS.actorInboxPropertyRequired]: actorInboxPropertyRequired, + [RULE_IDS.actorInboxPropertyMismatch]: actorInboxPropertyMismatch, + [RULE_IDS.actorSharedInboxPropertyRequired]: actorSharedInboxPropertyRequired, + [RULE_IDS.actorSharedInboxPropertyMismatch]: actorSharedInboxPropertyMismatch, + [RULE_IDS.actorPublicKeyRequired]: actorPublicKeyRequired, + [RULE_IDS.actorAssertionMethodRequired]: actorAssertionMethodRequired, + [RULE_IDS.collectionFilteringNotImplemented]: collectionFiltering, + [RULE_IDS.outboxListenerDeliveryRequired]: outboxListenerDeliveryRequired, +}; + +/** + * Plugin object compatible with Oxlint's JS plugin API. + * + * Kept module-private on purpose: consumers should rely on the default + * export and the Oxlint runtime to resolve the shape, rather than + * importing this type and risking drift from upstream's `Plugin` + * definition in `@oxlint/plugins`. + */ +interface OxlintPlugin { + meta: { name: string; version: string }; + rules: Record; +} + +const plugin: OxlintPlugin = { + meta: { + name: metadataRaw.name, + version: metadataRaw.version, + }, + rules, +}; + +export default plugin; diff --git a/packages/lint/tsdown.config.ts b/packages/lint/tsdown.config.ts index 41c45aead..7312e7fd0 100644 --- a/packages/lint/tsdown.config.ts +++ b/packages/lint/tsdown.config.ts @@ -1,7 +1,7 @@ import { defineConfig } from "tsdown"; export default defineConfig({ - entry: ["src/index.ts"], + entry: ["src/index.ts", "src/oxlint.ts"], dts: { compilerOptions: { isolatedDeclarations: true, declaration: true } }, format: ["esm", "cjs"], platform: "node", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 04daa6d4d..1f2341172 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -165,6 +165,9 @@ catalogs: nuxt: specifier: ^4.4.2 version: 4.4.2 + oxlint: + specifier: ^1.66.0 + version: 1.66.0 pkijs: specifier: ^3.3.3 version: 3.3.3 @@ -550,6 +553,22 @@ importers: specifier: ^5.5.4 version: 5.9.2 + examples/lint/oxlint: + dependencies: + '@fedify/fedify': + specifier: workspace:^ + version: link:../../../packages/fedify + '@fedify/vocab': + specifier: workspace:^ + version: link:../../../packages/vocab + devDependencies: + '@fedify/lint': + specifier: workspace:^ + version: link:../../../packages/lint + oxlint: + specifier: 'catalog:' + version: 1.66.0 + examples/next-integration: dependencies: '@fedify/fedify': @@ -719,7 +738,7 @@ importers: version: 1.15.11 nuxt: specifier: 'catalog:' - version: 4.4.2(75d3bb99739572e2ce1a5d32bf0400f3) + version: 4.4.2(9b6f49a83ed1b2fc8889dfd9b7f2c45d) devDependencies: '@types/node': specifier: 'catalog:' @@ -1399,6 +1418,9 @@ importers: specifier: ^8.0.0 version: 8.41.0(eslint@9.32.0(jiti@2.6.1))(typescript@6.0.3) devDependencies: + '@fedify/fixture': + specifier: workspace:* + version: link:../fixture '@types/eslint': specifier: ^9.0.0 version: 9.6.1 @@ -1408,6 +1430,9 @@ importers: eslint: specifier: ^9.0.0 version: 9.32.0(jiti@2.6.1) + oxlint: + specifier: 'catalog:' + version: 1.66.0 tsdown: specifier: 'catalog:' version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)) @@ -1503,7 +1528,7 @@ importers: version: 1.15.11 nuxt: specifier: 'catalog:' - version: 4.4.2(75d3bb99739572e2ce1a5d32bf0400f3) + version: 4.4.2(9b6f49a83ed1b2fc8889dfd9b7f2c45d) devDependencies: '@fedify/fixture': specifier: workspace:^ @@ -4766,6 +4791,120 @@ packages: cpu: [x64] os: [win32] + '@oxlint/binding-android-arm-eabi@1.66.0': + resolution: {integrity: sha512-f7kq8N51T4phpzqfBpA2qaVTI/KrkCmNwaj3t/97I/WLTDI+UhlP5GL9eER+zVxBhtlx5rKXWByJU1/zDAvyaw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxlint/binding-android-arm64@1.66.0': + resolution: {integrity: sha512-xu6QO71tdDS9mjmLZ3AqhtaVHBvdmsOKkYnReNNDgh+XiwnsipeQOIxbiYOOO0iAXycJ+GK0wdMSZP/2j/AmSg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxlint/binding-darwin-arm64@1.66.0': + resolution: {integrity: sha512-HZ24VimSOC7mxuEA99e0H2FS0C1yO3+iW13jPRAk+e2njsUs3QeAXsafCDyaIrV/MirdOVez+etQNQsJE43zNQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxlint/binding-darwin-x64@1.66.0': + resolution: {integrity: sha512-awhj8ZvJrrRSnXj7V++rpZvTmnl99L6mi0B7gg7Cp7BN6cKpzuI481bHNLvXGA9GB1/oEgA3ponuyoAc6Md12A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxlint/binding-freebsd-x64@1.66.0': + resolution: {integrity: sha512-KQF0oVV21/FjIqkRuL8Q1vh8ECsE5+ocdH5tcqTQ4ZnYuDVoYibQUNfqBjQaUsP6UIIda5Y75Wpm5p4RgQWiWw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxlint/binding-linux-arm-gnueabihf@1.66.0': + resolution: {integrity: sha512-9u1rgwZSEXWb30vbFZzQ78HVXBo0WCKNwJ3a2InRUTNMRng+PUDIoSFmA+m4HdUfBaIqftShq8J8qHc+eE/Vig==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm-musleabihf@1.66.0': + resolution: {integrity: sha512-Ynot2HR1bHxUaNWoC280MVTDfZuaWuP3XfSMRDhyuZrVjhzoaBCVFlw8h8qeZjWKVUBhPWFIxB7AQTlK8Z2WWg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm64-gnu@1.66.0': + resolution: {integrity: sha512-xCbgzciGgo+A4aQZEknsNrNiIwY7sU5SfRuMmRjPIvZAgdF34cIHiKvwOsS5XRLjlTVSFwitmq6YclTtHTfU+g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxlint/binding-linux-arm64-musl@1.66.0': + resolution: {integrity: sha512-hmo+ZB/lHkR1HdDmnziNpzSLmulnUSu10VEqX2Yex7OwvoBAbjJQLvy4gIBRV3AAwWnCvAxKp5Nv1GE6LU1QMg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxlint/binding-linux-ppc64-gnu@1.66.0': + resolution: {integrity: sha512-2Invd4Uyy81mVooQC5FBtfxSNrvcX1OxbMlVQ6M2erRrNI2awFYF26YNW2yFxdVFZ4ffNOWKghtMjhnUPsXsVA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@oxlint/binding-linux-riscv64-gnu@1.66.0': + resolution: {integrity: sha512-s0iXPDQVdgayE3RGa/N2DZF7tjgg0TwEtD1sGoDxqPDGrIXgo45H0yHknT0f9A0yteASsweYZtDyTuVlM4aSag==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxlint/binding-linux-riscv64-musl@1.66.0': + resolution: {integrity: sha512-OekL4XFiu7RPK0JIZi8VeHgtIXPREf42t8Cy/rKEsC+P3gcqDgNAAGiyuUOpdbG4wwbfue1q4CHcCO7spSve6w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxlint/binding-linux-s390x-gnu@1.66.0': + resolution: {integrity: sha512-Ga1D0kj1SFslm34ThA/BdkUlyAYEnTsXyRC4pF0C5agZSwtGdHYWMTQWemUfBGp4RCG4QWXgdO+HmmmKqOtlBg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@oxlint/binding-linux-x64-gnu@1.66.0': + resolution: {integrity: sha512-p5jfP1wUZe/IC3qpQO84n9DRnf9g3lKRtLBlQq23ykyrDglHcVx7sWmVTlPuU6SBw8mNnPzyOn022G3XZHnlww==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxlint/binding-linux-x64-musl@1.66.0': + resolution: {integrity: sha512-vUB/sYlYZorDL1ZD+o9mRv7zbsykrrFRtmgS6R8musZqLtrPRQn1gc1eGpuX+sfdccz42STl/AqldY6XRb2upQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxlint/binding-openharmony-arm64@1.66.0': + resolution: {integrity: sha512-yde+6p/F59xRkGR9H1HfngWRif1QRJjynZK349l+UI0H6w9hL3G8/AVaTHFyTtLVQ56qtNbX2/5Dc77n1ovnOg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxlint/binding-win32-arm64-msvc@1.66.0': + resolution: {integrity: sha512-O9GLucgoTdmOrbBX+EjzNe7o/Ze5TFOvXcib6bzUOtBOmj6cV+zw18NgB+cGKAkDw1Pdqs8vGkfHbbsLuDtXWg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxlint/binding-win32-ia32-msvc@1.66.0': + resolution: {integrity: sha512-m3Pjwc2MfTcom4E4gOv7DyuGyt7OfGNCbmqDHd+N7EzXmP+ppHuudm2NjcA3AjV5TSeGxaguVF4SbTKHe1USYA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxlint/binding-win32-x64-msvc@1.66.0': + resolution: {integrity: sha512-/DbBvw8UFBhja6PqudUjV4UtfsJr0Oa7jUjWVKB0g86lj/VwnPrkngn0sFql3c9RDA0O16dh7ozsXb6GjNAzBQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@parcel/watcher-android-arm64@2.5.6': resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} engines: {node: '>= 10.0.0'} @@ -10252,6 +10391,16 @@ packages: peerDependencies: oxc-parser: '>=0.98.0' + oxlint@1.66.0: + resolution: {integrity: sha512-N4LLxYLd94KEBqXDMDM5f+2PUpItTjDLreXe2Gn5KhjhCK4Qp2YUXaBi8Yu325ryOgKwt22m45fpD7nPOn69Yw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + oxlint-tsgolint: '>=0.22.1' + peerDependenciesMeta: + oxlint-tsgolint: + optional: true + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -14954,7 +15103,7 @@ snapshots: transitivePeerDependencies: - magicast - '@nuxt/nitro-server@4.4.2(@babel/core@7.29.0)(better-sqlite3@12.9.0)(db0@0.3.4(better-sqlite3@12.9.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.6.1)(better-sqlite3@12.9.0)(bun-types@1.3.3)(mysql2@3.22.3(@types/node@22.19.1))(postgres@3.4.7))(mysql2@3.22.3(@types/node@22.19.1)))(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.6.1)(better-sqlite3@12.9.0)(bun-types@1.3.3)(mysql2@3.22.3(@types/node@22.19.1))(postgres@3.4.7))(ioredis@5.10.1)(magicast@0.5.2)(mysql2@3.22.3(@types/node@22.19.1))(nuxt@4.4.2(75d3bb99739572e2ce1a5d32bf0400f3))(rolldown@1.0.0)(typescript@6.0.3)': + '@nuxt/nitro-server@4.4.2(@babel/core@7.29.0)(better-sqlite3@12.9.0)(db0@0.3.4(better-sqlite3@12.9.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.6.1)(better-sqlite3@12.9.0)(bun-types@1.3.3)(mysql2@3.22.3(@types/node@22.19.1))(postgres@3.4.7))(mysql2@3.22.3(@types/node@22.19.1)))(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.6.1)(better-sqlite3@12.9.0)(bun-types@1.3.3)(mysql2@3.22.3(@types/node@22.19.1))(postgres@3.4.7))(ioredis@5.10.1)(magicast@0.5.2)(mysql2@3.22.3(@types/node@22.19.1))(nuxt@4.4.2(9b6f49a83ed1b2fc8889dfd9b7f2c45d))(rolldown@1.0.0)(typescript@6.0.3)': dependencies: '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) '@nuxt/devalue': 2.0.2 @@ -14973,7 +15122,7 @@ snapshots: klona: 2.0.6 mocked-exports: 0.1.1 nitropack: 2.13.3(better-sqlite3@12.9.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.6.1)(better-sqlite3@12.9.0)(bun-types@1.3.3)(mysql2@3.22.3(@types/node@22.19.1))(postgres@3.4.7))(mysql2@3.22.3(@types/node@22.19.1))(rolldown@1.0.0) - nuxt: 4.4.2(75d3bb99739572e2ce1a5d32bf0400f3) + nuxt: 4.4.2(9b6f49a83ed1b2fc8889dfd9b7f2c45d) nypm: 0.6.5 ohash: 2.0.11 pathe: 2.0.3 @@ -15040,7 +15189,7 @@ snapshots: rc9: 3.0.1 std-env: 4.1.0 - '@nuxt/vite-builder@4.4.2(@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0))(@types/node@22.19.1)(lightningcss@1.30.1)(magicast@0.5.2)(nuxt@4.4.2(75d3bb99739572e2ce1a5d32bf0400f3))(optionator@0.9.4)(rolldown@1.0.0)(rollup-plugin-visualizer@7.0.1(rolldown@1.0.0)(rollup@4.60.2))(rollup@4.60.2)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.3)(vue@3.5.33(typescript@6.0.3))(yaml@2.9.0)': + '@nuxt/vite-builder@4.4.2(@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0))(@types/node@22.19.1)(lightningcss@1.30.1)(magicast@0.5.2)(nuxt@4.4.2(9b6f49a83ed1b2fc8889dfd9b7f2c45d))(optionator@0.9.4)(oxlint@1.66.0)(rolldown@1.0.0)(rollup-plugin-visualizer@7.0.1(rolldown@1.0.0)(rollup@4.60.2))(rollup@4.60.2)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.3)(vue@3.5.33(typescript@6.0.3))(yaml@2.9.0)': dependencies: '@nuxt/kit': 4.4.2(magicast@0.5.2) '@rollup/plugin-replace': 6.0.3(rollup@4.60.2) @@ -15058,7 +15207,7 @@ snapshots: magic-string: 0.30.21 mlly: 1.8.2 mocked-exports: 0.1.1 - nuxt: 4.4.2(75d3bb99739572e2ce1a5d32bf0400f3) + nuxt: 4.4.2(9b6f49a83ed1b2fc8889dfd9b7f2c45d) nypm: 0.6.5 pathe: 2.0.3 pkg-types: 2.3.0 @@ -15069,7 +15218,7 @@ snapshots: unenv: 2.0.0-rc.24 vite: 7.3.2(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0) vite-node: 5.3.0(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0) - vite-plugin-checker: 0.12.0(optionator@0.9.4)(typescript@6.0.3)(vite@7.3.2(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0)) + vite-plugin-checker: 0.12.0(optionator@0.9.4)(oxlint@1.66.0)(typescript@6.0.3)(vite@7.3.2(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0)) vue: 3.5.33(typescript@6.0.3) vue-bundle-renderer: 2.2.0 optionalDependencies: @@ -15877,6 +16026,63 @@ snapshots: '@oxc-transform/binding-win32-x64-msvc@0.117.0': optional: true + '@oxlint/binding-android-arm-eabi@1.66.0': + optional: true + + '@oxlint/binding-android-arm64@1.66.0': + optional: true + + '@oxlint/binding-darwin-arm64@1.66.0': + optional: true + + '@oxlint/binding-darwin-x64@1.66.0': + optional: true + + '@oxlint/binding-freebsd-x64@1.66.0': + optional: true + + '@oxlint/binding-linux-arm-gnueabihf@1.66.0': + optional: true + + '@oxlint/binding-linux-arm-musleabihf@1.66.0': + optional: true + + '@oxlint/binding-linux-arm64-gnu@1.66.0': + optional: true + + '@oxlint/binding-linux-arm64-musl@1.66.0': + optional: true + + '@oxlint/binding-linux-ppc64-gnu@1.66.0': + optional: true + + '@oxlint/binding-linux-riscv64-gnu@1.66.0': + optional: true + + '@oxlint/binding-linux-riscv64-musl@1.66.0': + optional: true + + '@oxlint/binding-linux-s390x-gnu@1.66.0': + optional: true + + '@oxlint/binding-linux-x64-gnu@1.66.0': + optional: true + + '@oxlint/binding-linux-x64-musl@1.66.0': + optional: true + + '@oxlint/binding-openharmony-arm64@1.66.0': + optional: true + + '@oxlint/binding-win32-arm64-msvc@1.66.0': + optional: true + + '@oxlint/binding-win32-ia32-msvc@1.66.0': + optional: true + + '@oxlint/binding-win32-x64-msvc@1.66.0': + optional: true + '@parcel/watcher-android-arm64@2.5.6': optional: true @@ -22508,16 +22714,16 @@ snapshots: dependencies: boolbase: 1.0.0 - nuxt@4.4.2(75d3bb99739572e2ce1a5d32bf0400f3): + nuxt@4.4.2(9b6f49a83ed1b2fc8889dfd9b7f2c45d): dependencies: '@dxup/nuxt': 0.4.1(magicast@0.5.2)(typescript@6.0.3) '@nuxt/cli': 3.34.0(@nuxt/schema@4.4.2)(cac@6.7.14)(magicast@0.5.2) '@nuxt/devtools': 3.2.4(vite@7.3.2(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.33(typescript@6.0.3)) '@nuxt/kit': 4.4.2(magicast@0.5.2) - '@nuxt/nitro-server': 4.4.2(@babel/core@7.29.0)(better-sqlite3@12.9.0)(db0@0.3.4(better-sqlite3@12.9.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.6.1)(better-sqlite3@12.9.0)(bun-types@1.3.3)(mysql2@3.22.3(@types/node@22.19.1))(postgres@3.4.7))(mysql2@3.22.3(@types/node@22.19.1)))(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.6.1)(better-sqlite3@12.9.0)(bun-types@1.3.3)(mysql2@3.22.3(@types/node@22.19.1))(postgres@3.4.7))(ioredis@5.10.1)(magicast@0.5.2)(mysql2@3.22.3(@types/node@22.19.1))(nuxt@4.4.2(75d3bb99739572e2ce1a5d32bf0400f3))(rolldown@1.0.0)(typescript@6.0.3) + '@nuxt/nitro-server': 4.4.2(@babel/core@7.29.0)(better-sqlite3@12.9.0)(db0@0.3.4(better-sqlite3@12.9.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.6.1)(better-sqlite3@12.9.0)(bun-types@1.3.3)(mysql2@3.22.3(@types/node@22.19.1))(postgres@3.4.7))(mysql2@3.22.3(@types/node@22.19.1)))(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260511.1)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.6.1)(better-sqlite3@12.9.0)(bun-types@1.3.3)(mysql2@3.22.3(@types/node@22.19.1))(postgres@3.4.7))(ioredis@5.10.1)(magicast@0.5.2)(mysql2@3.22.3(@types/node@22.19.1))(nuxt@4.4.2(9b6f49a83ed1b2fc8889dfd9b7f2c45d))(rolldown@1.0.0)(typescript@6.0.3) '@nuxt/schema': 4.4.2 '@nuxt/telemetry': 2.8.0(@nuxt/kit@4.4.2(magicast@0.5.2)) - '@nuxt/vite-builder': 4.4.2(@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0))(@types/node@22.19.1)(lightningcss@1.30.1)(magicast@0.5.2)(nuxt@4.4.2(75d3bb99739572e2ce1a5d32bf0400f3))(optionator@0.9.4)(rolldown@1.0.0)(rollup-plugin-visualizer@7.0.1(rolldown@1.0.0)(rollup@4.60.2))(rollup@4.60.2)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.3)(vue@3.5.33(typescript@6.0.3))(yaml@2.9.0) + '@nuxt/vite-builder': 4.4.2(@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0))(@types/node@22.19.1)(lightningcss@1.30.1)(magicast@0.5.2)(nuxt@4.4.2(9b6f49a83ed1b2fc8889dfd9b7f2c45d))(optionator@0.9.4)(oxlint@1.66.0)(rolldown@1.0.0)(rollup-plugin-visualizer@7.0.1(rolldown@1.0.0)(rollup@4.60.2))(rollup@4.60.2)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.3)(vue@3.5.33(typescript@6.0.3))(yaml@2.9.0) '@unhead/vue': 2.1.13(vue@3.5.33(typescript@6.0.3)) '@vue/shared': 3.5.33 c12: 3.3.4(magicast@0.5.2) @@ -22871,6 +23077,28 @@ snapshots: magic-regexp: 0.10.0 oxc-parser: 0.117.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + oxlint@1.66.0: + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.66.0 + '@oxlint/binding-android-arm64': 1.66.0 + '@oxlint/binding-darwin-arm64': 1.66.0 + '@oxlint/binding-darwin-x64': 1.66.0 + '@oxlint/binding-freebsd-x64': 1.66.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.66.0 + '@oxlint/binding-linux-arm-musleabihf': 1.66.0 + '@oxlint/binding-linux-arm64-gnu': 1.66.0 + '@oxlint/binding-linux-arm64-musl': 1.66.0 + '@oxlint/binding-linux-ppc64-gnu': 1.66.0 + '@oxlint/binding-linux-riscv64-gnu': 1.66.0 + '@oxlint/binding-linux-riscv64-musl': 1.66.0 + '@oxlint/binding-linux-s390x-gnu': 1.66.0 + '@oxlint/binding-linux-x64-gnu': 1.66.0 + '@oxlint/binding-linux-x64-musl': 1.66.0 + '@oxlint/binding-openharmony-arm64': 1.66.0 + '@oxlint/binding-win32-arm64-msvc': 1.66.0 + '@oxlint/binding-win32-ia32-msvc': 1.66.0 + '@oxlint/binding-win32-x64-msvc': 1.66.0 + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -25403,7 +25631,7 @@ snapshots: - tsx - yaml - vite-plugin-checker@0.12.0(optionator@0.9.4)(typescript@6.0.3)(vite@7.3.2(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0)): + vite-plugin-checker@0.12.0(optionator@0.9.4)(oxlint@1.66.0)(typescript@6.0.3)(vite@7.3.2(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0)): dependencies: '@babel/code-frame': 7.29.0 chokidar: 4.0.3 @@ -25416,6 +25644,7 @@ snapshots: vscode-uri: 3.1.0 optionalDependencies: optionator: 0.9.4 + oxlint: 1.66.0 typescript: 6.0.3 vite-plugin-inspect@11.3.3(@nuxt/kit@4.4.2(magicast@0.5.2))(vite@7.3.2(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0)): diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b6b1a1cf3..433b73212 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -37,6 +37,7 @@ packages: - examples/elysia - examples/express - examples/koa +- examples/lint/oxlint - examples/next-integration - examples/fastify - examples/next14-app-router @@ -108,6 +109,7 @@ catalog: pkijs: ^3.3.3 mysql2: ^3.22.3 nuxt: ^4.4.2 + oxlint: ^1.66.0 postgres: ^3.4.7 tsdown: ^0.22.0 typescript: ^6.0.0 From 97cb381df946e6376b32daa82be7ae29e635cdff Mon Sep 17 00:00:00 2001 From: nyanrus <68762426+nyanrus@users.noreply.github.com> Date: Tue, 23 Jun 2026 13:58:24 +0900 Subject: [PATCH 2/4] Add an oxlint example for @fedify/lint Demonstrate the plugin end-to-end: federation.ts intentionally violates several rules (missing id, inbox, outbox, followers) while federation.fixed.ts passes them all. Register the example in the test runner so that `mise run test:examples` verifies the fixed fixture lints clean and the violating fixture fails. Assisted-by: Claude Code:claude-opus-4-8 --- deno.json | 1 + examples/lint/oxlint/.oxlintrc.json | 11 ++++ examples/lint/oxlint/README.md | 72 ++++++++++++++++++++++++ examples/lint/oxlint/deno.json | 6 ++ examples/lint/oxlint/federation.fixed.ts | 21 +++++++ examples/lint/oxlint/federation.ts | 17 ++++++ examples/lint/oxlint/package.json | 19 +++++++ examples/test-examples/mod.ts | 10 ++++ 8 files changed, 157 insertions(+) create mode 100644 examples/lint/oxlint/.oxlintrc.json create mode 100644 examples/lint/oxlint/README.md create mode 100644 examples/lint/oxlint/deno.json create mode 100644 examples/lint/oxlint/federation.fixed.ts create mode 100644 examples/lint/oxlint/federation.ts create mode 100644 examples/lint/oxlint/package.json diff --git a/deno.json b/deno.json index 396f2554a..c7412369b 100644 --- a/deno.json +++ b/deno.json @@ -33,6 +33,7 @@ "./examples/astro", "./examples/fresh", "./examples/hono-sample", + "./examples/lint/oxlint", "./examples/monitoring", "./examples/rfc-9421-test", "./test/smoke/harness" diff --git a/examples/lint/oxlint/.oxlintrc.json b/examples/lint/oxlint/.oxlintrc.json new file mode 100644 index 000000000..fad2f581b --- /dev/null +++ b/examples/lint/oxlint/.oxlintrc.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json", + "jsPlugins": ["@fedify/lint/oxlint"], + "rules": { + "@fedify/lint/actor-id-required": "error", + "@fedify/lint/actor-id-mismatch": "error", + "@fedify/lint/actor-inbox-property-required": "warn", + "@fedify/lint/actor-outbox-property-required": "warn", + "@fedify/lint/actor-followers-property-required": "warn" + } +} diff --git a/examples/lint/oxlint/README.md b/examples/lint/oxlint/README.md new file mode 100644 index 000000000..e6e1eddcf --- /dev/null +++ b/examples/lint/oxlint/README.md @@ -0,0 +1,72 @@ + + +@fedify/lint with Oxlint +======================== + +This example demonstrates how to use [`@fedify/lint`] together with [Oxlint] +to catch common Fedify federation mistakes. Note that Oxlint's JS plugin +support is upstream alpha and may be unstable. + +[`@fedify/lint`]: https://www.npmjs.com/package/@fedify/lint +[Oxlint]: https://oxc.rs/docs/guide/usage/linter/ + + +Layout +------ + + - *.oxlintrc.json* — Oxlint configuration that enables `@fedify/lint` + via the JS plugin API. + - *federation.ts* — code that intentionally violates several rules + (missing `id`, `inbox`, `outbox`, `followers`). + - *federation.fixed.ts* — corrected version that passes all rules. + + +Usage +----- + +Install dependencies and run the linter: + +~~~~ sh +pnpm install +pnpm lint +~~~~ + +You should see at least one `@fedify/lint(actor-id-required)` error on +*federation.ts*. Running against *federation.fixed.ts* alone produces no +diagnostics: + +~~~~ sh +pnpm lint:fixed +~~~~ + +The same tasks are also wired into *deno.json*, so you can invoke Oxlint +through Deno (the plugin still resolves out of *node\_modules*, so +`pnpm install` is required first): + +~~~~ sh +deno task lint +deno task lint:fixed +~~~~ + + +How it works +------------ + +The plugin is loaded via the `jsPlugins` field in *.oxlintrc.json*: + +~~~~ json +{ + "jsPlugins": ["@fedify/lint/oxlint"], + "rules": { + "@fedify/lint/actor-id-required": "error" + } +} +~~~~ + +`@fedify/lint/oxlint` is a subpath export that exposes the same rules as the +ESLint plugin in Oxlint's plugin shape. Rule IDs are namespaced under +`@fedify/lint/`. + +See the [Linting] manual for the full rule reference. + +[Linting]: https://fedify.dev/manual/lint diff --git a/examples/lint/oxlint/deno.json b/examples/lint/oxlint/deno.json new file mode 100644 index 000000000..7e1b4d1d0 --- /dev/null +++ b/examples/lint/oxlint/deno.json @@ -0,0 +1,6 @@ +{ + "tasks": { + "lint": "deno run -A npm:oxlint .", + "lint:fixed": "deno run -A npm:oxlint federation.fixed.ts" + } +} diff --git a/examples/lint/oxlint/federation.fixed.ts b/examples/lint/oxlint/federation.fixed.ts new file mode 100644 index 000000000..5b4a22f56 --- /dev/null +++ b/examples/lint/oxlint/federation.fixed.ts @@ -0,0 +1,21 @@ +import { + createFederation, + InProcessMessageQueue, + MemoryKvStore, +} from "@fedify/fedify"; +import { Person } from "@fedify/vocab"; + +const federation = createFederation({ + kv: new MemoryKvStore(), + queue: new InProcessMessageQueue(), +}); + +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + name: "Example User", + inbox: ctx.getInboxUri(identifier), + outbox: ctx.getOutboxUri(identifier), + followers: ctx.getFollowersUri(identifier), + }); +}); diff --git a/examples/lint/oxlint/federation.ts b/examples/lint/oxlint/federation.ts new file mode 100644 index 000000000..7704040a5 --- /dev/null +++ b/examples/lint/oxlint/federation.ts @@ -0,0 +1,17 @@ +import { + createFederation, + InProcessMessageQueue, + MemoryKvStore, +} from "@fedify/fedify"; +import { Person } from "@fedify/vocab"; + +const federation = createFederation({ + kv: new MemoryKvStore(), + queue: new InProcessMessageQueue(), +}); + +federation.setActorDispatcher("/users/{identifier}", (_ctx, _identifier) => { + return new Person({ + name: "Example User", + }); +}); diff --git a/examples/lint/oxlint/package.json b/examples/lint/oxlint/package.json new file mode 100644 index 000000000..5dbef1cd8 --- /dev/null +++ b/examples/lint/oxlint/package.json @@ -0,0 +1,19 @@ +{ + "name": "@fedify/example-lint-oxlint", + "version": "0.0.0", + "private": true, + "description": "Example project demonstrating @fedify/lint with oxlint", + "type": "module", + "scripts": { + "lint": "oxlint .", + "lint:fixed": "oxlint federation.fixed.ts" + }, + "dependencies": { + "@fedify/fedify": "workspace:^", + "@fedify/vocab": "workspace:^" + }, + "devDependencies": { + "@fedify/lint": "workspace:^", + "oxlint": "catalog:" + } +} diff --git a/examples/test-examples/mod.ts b/examples/test-examples/mod.ts index 5c5318a8a..c15abd073 100644 --- a/examples/test-examples/mod.ts +++ b/examples/test-examples/mod.ts @@ -293,6 +293,16 @@ const SCRIPT_EXAMPLES: ScriptExample[] = [ cmd: ["deno", "run", "--allow-all", "main.ts"], description: "Custom collection demonstration (in-process federation)", }, + { + // Oxlint example for @fedify/lint. The corrected fixture must lint + // clean, and the intentionally violating fixture must produce + // diagnostics (non-zero exit). Requires the @fedify/lint package to + // be built first (`mise run prepare-each lint`). + name: "lint", + dir: "lint/oxlint", + cmd: ["sh", "-c", "pnpm lint:fixed && ! pnpm lint"], + description: "@fedify/lint with Oxlint (fixed passes, violating fails)", + }, ]; const MULTI_HANDLE_EXAMPLES: MultiHandleExample[] = [ From d3f7e5f540f511cad50c802358623763fb4e1c69 Mon Sep 17 00:00:00 2001 From: nyanrus <68762426+nyanrus@users.noreply.github.com> Date: Tue, 23 Jun 2026 13:58:39 +0900 Subject: [PATCH 3/4] Test the oxlint plugin end-to-end MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit oxlint is a Rust binary, so the only way to exercise the JS plugin loader is to spawn it as a subprocess against a real config file. Two lanes do this: - oxlint.test.ts asserts actor-id-required fires on a missing id. - integration.test.ts runs the whole rule suite through oxlint as an additional lane, so every rule is also checked through the oxlint adapter. The shared setup — locating the binary, the skip precondition (built loader plus binary), the tmpdir fixture, and the JSON parsing — lives in lib/oxlint.ts so both lanes reuse it. When the loader or the binary is missing, the oxlint lanes are skipped with a warning. Assisted-by: Claude Code:claude-opus-4-8 --- packages/lint/src/lib/oxlint.ts | 145 ++++++++++++++++++++ packages/lint/src/tests/integration.test.ts | 37 ++++- packages/lint/src/tests/oxlint.test.ts | 81 +++++++++++ 3 files changed, 261 insertions(+), 2 deletions(-) create mode 100644 packages/lint/src/lib/oxlint.ts create mode 100644 packages/lint/src/tests/oxlint.test.ts diff --git a/packages/lint/src/lib/oxlint.ts b/packages/lint/src/lib/oxlint.ts new file mode 100644 index 000000000..b0a735fa6 --- /dev/null +++ b/packages/lint/src/lib/oxlint.ts @@ -0,0 +1,145 @@ +/** + * Shared helpers for the oxlint-based tests. + * + * oxlint is a Rust binary, so the only way to exercise the JS plugin loader + * end-to-end is to spawn it as a subprocess against a real config file. Both + * `tests/oxlint.test.ts` and the oxlint lane of `tests/integration.test.ts` + * need the same setup — locating the built loader and the binary, writing a + * tmpdir fixture, running oxlint, and parsing its JSON diagnostics — so that + * logic lives here once. + */ +import { spawnSync } from "node:child_process"; +import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import process from "node:process"; +import { fileURLToPath } from "node:url"; + +const here = dirname(fileURLToPath(import.meta.url)); + +/** Absolute path to the built oxlint plugin loader (`dist/oxlint.js`). */ +export const oxlintPluginPath: string = resolve(here, "../../dist/oxlint.js"); + +/** Whether the built loader exists. */ +export const pluginBuilt: boolean = existsSync(oxlintPluginPath); + +/** + * Locate the oxlint binary: prefer the package- and workspace-local + * `node_modules/.bin`, then fall back to whatever is on `PATH`. Returns + * `null` when it cannot be found. + */ +export function findOxlint(): string | null { + const candidates = [ + resolve(here, "../../node_modules/.bin/oxlint"), + resolve(here, "../../../../node_modules/.bin/oxlint"), + ]; + for (const candidate of candidates) { + if (existsSync(candidate)) return candidate; + } + const where = spawnSync( + process.platform === "win32" ? "where" : "which", + ["oxlint"], + { encoding: "utf8" }, + ); + if (where.status === 0 && where.stdout) { + return where.stdout.trim().split(/\r?\n/)[0]; + } + return null; +} + +/** The resolved oxlint binary path, or `null` when it is unavailable. */ +export const oxlintBin: string | null = findOxlint(); + +/** + * `true` when either the built loader or the oxlint binary is missing, in + * which case the oxlint-based tests should be skipped via `{ ignore }`. + */ +export const oxlintUnavailable: boolean = !pluginBuilt || oxlintBin == null; + +/** + * Print a single warning explaining why the oxlint tests are being skipped. + * Call this only when {@link oxlintUnavailable} is `true`. + */ +export function warnOxlintSkipped(): void { + const missing: string[] = []; + if (!pluginBuilt) missing.push(`built loader at ${oxlintPluginPath}`); + if (oxlintBin == null) { + missing.push("oxlint binary on PATH or in node_modules"); + } + console.warn( + `Skipping oxlint tests — missing: ${missing.join(", ")}.\n` + + "To enable them, run `mise run install && mise run prepare-each lint` " + + "from the repository root so both the loader and the oxlint binary " + + "are available. Prefer `mise run test` (full suite) or " + + "`mise run test-each lint` (this package) to run tests — those tasks " + + "build the prerequisites for you.", + ); +} + +/** A single diagnostic as reported by oxlint's `--format=json` output. */ +export interface OxlintDiagnostic { + code?: string; + message?: string; + severity?: string; +} + +/** The result of running oxlint over a fixture. */ +export interface OxlintRunResult { + /** Process exit status (`null` if terminated by a signal). */ + status: number | null; + /** Parsed diagnostics. */ + diagnostics: OxlintDiagnostic[]; + /** Raw stdout, kept for assertion messages. */ + stdout: string; + /** Raw stderr, kept for assertion messages. */ + stderr: string; +} + +/** + * Run oxlint over a single source file inside a throwaway tmpdir. + * + * Writes `.oxlintrc.json` (always wiring up the built plugin loader and + * merging in `config`) plus a source file named `fileName` containing `code`, + * spawns oxlint with `--format=json`, parses the JSON report, and removes the + * tmpdir before returning. + * + * Callers must ensure {@link oxlintBin} is non-null first. + */ +export function runOxlint( + code: string, + config: Record, + fileName = "code.ts", +): OxlintRunResult { + const dir = mkdtempSync(join(tmpdir(), "fedify-lint-oxlint-")); + try { + writeFileSync( + join(dir, ".oxlintrc.json"), + JSON.stringify({ jsPlugins: [oxlintPluginPath], ...config }), + ); + writeFileSync(join(dir, fileName), code); + + const result = spawnSync(oxlintBin!, ["--format=json", fileName], { + cwd: dir, + encoding: "utf8", + }); + + let payload: { diagnostics?: OxlintDiagnostic[] }; + try { + payload = JSON.parse(result.stdout); + } catch (err) { + throw new Error( + `Failed to parse oxlint JSON output: ${(err as Error).message}\n` + + `stdout: ${result.stdout}\nstderr: ${result.stderr}`, + ); + } + + return { + status: result.status, + diagnostics: payload.diagnostics ?? [], + stdout: result.stdout, + stderr: result.stderr, + }; + } finally { + rmSync(dir, { recursive: true, force: true }); + } +} diff --git a/packages/lint/src/tests/integration.test.ts b/packages/lint/src/tests/integration.test.ts index 6136eca56..778d37d1b 100644 --- a/packages/lint/src/tests/integration.test.ts +++ b/packages/lint/src/tests/integration.test.ts @@ -12,6 +12,11 @@ import { Linter } from "eslint"; import { equal, ok } from "node:assert/strict"; import { test } from "node:test"; import { plugin as eslintPlugin } from "../index.ts"; +import { + oxlintUnavailable, + runOxlint, + warnOxlintSkipped, +} from "../lib/oxlint.ts"; import { replace } from "../lib/utils.ts"; import denoPlugin from "../mod.ts"; @@ -24,9 +29,16 @@ type Diagnostic = { /** * Run all lint rules on the given code and return diagnostics. + * + * The in-process linter for the current runtime (Deno Lint or ESLint) always + * runs. When the built oxlint loader and the oxlint binary are available, + * the same code additionally goes through oxlint, so every rule is exercised + * end-to-end through the oxlint JS plugin adapter as well. */ -const lintTest = (code: string): Diagnostic[] => - "Deno" in globalThis ? testDenoLint(code) : testEslint(code); +const lintTest = (code: string): Diagnostic[] => [ + ...("Deno" in globalThis ? testDenoLint(code) : testEslint(code)), + ...(oxlintUnavailable ? [] : testOxlint(code)), +]; const testDenoLint = (code: string) => Deno.lint.runPlugin( @@ -58,6 +70,27 @@ function testEslint(code: string) { })); } +// oxlint is a Rust binary, so it is exercised as a subprocess against a real +// config file — the setup lives in ../lib/oxlint.ts and is shared with +// oxlint.test.ts. When the built loader or the binary is missing, the oxlint +// lane is skipped with a warning. +if (oxlintUnavailable) warnOxlintSkipped(); + +function testOxlint(code: string): Diagnostic[] { + const { diagnostics } = runOxlint(code, { + // Turn off oxlint's built-in rules so that only the diagnostics of this + // plugin surface. + categories: { correctness: "off" }, + rules: eslintPlugin.configs.recommended.rules, + }); + return diagnostics.map((d) => ({ + // Normalize oxlint's "@fedify/lint(rule-name)" code into the same + // "/rule-name" shape the other lanes report. + id: (d.code ?? "").replace(/^@fedify\/lint\((.+)\)$/, `${PLUGIN_NAME}/$1`), + message: d.message ?? "", + })); +} + /** * Assert that the code passes all lint rules (no diagnostics). */ diff --git a/packages/lint/src/tests/oxlint.test.ts b/packages/lint/src/tests/oxlint.test.ts new file mode 100644 index 000000000..f9f3bc51b --- /dev/null +++ b/packages/lint/src/tests/oxlint.test.ts @@ -0,0 +1,81 @@ +/** + * Integration test for the oxlint plugin entry. + * + * Spawns the oxlint binary against a tmpdir fixture that violates + * `actor-id-required`, parses the JSON diagnostics, and asserts the + * `@fedify/lint/actor-id-required` rule fires. + * + * Runtime notes: + * + * - The shared helpers in `../lib/oxlint.ts` use `node:child_process`, + * `node:fs`, `node:os`, `node:path`, and `node:process`. Under Deno + * these resolve via the Node compatibility layer, so the same source + * runs under both `pnpm test` (via `node:test`) and `deno task test` + * (via `Deno.test`) — `@fedify/fixture` dispatches the test definition + * to the appropriate runtime. + * - Other rule tests in this package use the in-process linter APIs + * (`Deno.lint.runPlugin` / ESLint's `Linter`). This one is + * different on purpose: oxlint is a Rust binary, so we spawn it as + * a subprocess against a real config file. That's the only way to + * exercise the JS plugin loader end-to-end. + * - Two preconditions are checked at module load (see {@link + * oxlintUnavailable}). If either is missing, the test is skipped via + * `{ ignore }`: + * * the built loader at `/dist/oxlint.js` + * * the oxlint binary, located under `/node_modules/.bin`, + * the workspace root, or anywhere on `PATH` + */ +import { test } from "@fedify/fixture"; +import { ok } from "node:assert/strict"; +import { + oxlintUnavailable, + runOxlint, + warnOxlintSkipped, +} from "../lib/oxlint.ts"; + +if (oxlintUnavailable) warnOxlintSkipped(); + +const BAD_CODE = + `import { createFederation, InProcessMessageQueue, MemoryKvStore } from "@fedify/fedify"; +import { Person } from "@fedify/vocab"; + +const federation = createFederation({ + kv: new MemoryKvStore(), + queue: new InProcessMessageQueue(), +}); + +federation.setActorDispatcher("/users/{identifier}", (_ctx, _identifier) => { + return new Person({ + name: "Bad Actor", + }); +}); +`; + +test( + "oxlint plugin: actor-id-required fires on missing id", + { ignore: oxlintUnavailable }, + () => { + const { status, diagnostics, stderr } = runOxlint( + BAD_CODE, + { rules: { "@fedify/lint/actor-id-required": "error" } }, + "federation.ts", + ); + + ok( + status !== 0, + `Expected non-zero exit, got ${status}. stderr: ${stderr}`, + ); + + const codes = diagnostics.map((d) => d.code ?? ""); + const matched = codes.some((code) => + code === "@fedify/lint(actor-id-required)" || + code.includes("actor-id-required") + ); + ok( + matched, + `Expected @fedify/lint(actor-id-required) diagnostic, got: ${ + codes.join(", ") || "(none)" + }`, + ); + }, +); From 793d66379bcd853667e424cceb7b5b2088e9f19f Mon Sep 17 00:00:00 2001 From: nyanrus <68762426+nyanrus@users.noreply.github.com> Date: Tue, 23 Jun 2026 13:58:54 +0900 Subject: [PATCH 4/4] Depend the test:examples task on prepare MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The lint/oxlint example needs two things the runner cannot produce on its own: @fedify/lint's built dist/oxlint.js loader and the oxlint binary in node_modules. `prepare` provides both — it builds the loader via build:self and, through its install dependency, provisions the prebuilt oxlint binary — so depend on it. Assisted-by: Claude Code:claude-opus-4-8 --- mise.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mise.toml b/mise.toml index 4231499bb..d82465775 100644 --- a/mise.toml +++ b/mise.toml @@ -181,6 +181,10 @@ done [tasks."test:examples"] description = "Run tests for all example projects" +# `prepare` builds @fedify/lint's dist/oxlint.js loader (via build:self) and, +# through its `install` dependency, provisions the prebuilt oxlint native +# binary into node_modules — both are needed by the lint/oxlint example. +depends = ["prepare"] run = "deno run -A examples/test-examples/mod.ts" [tasks."test:monitoring"]