tsserver-lean
Advanced tools
Comparing version 0.0.0-pre-alpha.4 to 0.0.0-pre-alpha.5
@@ -32,2 +32,3 @@ "use strict"; | ||
const path = __importStar(require("path")); | ||
const node_perf_hooks_1 = require("node:perf_hooks"); | ||
const config_1 = require("./config"); | ||
@@ -108,7 +109,12 @@ /** | ||
} | ||
const specifier = components[0].indexOf('@') > -1 && components.length > 2 ? true : components.length > 1; | ||
return { resolvedPath: require.resolve(importSpecifier), external: true, specifier }; | ||
// third-party | ||
// absolute paths can be specific (pkg-a/lib/p.ts), within the project, or global (pkg-a) | ||
// because, for now, we are only interested in getting the project's tsconfig, we can overlook trying to find the specific file path | ||
const specifier = components[0].indexOf('@') > -1 && components.length > 2 ? true : components.length > 1; | ||
try { | ||
return { resolvedPath: require.resolve(importSpecifier, { paths: [importeePath] }), external: true, specifier }; | ||
} | ||
catch (err) { | ||
return undefined; | ||
} | ||
} | ||
@@ -139,4 +145,6 @@ // we are just gathering imports for the target path, and not for the entire module graph dependency | ||
if (importSpecifiers.length > 0) { | ||
let i = 0; | ||
for (const importSpecifier of importSpecifiers) { | ||
const start = Date.now(); | ||
const percentComplete = getPercentComplete('resolve-dependency', i / importSpecifiers.length + 1); | ||
const start = node_perf_hooks_1.performance.now(); | ||
// ts.resolveModuleName takes surprisingly ~9 SECONDS (!) to resolve a single import | ||
@@ -146,5 +154,6 @@ const resolvedModule = fastResolve(tsProject, importSpecifier, filePath); | ||
logger.info({ | ||
phase: 'resolveDeps', | ||
phase: 'resolve-dependency', | ||
status: 'skip', | ||
message: `could not resolve ${importSpecifier} in ${filePath}`, | ||
percentComplete, | ||
}); | ||
@@ -154,6 +163,7 @@ } | ||
logger.info({ | ||
phase: 'resolveDeps', | ||
phase: 'resolve-dependency', | ||
status: 'skip', | ||
message: `${importSpecifier} is third-party`, | ||
elapsed: Date.now() - start, | ||
elapsed: node_perf_hooks_1.performance.now() - start, | ||
percentComplete, | ||
}); | ||
@@ -163,6 +173,7 @@ } | ||
logger.info({ | ||
phase: 'resolveDeps', | ||
phase: 'resolve-dependency', | ||
status: 'end', | ||
message: `resolved ${importSpecifier} to ${resolvedModule.resolvedPath}`, | ||
elapsed: Date.now() - start, | ||
elapsed: node_perf_hooks_1.performance.now() - start, | ||
percentComplete, | ||
}); | ||
@@ -184,2 +195,23 @@ if (!collectedImports.has(resolvedModule.resolvedPath)) { | ||
} | ||
function getPercentComplete(phase, percentageComplete) { | ||
// |==|==========|=====| | ||
// 0 10 70 100 | ||
// | ||
// module-resolution => 0-10% | ||
// dependency-building => 11-70% | ||
// type-checking => 71-100% | ||
const realRange = 1.0 - 0; | ||
const ranges = new Map([ | ||
['resolve-dependency', [0, 0.1]], | ||
['build-dependency', [0.1, 0.5]], | ||
['emit-dependency', [0.5, 0.7]], | ||
['type-check', [0.7, 1.0]], | ||
]); | ||
const range = ranges.get(phase); | ||
if (!range) { | ||
return undefined; | ||
} | ||
const [lower, upper] = range; | ||
return (upper - lower) * percentageComplete; | ||
} | ||
/** | ||
@@ -195,2 +227,34 @@ * Creates a ProjectService for a given project | ||
this.files = {}; | ||
/** | ||
* Compiles all project references, dependencies of a given project | ||
* to allow for accurate diagnostics | ||
* | ||
* Caveats: | ||
* - compiles and emit only projects that are __directly imported by target file__; the optimal | ||
* approach would be to compile only the _modules_ that are in the graph path | ||
* - does not explicitly compile and emit indirect project references | ||
* (e.g. A -> B -> C, where C is an indirect project reference); however, the language service | ||
* when generate the programs, will compile (but not emit) the indirect project references | ||
* - to do that, we would need to | ||
* 1) resolve specific import specifiers (easy) e.g `import {x} from 'foo/bar/baz/qux' would | ||
* only need to compile `foo/bar/baz/qux.ts` | ||
* 2) resolve module specifiers from global imports e.g `import {x} from 'foo'`, we would need to track | ||
* in `foo/index.ts`, which module defines and exports `x`, and import only that one. | ||
*/ | ||
this.isDeclaration = (p) => /.*\.d\.ts$/g.test(p); | ||
this.isValidSource = (p) => /.*\.(ts|tsx)$/g.test(p); | ||
this.shouldEmit = (source, outputFiles) => { | ||
const declarationFile = outputFiles.find((p) => p.endsWith('.d.ts')); | ||
if (!declarationFile) | ||
return false; | ||
try { | ||
const statDecl = fs.statSync(declarationFile); | ||
const statSource = fs.statSync(source); | ||
return statSource.mtime > statDecl.mtime; | ||
} | ||
catch (err) { | ||
// probably ENOENT | ||
return true; | ||
} | ||
}; | ||
this.documentRegistry = documentRegistry; | ||
@@ -235,18 +299,2 @@ this.languageServiceHost = this.createLanguageServiceHost(tsProject); | ||
} | ||
/** | ||
* Compiles all project references, dependencies of a given project | ||
* to allow for accurate diagnostics | ||
* | ||
* Caveats: | ||
* - compiles and emit only projects that are __directly imported by target file__; the optimal | ||
* approach would be to compile only the _modules_ that are in the graph path | ||
* - does not explicitly compile and emit indirect project references | ||
* (e.g. A -> B -> C, where C is an indirect project reference); however, the language service | ||
* when generate the programs, will compile (but not emit) the indirect project references | ||
* - to do that, we would need to | ||
* 1) resolve specific import specifiers (easy) e.g `import {x} from 'foo/bar/baz/qux' would | ||
* only need to compile `foo/bar/baz/qux.ts` | ||
* 2) resolve module specifiers from global imports e.g `import {x} from 'foo'`, we would need to track | ||
* in `foo/index.ts`, which module defines and exports `x`, and import only that one. | ||
*/ | ||
compileProjectReferences(targetPath, getTypescriptProject, getProjectService) { | ||
@@ -256,5 +304,4 @@ if (!this.tsProject.projectReferences || this.tsProject.projectReferences.length === 0) { | ||
} | ||
const start = Date.now(); | ||
const start = node_perf_hooks_1.performance.now(); | ||
const projectsQueue = []; | ||
const seenProjects = new Set(); | ||
const emittedFiles = new Set(); | ||
@@ -265,2 +312,3 @@ const emitPromises = []; | ||
// populate the queue | ||
let i = 0; | ||
for (const importSpecifier of resolvedImportSpecifiers) { | ||
@@ -274,2 +322,4 @@ const relevantProj = getTypescriptProject(importSpecifier.resolvedPath); // todo: could share this with TypeChecker | ||
while (projectsQueue.length > 0) { | ||
i++; | ||
const percentComplete = getPercentComplete('build-dependency', i / resolvedImportSpecifiers.length + 1); | ||
const cur = projectsQueue.shift(); | ||
@@ -279,9 +329,7 @@ if (!cur) | ||
const { project: referenceProject, filesToMaybeEmit } = cur; | ||
if (seenProjects.has(referenceProject.configPath)) { | ||
continue; | ||
} | ||
this.logger.info({ | ||
phase: 'buildDeps', | ||
phase: 'build-dependency', | ||
status: 'start', | ||
message: `building dependency ${referenceProject.configPath}...`, | ||
message: `building project ${referenceProject.configPath}...`, | ||
percentComplete, | ||
}); | ||
@@ -292,8 +340,7 @@ const filesToEmit = []; | ||
for (const fileToMaybeEmit of filesToMaybeEmit) { | ||
if (fileToMaybeEmit.endsWith('.d.ts') || !fileToMaybeEmit.endsWith('.ts')) { | ||
if (this.isDeclaration(fileToMaybeEmit) || !this.isValidSource(fileToMaybeEmit)) { | ||
continue; | ||
} | ||
const outputFiles = ts.getOutputFileNames(referenceProject.raw, fileToMaybeEmit, /*forceDtsPaths*/ true); | ||
const declarationOutput = outputFiles.find((p) => p.endsWith('.d.ts')); | ||
if (declarationOutput && !fs.existsSync(declarationOutput)) { | ||
if (this.shouldEmit(fileToMaybeEmit, outputFiles)) { | ||
filesToEmit.push(fileToMaybeEmit); | ||
@@ -307,2 +354,9 @@ continue; | ||
if (emittedFiles.has(fileName)) { | ||
this.logger.info({ | ||
phase: 'build-dependency', | ||
status: 'skip', | ||
message: `${referenceProject.configPath} has already been built`, | ||
elapsed: node_perf_hooks_1.performance.now() - start, | ||
percentComplete, | ||
}); | ||
continue; | ||
@@ -312,2 +366,9 @@ } | ||
emitPromises.push(projectServiceRef.emitAndCollectDiagnostics(fileName, false /* collect diagnostics */, true /* emit */)); | ||
this.logger.info({ | ||
phase: 'build-dependency', | ||
status: 'end', | ||
message: `${referenceProject.configPath} has already been built`, | ||
elapsed: node_perf_hooks_1.performance.now() - start, | ||
percentComplete, | ||
}); | ||
} | ||
@@ -317,16 +378,10 @@ } | ||
this.logger.info({ | ||
phase: 'buildDeps', | ||
phase: 'build-dependency', | ||
status: 'skip', | ||
message: `declaration files for ${referenceProject.configPath} are up to date`, | ||
elapsed: Date.now() - start, | ||
elapsed: node_perf_hooks_1.performance.now() - start, | ||
percentComplete, | ||
}); | ||
} | ||
seenProjects.add(referenceProject.configPath); | ||
} | ||
this.logger.info({ | ||
phase: 'buildDeps', | ||
status: 'end', | ||
message: `done building dependencies`, | ||
elapsed: Date.now() - start, | ||
}); | ||
return Promise.all(emitPromises); | ||
@@ -336,15 +391,12 @@ } | ||
return new Promise((resolve, reject) => { | ||
const start = Date.now(); | ||
const start = node_perf_hooks_1.performance.now(); | ||
if (forceEmit) { | ||
this.logger.info({ | ||
phase: 'buildDepsEmit', | ||
phase: 'emit-dependency', | ||
status: 'start', | ||
message: `emitting declaration files for ${fileName}...`, | ||
percentComplete: getPercentComplete('emit-dependency', 0), | ||
}); | ||
const output = this.languageService.getEmitOutput(fileName, true /* emitOnlyDtsFiles */, true /* forceDtsPaths */); | ||
// got an error while emitting so we need to force diagnostics | ||
// todo: need to re-enable this once we expose the emit command | ||
// if (output.emitSkipped) { | ||
// forceDiagnostics = true; | ||
// } | ||
// todo: need to account for output.emitSkipped once we expose the emit command | ||
output.outputFiles.forEach((o) => { | ||
@@ -357,3 +409,3 @@ try { | ||
this.logger.err({ | ||
phase: 'buildDepsEmit', | ||
phase: 'emit-dependency', | ||
message: `failed to emit ${o.name}: ${JSON.stringify(e)}}`, | ||
@@ -364,6 +416,7 @@ }); | ||
this.logger.info({ | ||
phase: 'buildDepsEmit', | ||
phase: 'emit-dependency', | ||
status: 'end', | ||
elapsed: Date.now() - start, | ||
elapsed: node_perf_hooks_1.performance.now() - start, | ||
message: `done emitting declaration files for ${fileName}`, | ||
percentComplete: getPercentComplete('emit-dependency', 1.0), | ||
}); | ||
@@ -374,9 +427,9 @@ } | ||
this.logger.info({ | ||
phase: 'semanticDiag', | ||
phase: 'type-check', | ||
status: 'start', | ||
message: `getting diagnostics for ${fileName}...`, | ||
message: `requesting semantic & syntactic analysis for ${fileName}...`, | ||
percentComplete: getPercentComplete('type-check', 0), | ||
}); | ||
const startd = Date.now(); | ||
const startd = node_perf_hooks_1.performance.now(); | ||
const allDiagnostics = [ | ||
// ...this.languageService.getCompilerOptionsDiagnostics(), | ||
...this.languageService.getSyntacticDiagnostics(fileName), | ||
@@ -386,6 +439,7 @@ ...this.languageService.getSemanticDiagnostics(fileName), | ||
this.logger.info({ | ||
phase: 'semanticDiag', | ||
phase: 'type-check', | ||
status: 'end', | ||
elapsed: Date.now() - startd, | ||
elapsed: node_perf_hooks_1.performance.now() - startd, | ||
message: `done`, | ||
percentComplete: getPercentComplete('type-check', 1.0), | ||
}); | ||
@@ -410,7 +464,18 @@ emitCallback(allDiagnostics, version); | ||
mapInternalDiagnostics(fileName, diagnostics, version, elapsed) { | ||
const ignoredMessagesRe = [/Cannot find module/]; | ||
const diagnosticsRaw = diagnostics.filter((d) => !!d.file); | ||
const diagnosticsInternal = []; | ||
for (const diagnostic of diagnosticsRaw) { | ||
// Ignoring "Cannot find module 'third-party'. Did you mean to set 'moduleResolution'..." | ||
// because that's a message that only shows up in dev mode, when launching an npm-linked `tsserver-lean` | ||
// Being tackled on #703 | ||
const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); | ||
if (!diagnostic.file) | ||
let shouldIgnore = false; | ||
for (const ignoreRe of ignoredMessagesRe) { | ||
if (ignoreRe.test(message)) { | ||
shouldIgnore = true; | ||
break; | ||
} | ||
} | ||
if (shouldIgnore || !diagnostic.file) | ||
continue; | ||
@@ -449,3 +514,3 @@ const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); | ||
async getDiagnostics(targetPath, callback) { | ||
const start = Date.now(); | ||
const start = node_perf_hooks_1.performance.now(); | ||
const targetPathAbs = path.resolve(targetPath); | ||
@@ -457,3 +522,3 @@ // get project service | ||
.then(() => { | ||
return projectService.emitAndCollectDiagnostics(targetPathAbs, true /* forceDiagnostics */, false /* forceEmit */, (diagnostics, version) => callback(this.mapInternalDiagnostics(targetPath, diagnostics, version, Date.now() - start))); | ||
return projectService.emitAndCollectDiagnostics(targetPathAbs, true /* forceDiagnostics */, false /* forceEmit */, (diagnostics, version) => callback(this.mapInternalDiagnostics(targetPath, diagnostics, version, node_perf_hooks_1.performance.now() - start))); | ||
}); | ||
@@ -460,0 +525,0 @@ } |
@@ -38,2 +38,5 @@ "use strict"; | ||
async requestDiagnostics(request, requestSeq) { | ||
if (!request.arguments) { | ||
this.logger.err({ phase: 'request', message: `Invalid 'geterr' request. Missing 'arguments' property` }); | ||
} | ||
const { files } = request.arguments; | ||
@@ -40,0 +43,0 @@ if (files.length === 0) { |
@@ -13,3 +13,3 @@ { | ||
"sideEffects": false, | ||
"version": "0.0.0-pre-alpha.4", | ||
"version": "0.0.0-pre-alpha.5", | ||
"exports": { | ||
@@ -16,0 +16,0 @@ ".": { |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
No README
QualityPackage does not have a README. This may indicate a failed publish or a low quality package.
Found 1 instance in 1 package
331744
15
843
1
33