New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Expose @babel/eslint-parser/experimental-worker
#13398
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,67 +1,105 @@ | ||
const path = require("path"); | ||
|
||
let send; | ||
|
||
exports.getVersion = sendCached("GET_VERSION"); | ||
|
||
exports.getTypesInfo = sendCached("GET_TYPES_INFO"); | ||
|
||
exports.getVisitorKeys = sendCached("GET_VISITOR_KEYS"); | ||
|
||
exports.getTokLabels = sendCached("GET_TOKEN_LABELS"); | ||
|
||
exports.maybeParse = (code, options) => send("MAYBE_PARSE", { code, options }); | ||
|
||
function sendCached(action) { | ||
let cache = null; | ||
|
||
return () => { | ||
if (!cache) cache = send(action, undefined); | ||
return cache; | ||
}; | ||
const ACTIONS = { | ||
GET_VERSION: "GET_VERSION", | ||
GET_TYPES_INFO: "GET_TYPES_INFO", | ||
GET_VISITOR_KEYS: "GET_VISITOR_KEYS", | ||
GET_TOKEN_LABELS: "GET_TOKEN_LABELS", | ||
MAYBE_PARSE: "MAYBE_PARSE", | ||
MAYBE_PARSE_SYNC: "MAYBE_PARSE_SYNC", | ||
}; | ||
|
||
class Client { | ||
#send; | ||
|
||
constructor(send) { | ||
this.#send = send; | ||
} | ||
|
||
#vCache; | ||
getVersion() { | ||
return (this.#vCache ??= this.#send(ACTIONS.GET_VERSION, undefined)); | ||
} | ||
|
||
#tiCache; | ||
getTypesInfo() { | ||
return (this.#tiCache ??= this.#send(ACTIONS.GET_TYPES_INFO, undefined)); | ||
} | ||
|
||
#vkCache; | ||
getVisitorKeys() { | ||
return (this.#vkCache ??= this.#send(ACTIONS.GET_VISITOR_KEYS, undefined)); | ||
} | ||
|
||
#tlCache; | ||
getTokLabels() { | ||
return (this.#tlCache ??= this.#send(ACTIONS.GET_TOKEN_LABELS, undefined)); | ||
} | ||
|
||
maybeParse(code, options) { | ||
return this.#send(ACTIONS.MAYBE_PARSE, { code, options }); | ||
} | ||
} | ||
|
||
if (process.env.BABEL_8_BREAKING) { | ||
const { | ||
Worker, | ||
receiveMessageOnPort, | ||
MessageChannel, | ||
SHARE_ENV, | ||
} = require("worker_threads"); | ||
|
||
// We need to run Babel in a worker for two reasons: | ||
// 1. ESLint workers must be CJS files, and this is a problem | ||
// since Babel 8+ uses native ESM | ||
// 2. ESLint parsers must run synchronously, but many steps | ||
// of Babel's config loading (which is done for each file) | ||
// can be asynchronous | ||
// If ESLint starts supporting async parsers, we can move | ||
// everything back to the main thread. | ||
const worker = new Worker( | ||
// We need to run Babel in a worker for two reasons: | ||
// 1. ESLint workers must be CJS files, and this is a problem | ||
nicolo-ribaudo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// since Babel 8+ uses native ESM | ||
// 2. ESLint parsers must run synchronously, but many steps | ||
// of Babel's config loading (which is done for each file) | ||
// can be asynchronous | ||
// If ESLint starts supporting async parsers, we can move | ||
// everything back to the main thread. | ||
exports.WorkerClient = class WorkerClient extends Client { | ||
static #worker_threads_cache; | ||
static get #worker_threads() { | ||
return (WorkerClient.#worker_threads_cache ??= require("worker_threads")); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Q: What's the intention of private static getter? Doesn't There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This file is always executed, but |
||
|
||
#worker = new WorkerClient.#worker_threads.Worker( | ||
path.resolve(__dirname, "../lib/worker/index.cjs"), | ||
{ env: SHARE_ENV }, | ||
{ env: WorkerClient.#worker_threads.SHARE_ENV }, | ||
); | ||
|
||
// The worker will never exit by itself. Prevent it from keeping | ||
// the main process alive. | ||
worker.unref(); | ||
|
||
const signal = new Int32Array(new SharedArrayBuffer(4)); | ||
|
||
send = (action, payload) => { | ||
signal[0] = 0; | ||
const subChannel = new MessageChannel(); | ||
|
||
worker.postMessage({ signal, port: subChannel.port1, action, payload }, [ | ||
subChannel.port1, | ||
]); | ||
|
||
Atomics.wait(signal, 0, 0); | ||
const { message } = receiveMessageOnPort(subChannel.port2); | ||
|
||
if (message.error) throw Object.assign(message.error, message.errorData); | ||
else return message.result; | ||
#signal = new Int32Array(new SharedArrayBuffer(4)); | ||
|
||
constructor() { | ||
super((action, payload) => { | ||
this.#signal[0] = 0; | ||
const subChannel = new WorkerClient.#worker_threads.MessageChannel(); | ||
|
||
this.#worker.postMessage( | ||
{ signal: this.#signal, port: subChannel.port1, action, payload }, | ||
[subChannel.port1], | ||
); | ||
|
||
Atomics.wait(this.#signal, 0, 0); | ||
const { message } = WorkerClient.#worker_threads.receiveMessageOnPort( | ||
subChannel.port2, | ||
); | ||
|
||
if (message.error) throw Object.assign(message.error, message.errorData); | ||
else return message.result; | ||
}); | ||
|
||
// The worker will never exit by itself. Prevent it from keeping | ||
// the main process alive. | ||
this.#worker.unref(); | ||
} | ||
}; | ||
|
||
if (!process.env.BABEL_8_BREAKING) { | ||
exports.LocalClient = class LocalClient extends Client { | ||
static #handleMessage; | ||
|
||
constructor() { | ||
LocalClient.#handleMessage ??= require("./worker/handle-message.cjs"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ditto. |
||
|
||
super((action, payload) => { | ||
return LocalClient.#handleMessage( | ||
action === ACTIONS.MAYBE_PARSE ? ACTIONS.MAYBE_PARSE_SYNC : action, | ||
payload, | ||
); | ||
}); | ||
} | ||
}; | ||
} else { | ||
send = require("./worker/index.cjs"); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
const [major, minor] = process.versions.node.split(".").map(Number); | ||
|
||
if (major < 12 || (major === 12 && minor < 3)) { | ||
throw new Error( | ||
"@babel/eslint-parser/experimental-worker requires Node.js >= 12.3.0", | ||
); | ||
} | ||
|
||
const { normalizeESLintConfig } = require("./configuration.cjs"); | ||
const analyzeScope = require("./analyze-scope.cjs"); | ||
const baseParse = require("./parse.cjs"); | ||
|
||
const { WorkerClient } = require("./client.cjs"); | ||
const client = new WorkerClient(); | ||
|
||
exports.parseForESLint = function (code, options = {}) { | ||
const normalizedOptions = normalizeESLintConfig(options); | ||
const ast = baseParse(code, normalizedOptions, client); | ||
const scopeManager = analyzeScope(ast, normalizedOptions, client); | ||
|
||
return { ast, scopeManager, visitorKeys: client.getVisitorKeys() }; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,63 +1,20 @@ | ||
const semver = require("semver"); | ||
const { normalizeESLintConfig } = require("./configuration.cjs"); | ||
const analyzeScope = require("./analyze-scope.cjs"); | ||
const { | ||
getVersion, | ||
getVisitorKeys, | ||
getTokLabels, | ||
maybeParse, | ||
} = require("./client.cjs"); | ||
const convert = require("./convert/index.cjs"); | ||
const baseParse = require("./parse.cjs"); | ||
|
||
const babelParser = require(require.resolve("@babel/parser", { | ||
paths: [require.resolve("@babel/core/package.json")], | ||
})); | ||
|
||
let isRunningMinSupportedCoreVersion = null; | ||
|
||
function baseParse(code, options) { | ||
// Ensure we're using a version of `@babel/core` that includes `parse()` and `tokTypes`. | ||
const minSupportedCoreVersion = ">=7.2.0"; | ||
|
||
if (typeof isRunningMinSupportedCoreVersion !== "boolean") { | ||
isRunningMinSupportedCoreVersion = semver.satisfies( | ||
getVersion(), | ||
minSupportedCoreVersion, | ||
); | ||
} | ||
|
||
if (!isRunningMinSupportedCoreVersion) { | ||
throw new Error( | ||
`@babel/eslint-parser@${ | ||
PACKAGE_JSON.version | ||
} does not support @babel/core@${getVersion()}. Please upgrade to @babel/core@${minSupportedCoreVersion}.`, | ||
); | ||
} | ||
|
||
const { ast, parserOptions } = maybeParse(code, options); | ||
|
||
if (ast) return ast; | ||
|
||
try { | ||
return convert.ast( | ||
babelParser.parse(code, parserOptions), | ||
code, | ||
getTokLabels(), | ||
getVisitorKeys(), | ||
); | ||
} catch (err) { | ||
throw convert.error(err); | ||
} | ||
} | ||
const { LocalClient, WorkerClient } = require("./client.cjs"); | ||
const client = new ( | ||
process.env.BABEL_8_BREAKING ? WorkerClient : LocalClient | ||
)(); | ||
|
||
exports.parse = function (code, options = {}) { | ||
return baseParse(code, normalizeESLintConfig(options)); | ||
return baseParse(code, normalizeESLintConfig(options), client); | ||
}; | ||
|
||
exports.parseForESLint = function (code, options = {}) { | ||
const normalizedOptions = normalizeESLintConfig(options); | ||
const ast = baseParse(code, normalizedOptions); | ||
const scopeManager = analyzeScope(ast, normalizedOptions); | ||
const ast = baseParse(code, normalizedOptions, client); | ||
const scopeManager = analyzeScope(ast, normalizedOptions, client); | ||
|
||
return { ast, scopeManager, visitorKeys: getVisitorKeys() }; | ||
return { ast, scopeManager, visitorKeys: client.getVisitorKeys() }; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
"use strict"; | ||
|
||
const semver = require("semver"); | ||
const convert = require("./convert/index.cjs"); | ||
|
||
const babelParser = require(require.resolve("@babel/parser", { | ||
paths: [require.resolve("@babel/core/package.json")], | ||
})); | ||
|
||
let isRunningMinSupportedCoreVersion = null; | ||
|
||
module.exports = function parse(code, options, client) { | ||
// Ensure we're using a version of `@babel/core` that includes `parse()` and `tokTypes`. | ||
const minSupportedCoreVersion = ">=7.2.0"; | ||
|
||
if (typeof isRunningMinSupportedCoreVersion !== "boolean") { | ||
isRunningMinSupportedCoreVersion = semver.satisfies( | ||
client.getVersion(), | ||
minSupportedCoreVersion, | ||
); | ||
} | ||
|
||
if (!isRunningMinSupportedCoreVersion) { | ||
throw new Error( | ||
`@babel/eslint-parser@${ | ||
PACKAGE_JSON.version | ||
} does not support @babel/core@${client.getVersion()}. Please upgrade to @babel/core@${minSupportedCoreVersion}.`, | ||
); | ||
} | ||
|
||
const { ast, parserOptions } = client.maybeParse(code, options); | ||
|
||
if (ast) return ast; | ||
|
||
try { | ||
return convert.ast( | ||
babelParser.parse(code, parserOptions), | ||
code, | ||
client.getTokLabels(), | ||
client.getVisitorKeys(), | ||
); | ||
} catch (err) { | ||
throw convert.error(err); | ||
} | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I reorganized this file so that it can both run Babel using
worker_threads
or in the same thread. TheClient
class is the "public interface", while theWorkerClient
andLocalClient
provide the communication implementation.