parallelize
This commit is contained in:
@@ -27,6 +27,7 @@ const {parse} = require('@babel/parser')
|
||||
const fs = require('node:fs')
|
||||
const os = require('node:os')
|
||||
const path = require('node:path')
|
||||
const {Worker} = require('node:worker_threads')
|
||||
const ts = require('typescript')
|
||||
|
||||
const plugin = require('../babel-plugin-lexicon-leaf-imports')
|
||||
@@ -526,7 +527,7 @@ describe('app callsites: transformed sources typecheck', () => {
|
||||
*/
|
||||
test(
|
||||
'every file importing the barrel',
|
||||
() => {
|
||||
async () => {
|
||||
const consumers = []
|
||||
;(function walk(dir) {
|
||||
for (const entry of fs.readdirSync(dir, {withFileTypes: true})) {
|
||||
@@ -605,23 +606,36 @@ describe('app callsites: transformed sources typecheck', () => {
|
||||
|
||||
const options = loadAppCompilerOptions()
|
||||
|
||||
/*
|
||||
* The baseline and shadow typechecks are independent CPU-bound
|
||||
* programs, so each runs in its own worker thread and the two proceed
|
||||
* in parallel. The worker returns diagnostics as plain records (see
|
||||
* lexiconTypecheckWorker.js).
|
||||
*/
|
||||
function diagnose(overlays) {
|
||||
const host = createOverlayHost(options, overlays)
|
||||
const program = ts.createProgram(consumers, options, host)
|
||||
const byFile = new Map()
|
||||
for (const file of consumers) {
|
||||
const sf = program.getSourceFile(file)
|
||||
if (!sf) throw new Error(`${file} missing from program`)
|
||||
byFile.set(file, fileDiagnostics(program, sf))
|
||||
}
|
||||
return byFile
|
||||
return new Promise((resolve, reject) => {
|
||||
const worker = new Worker(
|
||||
path.join(__dirname, '..', 'lexiconTypecheckWorker.js'),
|
||||
{workerData: {consumers, overlays, options}},
|
||||
)
|
||||
worker.once('message', byFile =>
|
||||
resolve(new Map(Object.entries(byFile))),
|
||||
)
|
||||
worker.once('error', reject)
|
||||
worker.once('exit', code => {
|
||||
if (code !== 0) {
|
||||
reject(new Error(`typecheck worker exited with code ${code}`))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const baseline = diagnose(baselineOverlays)
|
||||
const shadow = diagnose(shadowOverlays)
|
||||
const [baseline, shadow] = await Promise.all([
|
||||
diagnose(baselineOverlays),
|
||||
diagnose(shadowOverlays),
|
||||
])
|
||||
|
||||
const diagKey = d =>
|
||||
`TS${d.code}: ${ts.flattenDiagnosticMessageText(d.messageText, ' ')}`
|
||||
const diagKey = d => `TS${d.code}: ${d.message}`
|
||||
const regressions = []
|
||||
for (const file of consumers) {
|
||||
const known = new Set(baseline.get(file).map(diagKey))
|
||||
@@ -630,8 +644,16 @@ describe('app callsites: transformed sources typecheck', () => {
|
||||
}
|
||||
}
|
||||
if (regressions.length > 0) {
|
||||
const details = regressions
|
||||
.slice(0, 20)
|
||||
.map(d =>
|
||||
d.fileName
|
||||
? `${d.fileName}:${d.line} TS${d.code}: ${d.message}`
|
||||
: `TS${d.code}: ${d.message}`,
|
||||
)
|
||||
.join('\n')
|
||||
throw new Error(
|
||||
`plugin introduced diagnostics in app sources:\n${formatDiagnostics(regressions)}`,
|
||||
`plugin introduced diagnostics in app sources:\n${details}`,
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Worker thread for the "app callsites" test in
|
||||
* __tests__/babel-plugin-lexicon-leaf-imports.test.js: typechecks the
|
||||
* consumer files with the given overlay contents and reports error
|
||||
* diagnostics. The baseline and shadow typechecks are independent CPU-bound
|
||||
* programs, so the test runs one worker for each in parallel.
|
||||
*
|
||||
* Lives outside __tests__/ so Jest does not collect it as a test suite.
|
||||
* ts.Diagnostic objects do not survive structured clone, so diagnostics are
|
||||
* flattened to plain {code, message, fileName, line} records here.
|
||||
*/
|
||||
const {parentPort, workerData} = require('node:worker_threads')
|
||||
const ts = require('typescript')
|
||||
|
||||
const {consumers, overlays, options} = workerData
|
||||
|
||||
const host = ts.createCompilerHost(options)
|
||||
const origGetSourceFile = host.getSourceFile.bind(host)
|
||||
const origFileExists = host.fileExists.bind(host)
|
||||
const origReadFile = host.readFile.bind(host)
|
||||
host.fileExists = f => overlays.has(f) || origFileExists(f)
|
||||
host.readFile = f => overlays.get(f) ?? origReadFile(f)
|
||||
host.getSourceFile = (f, lang, ...rest) =>
|
||||
overlays.has(f)
|
||||
? ts.createSourceFile(f, overlays.get(f), lang)
|
||||
: origGetSourceFile(f, lang, ...rest)
|
||||
|
||||
const program = ts.createProgram(consumers, options, host)
|
||||
const byFile = {}
|
||||
for (const file of consumers) {
|
||||
const sf = program.getSourceFile(file)
|
||||
if (!sf) throw new Error(`${file} missing from program`)
|
||||
byFile[file] = [
|
||||
...program.getSyntacticDiagnostics(sf),
|
||||
...program.getSemanticDiagnostics(sf),
|
||||
]
|
||||
.filter(d => d.category === ts.DiagnosticCategory.Error)
|
||||
.map(d => ({
|
||||
code: d.code,
|
||||
message: ts.flattenDiagnosticMessageText(d.messageText, ' '),
|
||||
fileName: d.file?.fileName,
|
||||
line:
|
||||
d.file && d.start !== undefined
|
||||
? d.file.getLineAndCharacterOfPosition(d.start).line + 1
|
||||
: undefined,
|
||||
}))
|
||||
}
|
||||
parentPort.postMessage(byFile)
|
||||
Reference in New Issue
Block a user