diff --git a/.github/workflows/release_apps.yml b/.github/workflows/release_apps.yml index 4d4cb178c4186..0c4a1164e2767 100644 --- a/.github/workflows/release_apps.yml +++ b/.github/workflows/release_apps.yml @@ -274,6 +274,7 @@ jobs: env: package_path: npm/oxlint plugins_package_path: npm/oxlint-plugins + plugin_eslint_package_path: npm/oxlint-plugin-eslint npm_dir: npm/oxlint-release PUBLISH_FLAGS: "--provenance --access public --no-git-checks" steps: @@ -303,12 +304,17 @@ jobs: run: | cp apps/oxlint/dist-pkg-plugins/* ${plugins_package_path}/ + - name: Copy dist files to oxlint-plugin-eslint npm package + run: | + cp -r apps/oxlint/dist-pkg-plugin-eslint/. ${plugin_eslint_package_path}/ + - run: npm install -g npm@latest # For trusted publishing support - name: Check Publish run: | node .github/scripts/check-npm-packages.js "${npm_dir}/*" "${package_path}" node .github/scripts/check-npm-packages.js "${plugins_package_path}" + node .github/scripts/check-npm-packages.js "${plugin_eslint_package_path}" - name: Trusted Publish run: | @@ -319,6 +325,8 @@ jobs: pnpm publish ${package_path}/ ${PUBLISH_FLAGS} # Publish `@oxlint/plugins` package pnpm publish ${plugins_package_path}/ ${PUBLISH_FLAGS} + # Publish `oxlint-plugin-eslint` package + pnpm publish ${plugin_eslint_package_path}/ ${PUBLISH_FLAGS} build-oxfmt: needs: check diff --git a/apps/oxlint/.gitignore b/apps/oxlint/.gitignore index 8b33a8b91eb50..e1e1120765b11 100644 --- a/apps/oxlint/.gitignore +++ b/apps/oxlint/.gitignore @@ -1,4 +1,6 @@ /node_modules/ /dist/ /dist-pkg-plugins/ +/dist-pkg-plugin-eslint/ +/src-js/generated/plugin-eslint/ *.node diff --git a/apps/oxlint/scripts/build.ts b/apps/oxlint/scripts/build.ts index d47b5afa9c627..a7eb1e5a89f38 100755 --- a/apps/oxlint/scripts/build.ts +++ b/apps/oxlint/scripts/build.ts @@ -7,12 +7,24 @@ import { join } from "node:path"; const oxlintDirPath = join(import.meta.dirname, ".."), srcDirPath = join(oxlintDirPath, "src-js"), distDirPath = join(oxlintDirPath, "dist"), - distPkgPluginsDirPath = join(oxlintDirPath, "dist-pkg-plugins"); + distPkgPluginsDirPath = join(oxlintDirPath, "dist-pkg-plugins"), + distPkgPluginEslintDirPath = join(oxlintDirPath, "dist-pkg-plugin-eslint"); // Delete `dist-pkg-plugins` directory console.log("Deleting `dist-pkg-plugins` directory..."); rmSync(distPkgPluginsDirPath, { recursive: true, force: true }); +// Delete `dist-pkg-plugin-eslint` directory +console.log("Deleting `dist-pkg-plugin-eslint` directory..."); +rmSync(distPkgPluginEslintDirPath, { recursive: true, force: true }); + +// Generate plugin-eslint files +console.log("Generating plugin-eslint files..."); +execSync("node scripts/generate-plugin-eslint.ts", { + stdio: "inherit", + cwd: oxlintDirPath, +}); + // Build with tsdown console.log("Building with tsdown..."); execSync("pnpm tsdown", { stdio: "inherit", cwd: oxlintDirPath }); diff --git a/apps/oxlint/scripts/generate-plugin-eslint.ts b/apps/oxlint/scripts/generate-plugin-eslint.ts new file mode 100644 index 0000000000000..89129e88a046b --- /dev/null +++ b/apps/oxlint/scripts/generate-plugin-eslint.ts @@ -0,0 +1,57 @@ +import { readdirSync, mkdirSync, writeFileSync } from "node:fs"; +import { join, basename } from "node:path"; +import { createRequire } from "node:module"; + +const require = createRequire(import.meta.url); + +const oxlintDirPath = join(import.meta.dirname, ".."); +const eslintRulesDir = join(require.resolve("eslint/package.json"), "../lib/rules"); +const generatedDirPath = join(oxlintDirPath, "src-js/generated/plugin-eslint"); +const generatedRulesDirPath = join(generatedDirPath, "rules"); + +// Get all ESLint rule names (exclude index.js which is the registry, not a rule) +const ruleNames = readdirSync(eslintRulesDir) + .filter((f) => f.endsWith(".js") && f !== "index.js") + .map((f) => basename(f, ".js")) + .sort(); + +// oxlint-disable-next-line no-console +console.log(`Found ${ruleNames.length} ESLint rules`); + +// Create generated directories +mkdirSync(generatedRulesDirPath, { recursive: true }); + +// Generate a CJS wrapper file for each rule +for (const ruleName of ruleNames) { + const content = `module.exports = require("../../../../node_modules/eslint/lib/rules/${ruleName}.js");`; + writeFileSync(join(generatedRulesDirPath, `${ruleName}.cjs`), content); +} + +// Generate the plugin rules index (ESM with lazy getters) +const indexLines = [ + `import { createRequire } from "node:module";`, + `const require = createRequire(import.meta.url);`, + ``, + `export default {`, +]; +for (const ruleName of ruleNames) { + indexLines.push( + ` get ${JSON.stringify(ruleName)}() { return require("./rules/${ruleName}.cjs"); },`, + ); +} +indexLines.push(`};`, ``); + +writeFileSync(join(generatedDirPath, "index.ts"), indexLines.join("\n")); + +// Generate the rule_names.ts file for use in tsdown config +const ruleNamesLines = [ + `export default [`, + ...ruleNames.map((name) => ` ${JSON.stringify(name)},`), + `] as const;`, + ``, +]; + +writeFileSync(join(generatedDirPath, "rule_names.ts"), ruleNamesLines.join("\n")); + +// oxlint-disable-next-line no-console +console.log("Generated plugin-eslint files."); diff --git a/apps/oxlint/src-js/plugin-eslint/index.ts b/apps/oxlint/src-js/plugin-eslint/index.ts new file mode 100644 index 0000000000000..d0665a1d76e80 --- /dev/null +++ b/apps/oxlint/src-js/plugin-eslint/index.ts @@ -0,0 +1,8 @@ +import rules from "../generated/plugin-eslint/index.ts"; + +export default { + meta: { + name: "eslint-js", + }, + rules, +}; diff --git a/apps/oxlint/tsdown.config.ts b/apps/oxlint/tsdown.config.ts index 6bed83260437c..be3d5219de016 100644 --- a/apps/oxlint/tsdown.config.ts +++ b/apps/oxlint/tsdown.config.ts @@ -2,6 +2,7 @@ import fs from "node:fs"; import { join as pathJoin, relative as pathRelative, dirname } from "node:path"; import { defineConfig } from "tsdown"; import { parseSync, Visitor } from "oxc-parser"; +import ruleNames from "./src-js/generated/plugin-eslint/rule_names.ts"; import type { Plugin } from "rolldown"; @@ -63,6 +64,25 @@ const pluginsPkgConfig = defineConfig({ define: definedGlobals, }); +// Build entries for `oxlint-plugin-eslint` rule files. +// Each rule is a separate CJS file, lazy-loaded on demand. +const pluginEslintRulesEntries: Record = {}; +for (const ruleName of ruleNames) { + pluginEslintRulesEntries[`rules/${ruleName}`] = + `src-js/generated/plugin-eslint/rules/${ruleName}.cjs`; +} + +// Base config for `oxlint-plugin-eslint` package. +// "node12" target to match `engines` field of last ESLint 8 release (8.57.1). +const pluginEslintPkgConfig = defineConfig({ + ...commonConfig, + outDir: "dist-pkg-plugin-eslint", + // `build.ts` deletes the directory before TSDown runs. + // This allows generating the ESM and CommonJS builds in the same directory. + clean: false, + dts: false, +}); + // Plugins. // Only remove debug assertions in release build. const plugins = [createReplaceGlobalsPlugin()]; @@ -103,6 +123,18 @@ export default defineConfig([ format: "commonjs", dts: false, }, + + // `oxlint-plugin-eslint` package + { + ...pluginEslintPkgConfig, + entry: { index: "src-js/plugin-eslint/index.ts" }, + format: "esm", + }, + { + ...pluginEslintPkgConfig, + entry: pluginEslintRulesEntries, + format: "commonjs", + }, ]); /** diff --git a/npm/oxlint-plugin-eslint/CHANGELOG.md b/npm/oxlint-plugin-eslint/CHANGELOG.md new file mode 100644 index 0000000000000..e30b69168493c --- /dev/null +++ b/npm/oxlint-plugin-eslint/CHANGELOG.md @@ -0,0 +1,3 @@ +# Changelog + +All notable changes to this package will be documented in this file. diff --git a/npm/oxlint-plugin-eslint/README.md b/npm/oxlint-plugin-eslint/README.md new file mode 100644 index 0000000000000..ca33d382bc18b --- /dev/null +++ b/npm/oxlint-plugin-eslint/README.md @@ -0,0 +1,37 @@ +# oxlint-plugin-eslint + +ESLint's built-in rules as an Oxlint plugin. + +This package exports all of ESLint's built-in rules as a JS plugin that Oxlint users can use. + +Allows using ESLint rules that Oxlint doesn't implement natively yet. + +More details in [Oxlint docs](https://oxc.rs/docs/guide/usage/linter/js-plugins). + +## Usage + +Install the package: + +```sh +npm install --save-dev oxlint-plugin-eslint +``` + +Add to your Oxlint config: + +```json +{ + "jsPlugins": ["oxlint-plugin-eslint"], + "rules": { + "eslint-js/no-restricted-syntax": [ + "error", + { + "selector": "ThrowStatement > CallExpression[callee.name=/Error$/]", + "message": "Use `new` keyword when throwing an `Error`." + } + ] + } +} +``` + +All rules are prefixed with `eslint-js/`, to distinguish them from the native implementation of many ESLint rules +in Oxlint. diff --git a/npm/oxlint-plugin-eslint/package.json b/npm/oxlint-plugin-eslint/package.json new file mode 100644 index 0000000000000..419cb29dea939 --- /dev/null +++ b/npm/oxlint-plugin-eslint/package.json @@ -0,0 +1,33 @@ +{ + "name": "oxlint-plugin-eslint", + "version": "1.51.0", + "description": "ESLint's built-in rules as an Oxlint plugin", + "keywords": [ + "eslint", + "linter", + "oxlint", + "plugin" + ], + "homepage": "https://oxc.rs/docs/guide/usage/linter/js-plugins", + "bugs": "https://github.com/oxc-project/oxc/issues", + "license": "MIT", + "author": "Boshen and oxc contributors", + "repository": { + "type": "git", + "url": "git+https://github.com/oxc-project/oxc.git", + "directory": "npm/oxlint-plugin-eslint" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "files": [ + "index.js", + "rules", + "README.md" + ], + "type": "module", + "main": "index.js", + "engines": { + "node": "^20.19.0 || >=22.12.0" + } +} diff --git a/oxc_release.toml b/oxc_release.toml index b2fc598852416..7ecfa2259b077 100644 --- a/oxc_release.toml +++ b/oxc_release.toml @@ -21,6 +21,7 @@ versioned_files = [ "apps/oxlint/package.json", "crates/oxc_linter/Cargo.toml", "npm/oxlint/package.json", + "npm/oxlint-plugin-eslint/package.json", "npm/oxlint-plugins/package.json", ] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9542efc9ae646..0168a004d1395 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -284,6 +284,12 @@ importers: specifier: '>=0.15.0' version: 0.16.0 + npm/oxlint-plugin-eslint: + dependencies: + eslint: + specifier: '>=8.0.0' + version: 9.39.2(jiti@2.6.1) + npm/oxlint-plugins: {} npm/runtime: {}