Skip to content

Commit 8f64d66

Browse files
authored
If emitting declaration file, update the d.ts signature in state to not have to save all files when different file changes without changing its shape (#37483)
* Add test that calling getAffectedFiles on two different files (even if the second one is saved) returns all files * If emitting declaration file, update the d.ts signature in state to not have to save all files when different file changes without changing its shape Fixes #30508
1 parent 5e9c436 commit 8f64d66

File tree

3 files changed

+140
-4
lines changed

3 files changed

+140
-4
lines changed

src/compiler/builderState.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -292,10 +292,12 @@ namespace ts {
292292
* This should be called whenever it is safe to commit the state of the builder
293293
*/
294294
export function updateSignaturesFromCache(state: BuilderState, signatureCache: Map<string>) {
295-
signatureCache.forEach((signature, path) => {
296-
state.fileInfos.get(path)!.signature = signature;
297-
state.hasCalledUpdateShapeSignature.set(path, true);
298-
});
295+
signatureCache.forEach((signature, path) => updateSignatureOfFile(state, signature, path as Path));
296+
}
297+
298+
export function updateSignatureOfFile(state: BuilderState, signature: string | undefined, path: Path) {
299+
state.fileInfos.get(path)!.signature = signature;
300+
state.hasCalledUpdateShapeSignature.set(path, true);
299301
}
300302

301303
/**

src/server/project.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,15 @@ namespace ts.server {
603603
const outputFileAbsoluteFileName = getNormalizedAbsolutePath(outputFile.name, this.currentDirectory);
604604
writeFile(outputFileAbsoluteFileName, outputFile.text, outputFile.writeByteOrderMark);
605605
}
606+
607+
// Update the signature
608+
if (this.builderState && getEmitDeclarations(this.compilerOptions)) {
609+
const dtsFiles = outputFiles.filter(f => fileExtensionIs(f.name, Extension.Dts));
610+
if (dtsFiles.length === 1) {
611+
const sourceFile = this.program!.getSourceFile(scriptInfo.fileName)!;
612+
BuilderState.updateSignatureOfFile(this.builderState, this.projectService.host.createHash!(dtsFiles[0].text), sourceFile.resolvedPath);
613+
}
614+
}
606615
}
607616

608617
return { emitSkipped, diagnostics };

src/testRunner/unittests/tsserver/compileOnSave.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -880,6 +880,131 @@ namespace ts.projectSystem {
880880
assert.isFalse(host.fileExists(`${tscWatch.projectRoot}/test/file2.d.ts`));
881881
}
882882
});
883+
884+
describe("compile on save in global files", () => {
885+
describe("when program contains module", () => {
886+
it("when d.ts emit is enabled", () => {
887+
verifyGlobalSave(/*declaration*/ true, /*hasModule*/ true);
888+
});
889+
it("when d.ts emit is not enabled", () => {
890+
verifyGlobalSave(/*declaration*/ false, /*hasModule*/ true);
891+
});
892+
});
893+
describe("when program doesnt have module", () => {
894+
it("when d.ts emit is enabled", () => {
895+
verifyGlobalSave(/*declaration*/ true, /*hasModule*/ false);
896+
});
897+
it("when d.ts emit is not enabled", () => {
898+
verifyGlobalSave(/*declaration*/ false, /*hasModule*/ false);
899+
});
900+
});
901+
function verifyGlobalSave(declaration: boolean,hasModule: boolean) {
902+
const config: File = {
903+
path: `${tscWatch.projectRoot}/tsconfig.json`,
904+
content: JSON.stringify({
905+
compileOnSave: true,
906+
compilerOptions: {
907+
declaration,
908+
module: hasModule ? undefined : "none"
909+
},
910+
})
911+
};
912+
const file1: File = {
913+
path: `${tscWatch.projectRoot}/file1.ts`,
914+
content: `const x = 1;
915+
function foo() {
916+
return "hello";
917+
}`
918+
};
919+
const file2: File = {
920+
path: `${tscWatch.projectRoot}/file2.ts`,
921+
content: `const y = 2;
922+
function bar() {
923+
return "world";
924+
}`
925+
};
926+
const file3: File = {
927+
path: `${tscWatch.projectRoot}/file3.ts`,
928+
content: "const xy = 3;"
929+
};
930+
const module: File = {
931+
path: `${tscWatch.projectRoot}/module.ts`,
932+
content: "export const xyz = 4;"
933+
};
934+
const files = [file1, file2, file3, ...(hasModule ? [module] : emptyArray)];
935+
const host = createServerHost([...files, config, libFile]);
936+
const session = createSession(host);
937+
openFilesForSession([file1, file2], session);
938+
939+
const affectedFileResponse = session.executeCommandSeq<protocol.CompileOnSaveAffectedFileListRequest>({
940+
command: protocol.CommandTypes.CompileOnSaveAffectedFileList,
941+
arguments: { file: file1.path }
942+
}).response as protocol.CompileOnSaveAffectedFileListSingleProject[];
943+
assert.deepEqual(affectedFileResponse, [
944+
{ fileNames: files.map(f => f.path), projectFileName: config.path, projectUsesOutFile: false }
945+
]);
946+
verifyFileSave(file1);
947+
verifyFileSave(file2);
948+
verifyFileSave(file3);
949+
if (hasModule) {
950+
verifyFileSave(module);
951+
}
952+
953+
// Change file1 get affected file list
954+
verifyLocalEdit(file1, "hello", "world");
955+
956+
// Change file2 get affected file list = will return only file2 if --declaration otherwise all files
957+
verifyLocalEdit(file2, "world", "hello", /*returnsAllFilesAsAffected*/ !declaration);
958+
959+
function verifyFileSave(file: File) {
960+
const response = session.executeCommandSeq<protocol.CompileOnSaveEmitFileRequest>({
961+
command: protocol.CommandTypes.CompileOnSaveEmitFile,
962+
arguments: { file: file.path }
963+
}).response;
964+
assert.isTrue(response);
965+
assert.strictEqual(
966+
host.readFile(changeExtension(file.path, ".js")),
967+
file === module ?
968+
`"use strict";\nexports.__esModule = true;\nexports.xyz = void 0;\nexports.xyz = 4;\n` :
969+
`${file.content.replace("const", "var")}\n`
970+
);
971+
if (declaration) {
972+
assert.strictEqual(
973+
host.readFile(changeExtension(file.path, ".d.ts")),
974+
(file.content.substr(0, file.content.indexOf(" {") === -1 ? file.content.length : file.content.indexOf(" {"))
975+
.replace("const ", "declare const ")
976+
.replace("function ", "declare function ")
977+
.replace(")", "): string;")) + "\n"
978+
);
979+
}
980+
}
981+
982+
function verifyLocalEdit(file: File, oldText: string, newText: string, returnsAllFilesAsAffected?: boolean) {
983+
// Change file1 get affected file list
984+
session.executeCommandSeq<protocol.UpdateOpenRequest>({
985+
command: protocol.CommandTypes.UpdateOpen,
986+
arguments: {
987+
changedFiles: [{
988+
fileName: file.path,
989+
textChanges: [{
990+
newText,
991+
...protocolTextSpanFromSubstring(file.content, oldText)
992+
}]
993+
}]
994+
}
995+
});
996+
const affectedFileResponse = session.executeCommandSeq<protocol.CompileOnSaveAffectedFileListRequest>({
997+
command: protocol.CommandTypes.CompileOnSaveAffectedFileList,
998+
arguments: { file: file.path }
999+
}).response as protocol.CompileOnSaveAffectedFileListSingleProject[];
1000+
assert.deepEqual(affectedFileResponse, [
1001+
{ fileNames: [file.path, ...(returnsAllFilesAsAffected ? files.filter(f => f !== file).map(f => f.path) : emptyArray)], projectFileName: config.path, projectUsesOutFile: false }
1002+
]);
1003+
file.content = file.content.replace(oldText, newText);
1004+
verifyFileSave(file);
1005+
}
1006+
}
1007+
});
8831008
});
8841009

8851010
describe("unittests:: tsserver:: compileOnSave:: CompileOnSaveAffectedFileListRequest with and without projectFileName in request", () => {

0 commit comments

Comments
 (0)