diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index c9b501be..d3f8df9a 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -1,7 +1,7 @@ name: CI on: push: - branches: [master] + branches: [master, try] pull_request: branches: [master] schedule: @@ -21,7 +21,7 @@ jobs: with: node-version: 14 - name: Install Packages - run: npm install + run: npm install && cd test/fixtures/eslint && npm install - name: Lint run: npm run -s lint diff --git a/src/script/espree.ts b/src/script/espree.ts new file mode 100644 index 00000000..4e25d07b --- /dev/null +++ b/src/script/espree.ts @@ -0,0 +1,69 @@ +import Module from "module" +import path from "path" +import { ESLintExtendedProgram, ESLintProgram } from "../ast" + +/** + * The interface of a result of ESLint custom parser. + */ +export type ESLintCustomParserResult = ESLintProgram | ESLintExtendedProgram + +/** + * The interface of ESLint custom parsers. + */ +export interface ESLintCustomParser { + parse(code: string, options: any): ESLintCustomParserResult + parseForESLint?(code: string, options: any): ESLintCustomParserResult +} + +const createRequire: (filename: string) => (filename: string) => any = + // Added in v12.2.0 + (Module as any).createRequire || + // Added in v10.12.0, but deprecated in v12.2.0. + Module.createRequireFromPath || + // Polyfill - This is not executed on the tests on node@>=10. + /* istanbul ignore next */ + (filename => { + const mod = new Module(filename) + + mod.filename = filename + mod.paths = (Module as any)._nodeModulePaths(path.dirname(filename)) + ;(mod as any)._compile("module.exports = require;", filename) + return mod.exports + }) + +let espreeCache: ESLintCustomParser | null = null + +function isLinterPath(p: string): boolean { + return ( + // ESLint 6 and above + p.includes( + `eslint${path.sep}lib${path.sep}linter${path.sep}linter.js`, + ) || + // ESLint 5 + p.includes(`eslint${path.sep}lib${path.sep}linter.js`) + ) +} + +/** + * Load `espree` from the loaded ESLint. + * If the loaded ESLint was not found, just returns `require("espree")`. + */ +export function getEspree(): ESLintCustomParser { + if (!espreeCache) { + // Lookup the loaded eslint + const linterPath = Object.keys(require.cache).find(isLinterPath) + if (linterPath) { + try { + espreeCache = createRequire(linterPath)("espree") + } catch { + // ignore + } + } + if (!espreeCache) { + //eslint-disable-next-line @mysticatea/ts/no-require-imports + espreeCache = require("espree") + } + } + + return espreeCache! +} diff --git a/src/script/index.ts b/src/script/index.ts index c5d9e2f8..bfa9100f 100644 --- a/src/script/index.ts +++ b/src/script/index.ts @@ -17,7 +17,6 @@ import { ESLintForOfStatement, ESLintFunctionExpression, ESLintPattern, - ESLintProgram, ESLintVariableDeclaration, ESLintUnaryExpression, HasLocation, @@ -39,6 +38,7 @@ import { analyzeExternalReferences, analyzeVariablesAndExternalReferences, } from "./scope-analyzer" +import { ESLintCustomParser, getEspree } from "./espree" // [1] = spacing before the aliases. // [2] = aliases. @@ -51,14 +51,6 @@ const DUMMY_PARENT: any = {} const IS_FUNCTION_EXPRESSION = /^\s*([\w$_]+|\([^)]*?\))\s*=>|^function\s*\(/u const IS_SIMPLE_PATH = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['[^']*?'\]|\["[^"]*?"\]|\[\d+\]|\[[A-Za-z_$][\w$]*\])*$/u -/** - * The interface of ESLint custom parsers. - */ -interface ESLintCustomParser { - parse(code: string, options: any): ESLintCustomParserResult - parseForESLint?(code: string, options: any): ESLintCustomParserResult -} - /** * Do post-process of parsing an expression. * @@ -548,11 +540,6 @@ export interface ExpressionParseResult { variables: Variable[] } -/** - * The interface of a result of ESLint custom parser. - */ -export type ESLintCustomParserResult = ESLintProgram | ESLintExtendedProgram - /** * Parse the given source code. * @@ -568,8 +555,7 @@ export function parseScript( typeof parserOptions.parser === "string" ? // eslint-disable-next-line @mysticatea/ts/no-require-imports require(parserOptions.parser) - : // eslint-disable-next-line @mysticatea/ts/no-require-imports - require("espree") + : getEspree() const result: any = // eslint-disable-next-line @mysticatea/ts/unbound-method typeof parser.parseForESLint === "function" diff --git a/test/espree.js b/test/espree.js new file mode 100644 index 00000000..284da5dd --- /dev/null +++ b/test/espree.js @@ -0,0 +1,60 @@ +"use strict" + +const path = require("path") + +/** + * Spawn a child process to run `childMain()`. + */ +function parentMain() { + const { spawn } = require("child_process") + + describe("Loading espree from ESLint", () => { + it("should load espree from the ESLint location.", done => { + spawn(process.execPath, [__filename, "--child"], { + stdio: "inherit", + }) + .on("error", done) + .on("exit", code => + code + ? done(new Error(`Exited with non-zero: ${code}`)) + : done() + ) + }) + }) +} + +/** + * Check this parser loads the `espree` from the location of the loaded ESLint. + */ +function childMain() { + const assert = require("assert") + const { Linter } = require("./fixtures/eslint") + const linter = new Linter() + linter.defineParser("vue-eslint-parser", require("../src")) + + const beforeEsprees = Object.keys(require.cache).filter(isEspreePath) + + linter.verify( + "", + { parser: "vue-eslint-parser" }, + { filename: "a.vue" } + ) + + const afterEsprees = Object.keys(require.cache).filter(isEspreePath) + + assert.strictEqual( + afterEsprees.length, + beforeEsprees.length, + "espree should be loaded from the expected place" + ) +} + +function isEspreePath(p) { + return p.includes(`${path.sep}node_modules${path.sep}espree${path.sep}`) +} + +if (process.argv.includes("--child")) { + childMain() +} else { + parentMain() +}