run single typecheck in the plugin test
This commit is contained in:
@@ -27,7 +27,6 @@ const {parse} = require('@babel/parser')
|
|||||||
const fs = require('node:fs')
|
const fs = require('node:fs')
|
||||||
const os = require('node:os')
|
const os = require('node:os')
|
||||||
const path = require('node:path')
|
const path = require('node:path')
|
||||||
const {Worker} = require('node:worker_threads')
|
|
||||||
const ts = require('typescript')
|
const ts = require('typescript')
|
||||||
|
|
||||||
const plugin = require('..')
|
const plugin = require('..')
|
||||||
@@ -530,7 +529,7 @@ describe('app callsites: transformed sources typecheck', () => {
|
|||||||
*/
|
*/
|
||||||
test(
|
test(
|
||||||
'every file importing the barrel',
|
'every file importing the barrel',
|
||||||
async () => {
|
() => {
|
||||||
const consumers = []
|
const consumers = []
|
||||||
;(function walk(dir) {
|
;(function walk(dir) {
|
||||||
for (const entry of fs.readdirSync(dir, {withFileTypes: true})) {
|
for (const entry of fs.readdirSync(dir, {withFileTypes: true})) {
|
||||||
@@ -598,8 +597,7 @@ describe('app callsites: transformed sources typecheck', () => {
|
|||||||
/*
|
/*
|
||||||
* Files whose shadow output is byte-identical to the baseline output
|
* Files whose shadow output is byte-identical to the baseline output
|
||||||
* cannot produce a diagnostics diff, so only changed files are overlaid,
|
* cannot produce a diagnostics diff, so only changed files are overlaid,
|
||||||
* typechecked, and compared - a bit under half of the consumers, which
|
* typechecked, and compared - a bit under half of the consumers.
|
||||||
* roughly halves the cost of the two programs below.
|
|
||||||
*/
|
*/
|
||||||
const shadowOverlays = new Map()
|
const shadowOverlays = new Map()
|
||||||
const baselineOverlays = new Map()
|
const baselineOverlays = new Map()
|
||||||
@@ -620,53 +618,50 @@ describe('app callsites: transformed sources typecheck', () => {
|
|||||||
const options = loadAppCompilerOptions()
|
const options = loadAppCompilerOptions()
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* The baseline and shadow typechecks are independent CPU-bound
|
* One program holds both versions of every changed consumer: the
|
||||||
* programs, so each runs in its own worker thread and the two proceed
|
* baseline at the file's real path and the shadow at a virtual
|
||||||
* in parallel. The worker returns diagnostics as plain records (see
|
* `.__shadow__.` sibling path - same directory and extension, so every
|
||||||
* lexiconTypecheckWorker.js).
|
* relative/aliased import and platform-extension resolution behaves as
|
||||||
|
* it would from the real file. The two versions differ only in these
|
||||||
|
* consumer files; everything they import resolves to the same modules.
|
||||||
|
* A single program therefore parses and binds the shared app +
|
||||||
|
* node_modules closure once, where separate baseline and shadow
|
||||||
|
* programs would each redo it - that closure, not checking the roots,
|
||||||
|
* dominates the cost.
|
||||||
*/
|
*/
|
||||||
function diagnose(overlays) {
|
const shadowPath = f => f.replace(/\.(tsx?)$/, '.__shadow__.$1')
|
||||||
return new Promise((resolve, reject) => {
|
const overlays = new Map()
|
||||||
const worker = new Worker(
|
for (const file of changed) {
|
||||||
path.join(__dirname, '..', 'lexiconTypecheckWorker.js'),
|
overlays.set(file, baselineOverlays.get(file))
|
||||||
{workerData: {consumers: changed, overlays, options}},
|
overlays.set(shadowPath(file), shadowOverlays.get(file))
|
||||||
)
|
|
||||||
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 host = createOverlayHost(options, overlays)
|
||||||
|
const program = ts.createProgram([...overlays.keys()], options, host)
|
||||||
|
|
||||||
const [baseline, shadow] = await Promise.all([
|
/*
|
||||||
diagnose(baselineOverlays),
|
* Shadow-file diagnostics may spell the virtual path inside messages;
|
||||||
diagnose(shadowOverlays),
|
* normalize it away so a diagnostic differing only in that spelling
|
||||||
])
|
* does not count as a regression.
|
||||||
|
*/
|
||||||
const diagKey = d => `TS${d.code}: ${d.message}`
|
const diagKey = d =>
|
||||||
|
`TS${d.code}: ${ts
|
||||||
|
.flattenDiagnosticMessageText(d.messageText, ' ')
|
||||||
|
.replaceAll('.__shadow__.', '.')}`
|
||||||
const regressions = []
|
const regressions = []
|
||||||
for (const file of changed) {
|
for (const file of changed) {
|
||||||
const known = new Set(baseline.get(file).map(diagKey))
|
const baselineSf = program.getSourceFile(file)
|
||||||
for (const d of shadow.get(file)) {
|
const shadowSf = program.getSourceFile(shadowPath(file))
|
||||||
|
if (!baselineSf || !shadowSf) {
|
||||||
|
throw new Error(`${file} missing from program`)
|
||||||
|
}
|
||||||
|
const known = new Set(fileDiagnostics(program, baselineSf).map(diagKey))
|
||||||
|
for (const d of fileDiagnostics(program, shadowSf)) {
|
||||||
if (!known.has(diagKey(d))) regressions.push(d)
|
if (!known.has(diagKey(d))) regressions.push(d)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (regressions.length > 0) {
|
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(
|
throw new Error(
|
||||||
`plugin introduced diagnostics in app sources:\n${details}`,
|
`plugin introduced diagnostics in app sources:\n${formatDiagnostics(regressions)}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
/*
|
|
||||||
* Worker thread for the "app callsites" test in
|
|
||||||
* __tests__/lexiconLeafImports.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