Description
In a project with about 20MB of Javascript / Typescript code, opening the second file will reliably trigger an error looking like this:
Error: Could not find source file: '/path/file1.ts'.
at getValidSourceFile (/path/node_modules/typescript/lib/tsserver.js:143801:22)
at Object.getEncodedSemanticClassifications3 [as getEncodedSemanticClassifications] (/path/node_modules/typescript/lib/tsserver.js:144328:77)
at IpcIOSession.getEncodedSemanticClassifications (/path/node_modules/typescript/lib/tsserver.js:183203:41)
at encodedSemanticClassifications-full (/path/node_modules/typescript/lib/tsserver.js:182505:43)
at /path/node_modules/typescript/lib/tsserver.js:184838:69
at IpcIOSession.executeWithRequestId (/path/node_modules/typescript/lib/tsserver.js:184830:14)
at IpcIOSession.executeCommand (/path/node_modules/typescript/lib/tsserver.js:184838:29)
at IpcIOSession.onMessage (/path/node_modules/typescript/lib/tsserver.js:184880:51)
at process.<anonymous> (/path/node_modules/typescript/lib/tsserver.js:186461:14)
at process.emit (node:events:517:28)
at emit (node:internal/child_process:944:14)
at process.processTicksAndRejections (node:internal/process/task_queues:83:21)
even though the file exists.
In my case this size of code existed because we had a large amount of vendor code in dist
directories which we had not excluded via the tsconfig
. I'm mostly interested to see if the error reporting can be improved.
I put together a standalone script which generates two "legit" files, file1.ts
and file2.ts
, and a huge file garbage-0.js
(39MB). It then opens typescript-language-server and plays back the commands I recorded when I was experiencing this problem from my editor (Neovim).
Saving test.js
and tsconfig.json
to an empty directory and running node test.js
should be sufficient to see the error and stack trace in the output.
Here is the script test.js
.
const {spawn} = require("child_process");
const fs = require("fs");
async function main() {
const commands = [
{
method: "initialize",
jsonrpc: "2.0",
params: {
trace: "off",
rootPath: __dirname,
rootUri: `file://${__dirname}`,
workspaceFolders: [
{
name: __dirname,
uri: `file://${__dirname}`,
},
],
initializationOptions: { hostInfo: "neovim" },
capabilities: {
workspace: {
symbol: {
symbolKind: {
valueSet: [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18,
19, 20, 21, 22, 23, 24, 25, 26,
],
},
dynamicRegistration: false,
},
semanticTokens: { refreshSupport: true },
configuration: true,
applyEdit: true,
inlayHint: { refreshSupport: true },
workspaceFolders: true,
workspaceEdit: {
resourceOperations: ["rename", "create", "delete"],
},
didChangeWatchedFiles: {
relativePatternSupport: true,
dynamicRegistration: true,
},
},
general: { positionEncodings: ["utf-16"] },
window: {
showMessage: {
messageActionItem: { additionalPropertiesSupport: false },
},
showDocument: { support: true },
workDoneProgress: true,
},
textDocument: {
documentHighlight: { dynamicRegistration: false },
documentSymbol: {
hierarchicalDocumentSymbolSupport: true,
symbolKind: {
valueSet: [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18,
19, 20, 21, 22, 23, 24, 25, 26,
],
},
dynamicRegistration: false,
},
synchronization: {
willSave: true,
willSaveWaitUntil: true,
didSave: true,
dynamicRegistration: false,
},
codeAction: {
isPreferredSupport: true,
dataSupport: true,
resolveSupport: { properties: ["edit"] },
codeActionLiteralSupport: {
codeActionKind: {
valueSet: [
"",
"quickfix",
"refactor",
"refactor.extract",
"refactor.inline",
"refactor.rewrite",
"source",
"source.organizeImports",
],
},
},
dynamicRegistration: true,
},
callHierarchy: { dynamicRegistration: false },
hover: {
contentFormat: ["markdown", "plaintext"],
dynamicRegistration: true,
},
formatting: { dynamicRegistration: true },
rangeFormatting: { dynamicRegistration: true },
rename: { prepareSupport: true, dynamicRegistration: true },
declaration: { linkSupport: true },
definition: { linkSupport: true, dynamicRegistration: true },
implementation: { linkSupport: true },
typeDefinition: { linkSupport: true },
publishDiagnostics: {
dataSupport: true,
relatedInformation: true,
tagSupport: { valueSet: [1, 2] },
},
signatureHelp: {
signatureInformation: {
activeParameterSupport: true,
parameterInformation: { labelOffsetSupport: true },
documentationFormat: ["markdown", "plaintext"],
},
dynamicRegistration: false,
},
semanticTokens: {
tokenTypes: [
"namespace",
"type",
"class",
"enum",
"interface",
"struct",
"typeParameter",
"parameter",
"variable",
"property",
"enumMember",
"event",
"function",
"method",
"macro",
"keyword",
"modifier",
"comment",
"string",
"number",
"regexp",
"operator",
"decorator",
],
serverCancelSupport: false,
tokenModifiers: [
"declaration",
"definition",
"readonly",
"static",
"deprecated",
"abstract",
"async",
"modification",
"documentation",
"defaultLibrary",
],
requests: { range: false, full: { delta: true } },
dynamicRegistration: false,
augmentsSyntaxTokens: true,
formats: ["relative"],
multilineTokenSupport: false,
overlappingTokenSupport: true,
},
inlayHint: {
resolveSupport: { properties: [] },
dynamicRegistration: true,
},
completion: {
completionItemKind: {
valueSet: [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18,
19, 20, 21, 22, 23, 24, 25,
],
},
contextSupport: false,
completionItem: {
commitCharactersSupport: false,
preselectSupport: false,
deprecatedSupport: false,
documentationFormat: ["markdown", "plaintext"],
snippetSupport: false,
},
dynamicRegistration: false,
},
diagnostic: { dynamicRegistration: false },
references: { dynamicRegistration: false },
},
},
processId: 40770,
clientInfo: { name: "Neovim", version: "0.10.0-dev+gd7359a874" },
},
id: 1,
},
{ jsonrpc: "2.0", result: null, id: 0 },
{ method: "initialized", jsonrpc: "2.0", params: {} },
{
method: "textDocument/didOpen",
jsonrpc: "2.0",
params: {
textDocument: {
version: 0,
text: 'console.log("lol")\n',
languageId: "typescript",
uri: `file://${__dirname}/file1.ts`,
},
},
},
{
method: "textDocument/semanticTokens/full",
jsonrpc: "2.0",
params: {
textDocument: {
uri: `file://${__dirname}/file1.ts`,
},
},
id: 2,
},
{ jsonrpc: "2.0", result: null, id: 1 },
{
method: "textDocument/didOpen",
jsonrpc: "2.0",
params: {
textDocument: {
version: 0,
text: 'console.log("lol 2")\n',
languageId: "typescript",
uri: `file://${__dirname}/file2.ts`,
},
},
},
{
method: "textDocument/semanticTokens/full",
jsonrpc: "2.0",
params: {
textDocument: {
uri: `file://${__dirname}/file2.ts`,
},
},
id: 3,
},
];
fs.writeFileSync("file1.ts", `console.log("lol")\n`);
fs.writeFileSync("file2.ts", `console.log("lol 2")\n`);
// Delete any existing garbage-* files.
const files = fs.readdirSync('.');
const garbageFiles = files.filter(file => file.startsWith('garbage-'));
for (const f of garbageFiles) {
fs.unlinkSync(f);
}
for (let i = 0; i < 400; i++) {
// For me the error happens somewhere between 1 million (20MB)
// and 2 million (40MB). It's also reproducible when there are many
// small files adding up to roughly the same amount, hence the (now-redundant)
// loop.
fs.writeFileSync(`garbage-${i}.js`, "console.log('lol');\n".repeat(4000));
}
const process = spawn(
"typescript-language-server",
['--stdio'],
{
shell: true,
stdio: 'pipe'
}
);
process.stdout.on("data", (chunk) => {
console.log(chunk.toString());
});
process.stderr.on("data", (chunk) => {
console.error(chunk.toString());
});
let i = 1;
for (const cmd of commands) {
console.log("command", i++, "of", commands.length);
const cmdString = JSON.stringify(cmd);
process.stdin.write(`Content-Length: ${cmdString.length}\r\n\r\n${cmdString}`);
// Some small delay is necessary to reproduce the problem. Increasing
// this to 10 seconds did not fix the problem for me.
await new Promise(r => setTimeout(r, 1000));
}
}
main()
Here is the tsconfig.json:
{
"compilerOptions": {
"lib": ["ESNext", "DOM"],
"module": "esnext",
"target": "esnext",
"moduleResolution": "bundler",
"moduleDetection": "force",
"allowImportingTsExtensions": true,
"noEmit": true,
"composite": true,
"strict": true,
"downlevelIteration": true,
"skipLibCheck": true,
"jsx": "react-jsx",
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"allowJs": true
}
}