Skip to content

Commit

Permalink
fix: add support import('eslint/use-at-your-own-risk') for eslint v9 (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
ota-meshi committed Apr 13, 2024
1 parent fd600f0 commit a9bef96
Show file tree
Hide file tree
Showing 19 changed files with 791 additions and 29 deletions.
5 changes: 5 additions & 0 deletions .changeset/forty-worms-accept.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"vite-plugin-eslint4b": minor
---

fix: add support import('eslint/use-at-your-own-risk') for eslint v9
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
/lib
node_modules
tests/fixtures/build-test/dist
tests/fixtures/build-test-v8/dist
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@
},
"homepage": "https://github.com/ota-meshi/vite-plugin-eslint4b#readme",
"peerDependencies": {
"eslint": "^8.0.0 || ^9.0.0"
"eslint": "^8.0.0 || ^9.0.0",
"vite": ">=4.0.0"
},
"devDependencies": {
"@changesets/changelog-github": "^0.5.0",
Expand Down Expand Up @@ -79,6 +80,7 @@
},
"dependencies": {
"assert": "^2.0.0",
"esbuild": "^0.20.0"
"esbuild": "^0.20.0",
"magic-string": "^0.30.9"
}
}
1 change: 1 addition & 0 deletions shim/fs-shim.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export function existsSync() {
return true;
}
export default { existsSync };
138 changes: 120 additions & 18 deletions src/vite-plugin-eslint4b.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import type { Plugin as VitePlugin, UserConfig } from "vite";
import { createFilter } from "vite";
import path from "path";
import fs from "fs";
import { createRequire } from "module";
import esbuild from "esbuild";
import { fileURLToPath } from "url";
import MagicString from "magic-string";

const filename = import.meta.url ? fileURLToPath(import.meta.url) : __filename;
const dirname = path.dirname(filename);
Expand All @@ -15,6 +17,8 @@ const virtualLinterModuleId = `${baseVirtualModuleId}_eslint_linter`;
const resolvedVirtualLinterModuleId = `\0${virtualLinterModuleId}`;
const virtualSourceCodeModuleId = `${baseVirtualModuleId}_eslint_source_code`;
const resolvedVirtualSourceCodeModuleId = `\0${virtualSourceCodeModuleId}`;
const virtualRulesModuleId = `${baseVirtualModuleId}_eslint_rules`;
const resolvedVirtualRulesModuleId = `\0${virtualRulesModuleId}`;
const virtualPackageJsonModuleId = `${virtualESLintModuleId}/package.json`;
const resolvedVirtualPackageJsonModuleId = `\0${virtualPackageJsonModuleId.replace(
/\./gu,
Expand All @@ -30,6 +34,7 @@ const resolveIds: Record<string, string | undefined> = {
[virtualESLintModuleId]: resolvedVirtualESLintModuleId,
[virtualLinterModuleId]: resolvedVirtualLinterModuleId,
[virtualSourceCodeModuleId]: resolvedVirtualSourceCodeModuleId,
[virtualRulesModuleId]: resolvedVirtualRulesModuleId,
[virtualPackageJsonModuleId]: resolvedVirtualPackageJsonModuleId,
[virtualUseAtYourOwnRiskModuleId]: resolvedVirtualUseAtYourOwnRiskModuleId,
};
Expand All @@ -48,10 +53,14 @@ export default {
};
`;

const virtualUseAtYourOwnRisk = `import { Linter } from '${virtualESLintModuleId}';
export const builtinRules = new Linter().getRules();
const virtualUseAtYourOwnRisk = `import rules from '${virtualRulesModuleId}';
export const builtinRules = rules;
export const LegacyESLint = class FakeLegacyESLint {}
export const FlatESLint = class FakeFlatESLint {}
export default {
builtinRules
builtinRules,
LegacyESLint,
FlatESLint
};
`;

Expand All @@ -72,6 +81,8 @@ export default function eslint4b(): VitePlugin {
"An eslint alias is specified but is ignored by vite-plugin-eslint4b.",
);
}
result.resolve.alias["eslint/use-at-your-own-risk"] =
virtualUseAtYourOwnRiskModuleId;
result.resolve.alias.eslint = virtualESLintModuleId;

if (!hasAlias("path")) {
Expand All @@ -98,7 +109,7 @@ export default function eslint4b(): VitePlugin {
return (configAlias as { [find: string]: string })[m] != null;
}
},
resolveId(source) {
resolveId(source, _importer) {
return resolveIds[source];
},
load(id, _options) {
Expand All @@ -117,13 +128,59 @@ export default function eslint4b(): VitePlugin {
if (id === resolvedVirtualUseAtYourOwnRiskModuleId) {
return virtualUseAtYourOwnRisk;
}
if (id === resolvedVirtualRulesModuleId) {
return buildRules();
}

return undefined;
},
};
}

export interface RequireESLintUseAtYourOwnRisk4bOptions {
include?: string | RegExp | (string | RegExp)[];
exclude?: string | RegExp | (string | RegExp)[];
}

export function requireESLintUseAtYourOwnRisk4b(options?: {
include?: string | RegExp | (string | RegExp)[];
exclude?: string | RegExp | (string | RegExp)[];
}): VitePlugin {
const filter = createFilter(
options?.include,
options?.exclude ?? [/(^|\/)node_modules\//u],
);
return {
name: "vite-plugin-require('eslint/use-at-your-own-risk')4b",
transform(code, id) {
const [filename] = id.split(`?`, 2);
if (!filter(filename)) return undefined;
const re = /require\s*\(\s*(["'])eslint\/use-at-your-own-risk\1\s*\)/gu;
if (re.test(code)) {
const ms = new MagicString(code);
ms.prepend(
`import { builtinRules as ___builtinRules___ } from '${virtualUseAtYourOwnRiskModuleId}';\n`,
);
re.lastIndex = 0;
ms.replaceAll(re, "{ builtinRules: ___builtinRules___ }");
return {
code: ms.toString(),
map: ms.generateMap(),
};
}
return undefined;
},
};
}

function requireResolved(targetPath: string) {
try {
return createRequire(
path.join(process.cwd(), "__placeholder__.js"),
).resolve(targetPath);
} catch {
// ignore
}
return createRequire(filename).resolve(targetPath);
}

Expand All @@ -133,7 +190,14 @@ function buildLinter() {
eslintPackageJsonPath,
"../lib/linter/linter.js",
);
return build(linterPath, ["path", "assert", "util"]);
const rulesPath = path.resolve(
eslintPackageJsonPath,
"../lib/rules/index.js",
);
const code = build(linterPath, ["path", "assert", "util"], {
[rulesPath]: virtualRulesModuleId,
});
return code;
}

function buildSourceCode() {
Expand All @@ -145,6 +209,15 @@ function buildSourceCode() {
return build(sourceCodePath, ["path"]);
}

function buildRules() {
const eslintPackageJsonPath = requireResolved("eslint/package.json");
const rulesPath = path.resolve(
eslintPackageJsonPath,
"../lib/rules/index.js",
);
return build(rulesPath, ["path"]);
}

function buildPackageJSON() {
const isId = /^[\p{ID_Start}$_][\p{ID_Continue}$\u200c\u200d]*$/u;

Expand All @@ -165,34 +238,63 @@ export default { ${defaultExports.join(", ")} };
`;
}

function build(input: string, externals: string[]) {
const bundledCode = bundle(input, externals);
return transform(bundledCode, externals);
function build(
input: string,
externals: string[],
alias: Record<string, string> = {},
) {
const bundledCode = bundle(input, externals, alias);
return transform(bundledCode, externals, alias);
}

function bundle(entryPoint: string, externals: string[]) {
function bundle(
entryPoint: string,
externals: string[],
alias: Record<string, string>,
) {
const external = [...externals, ...Object.keys(alias)];
const result = esbuild.buildSync({
entryPoints: [entryPoint],
format: "esm",
bundle: true,
external: externals,
external,
write: false,
inject: [path.join(dirname, "../shim/process-shim.mjs")],
});

return `${result.outputFiles[0].text}`;
}

function transform(code: string, injects: string[]) {
function transform(
code: string,
injects: string[],
alias: Record<string, string>,
) {
const injectSources: { id: string; source: string; module: string }[] = [
...injects.map((inject) => ({
id: `$inject_${inject.replace(/[^\w$]/giu, "_")}`,
source: inject,
module: inject,
})),
...Object.entries(alias).map(([module, source]) => ({
id: `$inject_${module.replace(/[^\w$]/giu, "_")}`,
source,
module,
})),
];

injectSources.forEach((s) => {
if (path.isAbsolute(s.module)) {
s.module = `./${path.relative(process.cwd(), s.module)}`;
}
});

return `
${injects
.map(
(inject) =>
`import $inject_${inject.replace(/-/g, "_")}$ from '${inject}';`,
)
${injectSources
.map(({ id, source }) => `import ${id} from '${source}';`)
.join("\n")}
const $_injects_$ = {${injects
.map((inject) => `${inject.replace(/-/g, "_")}:$inject_${inject}$`)
const $_injects_$ = {${injectSources
.map(({ id, module }) => `${JSON.stringify(module)}:${id}`)
.join(",\n")}};
function require(module, ...args) {
const mod = $_injects_$[module]
Expand Down
2 changes: 2 additions & 0 deletions tests/fixtures/build-test-v8/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
engine-strict=true
package-lock=false
26 changes: 26 additions & 0 deletions tests/fixtures/build-test-v8/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "vite-plugin-eslint4b-buildtest",
"private": true,
"version": "0.0.0",
"type": "module",
"exports": {
".": {
"import": "./dist/index.js"
},
"./package.json": "./package.json"
},
"dependencies": {
"@typescript-eslint/utils": "latest",
"@typescript-eslint/eslint-plugin": "latest",
"eslint": "^8.29.0",
"vite": "^4.0.0",
"vite-plugin-eslint4b": "file:../../.."
},
"scripts": {
"dev": "vite",
"build": "vite build"
},
"devDependencies": {
"@types/eslint": "^8.4.10"
}
}
27 changes: 27 additions & 0 deletions tests/fixtures/build-test-v8/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Linter } from "eslint";
// @ts-expect-error -- for test
import { name } from "eslint/package.json";
import * as TSESLintUtils from "@typescript-eslint/utils";
import * as TSESLintPlugin from "@typescript-eslint/eslint-plugin";

const linter = new Linter();

export function lint(): Linter.LintMessage[] {
return linter.verify("const a = 1", {
parserOptions: {
ecmaVersion: 2020,
},
rules: { semi: "error" },
});
}

export function getName(): string {
return name;
}

export { TSESLintUtils, TSESLintPlugin };

export function getRules(): Map<any, any> {
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires -- Test
return require("eslint/use-at-your-own-risk").builtinRules;
}
1 change: 1 addition & 0 deletions tests/fixtures/build-test-v8/src/shim/empty.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/* empty */

0 comments on commit a9bef96

Please sign in to comment.