Watch generated file if it doesnt exist when trying to translate it to to source generated position
This commit is contained in:
parent
903527e757
commit
3e49556a88
|
@ -2234,7 +2234,13 @@ namespace ts.server {
|
|||
getDocumentPositionMapper(project: Project, generatedFileName: string, sourceFileName?: string): DocumentPositionMapper | undefined {
|
||||
// Since declaration info and map file watches arent updating project's directory structure host (which can cache file structure) use host
|
||||
const declarationInfo = this.getOrCreateScriptInfoNotOpenedByClient(generatedFileName, project.currentDirectory, this.host);
|
||||
if (!declarationInfo) return undefined;
|
||||
if (!declarationInfo) {
|
||||
if (sourceFileName) {
|
||||
// Project contains source file and it generates the generated file name
|
||||
project.addGeneratedFileWatch(generatedFileName, sourceFileName);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Try to get from cache
|
||||
declarationInfo.getSnapshot(); // Ensure synchronized
|
||||
|
|
|
@ -109,12 +109,22 @@ namespace ts.server {
|
|||
return value instanceof ScriptInfo;
|
||||
}
|
||||
|
||||
interface GeneratedFileWatcher {
|
||||
generatedFilePath: Path;
|
||||
watcher: FileWatcher;
|
||||
}
|
||||
type GeneratedFileWatcherMap = GeneratedFileWatcher | Map<GeneratedFileWatcher>;
|
||||
function isGeneratedFileWatcher(watch: GeneratedFileWatcherMap): watch is GeneratedFileWatcher {
|
||||
return (watch as GeneratedFileWatcher).generatedFilePath !== undefined;
|
||||
}
|
||||
|
||||
export abstract class Project implements LanguageServiceHost, ModuleResolutionHost {
|
||||
private rootFiles: ScriptInfo[] = [];
|
||||
private rootFilesMap: Map<ProjectRoot> = createMap<ProjectRoot>();
|
||||
private program: Program | undefined;
|
||||
private externalFiles: SortedReadonlyArray<string> | undefined;
|
||||
private missingFilesMap: Map<FileWatcher> | undefined;
|
||||
private generatedFilesMap: GeneratedFileWatcherMap | undefined;
|
||||
private plugins: PluginModuleWithName[] = [];
|
||||
|
||||
/*@internal*/
|
||||
|
@ -568,6 +578,7 @@ namespace ts.server {
|
|||
this.lastFileExceededProgramSize = lastFileExceededProgramSize;
|
||||
this.builderState = undefined;
|
||||
this.resolutionCache.closeTypeRootsWatch();
|
||||
this.clearGeneratedFileWatch();
|
||||
this.projectService.onUpdateLanguageServiceStateForProject(this, /*languageServiceEnabled*/ false);
|
||||
}
|
||||
|
||||
|
@ -649,6 +660,7 @@ namespace ts.server {
|
|||
clearMap(this.missingFilesMap, closeFileWatcher);
|
||||
this.missingFilesMap = undefined!;
|
||||
}
|
||||
this.clearGeneratedFileWatch();
|
||||
|
||||
// signal language service to release source files acquired from document registry
|
||||
this.languageService.dispose();
|
||||
|
@ -942,6 +954,39 @@ namespace ts.server {
|
|||
missingFilePath => this.addMissingFileWatcher(missingFilePath)
|
||||
);
|
||||
|
||||
if (this.generatedFilesMap) {
|
||||
const outPath = this.compilerOptions.outFile && this.compilerOptions.out;
|
||||
if (isGeneratedFileWatcher(this.generatedFilesMap)) {
|
||||
// --out
|
||||
if (!outPath || !this.isValidGeneratedFileWatcher(
|
||||
removeFileExtension(outPath) + Extension.Dts,
|
||||
this.generatedFilesMap,
|
||||
)) {
|
||||
this.clearGeneratedFileWatch();
|
||||
}
|
||||
}
|
||||
else {
|
||||
// MultiFile
|
||||
if (outPath) {
|
||||
this.clearGeneratedFileWatch();
|
||||
}
|
||||
else {
|
||||
this.generatedFilesMap.forEach((watcher, source) => {
|
||||
const sourceFile = this.program!.getSourceFileByPath(source as Path);
|
||||
if (!sourceFile ||
|
||||
sourceFile.resolvedPath !== source ||
|
||||
!this.isValidGeneratedFileWatcher(
|
||||
getDeclarationEmitOutputFilePathWorker(sourceFile.fileName, this.compilerOptions, this.currentDirectory, this.program!.getCommonSourceDirectory(), this.getCanonicalFileName),
|
||||
watcher
|
||||
)) {
|
||||
closeFileWatcherOf(watcher);
|
||||
(this.generatedFilesMap as Map<GeneratedFileWatcher>).delete(source);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Watch the type locations that would be added to program as part of automatic type resolutions
|
||||
if (this.languageServiceEnabled) {
|
||||
this.resolutionCache.updateTypeRootsWatch();
|
||||
|
@ -1006,6 +1051,61 @@ namespace ts.server {
|
|||
return !!this.missingFilesMap && this.missingFilesMap.has(path);
|
||||
}
|
||||
|
||||
/* @internal */
|
||||
addGeneratedFileWatch(generatedFile: string, sourceFile: string) {
|
||||
if (this.compilerOptions.outFile || this.compilerOptions.out) {
|
||||
// Single watcher
|
||||
if (!this.generatedFilesMap) {
|
||||
this.generatedFilesMap = this.createGeneratedFileWatcher(generatedFile);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Map
|
||||
const path = this.toPath(sourceFile);
|
||||
if (this.generatedFilesMap) {
|
||||
if (isGeneratedFileWatcher(this.generatedFilesMap)) {
|
||||
Debug.fail(`${this.projectName} Expected not to have --out watcher for generated file with options: ${JSON.stringify(this.compilerOptions)}`);
|
||||
return;
|
||||
}
|
||||
if (this.generatedFilesMap.has(path)) return;
|
||||
}
|
||||
else {
|
||||
this.generatedFilesMap = createMap();
|
||||
}
|
||||
this.generatedFilesMap.set(path, this.createGeneratedFileWatcher(generatedFile));
|
||||
}
|
||||
}
|
||||
|
||||
private createGeneratedFileWatcher(generatedFile: string): GeneratedFileWatcher {
|
||||
return {
|
||||
generatedFilePath: this.toPath(generatedFile),
|
||||
watcher: this.projectService.watchFactory.watchFile(
|
||||
this.projectService.host,
|
||||
generatedFile,
|
||||
() => this.projectService.delayUpdateProjectGraphAndEnsureProjectStructureForOpenFiles(this),
|
||||
PollingInterval.High,
|
||||
WatchType.MissingGeneratedFile,
|
||||
this
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
private isValidGeneratedFileWatcher(generateFile: string, watcher: GeneratedFileWatcher) {
|
||||
return this.toPath(generateFile) === watcher.generatedFilePath;
|
||||
}
|
||||
|
||||
private clearGeneratedFileWatch() {
|
||||
if (this.generatedFilesMap) {
|
||||
if (isGeneratedFileWatcher(this.generatedFilesMap)) {
|
||||
closeFileWatcherOf(this.generatedFilesMap);
|
||||
}
|
||||
else {
|
||||
clearMap(this.generatedFilesMap, closeFileWatcherOf);
|
||||
}
|
||||
this.generatedFilesMap = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
getScriptInfoForNormalizedPath(fileName: NormalizedPath): ScriptInfo | undefined {
|
||||
const scriptInfo = this.projectService.getScriptInfoForPath(this.toPath(fileName));
|
||||
if (scriptInfo && !scriptInfo.isAttached(this)) {
|
||||
|
|
|
@ -227,5 +227,6 @@ namespace ts {
|
|||
NodeModulesForClosedScriptInfo = "node_modules for closed script infos in them",
|
||||
MissingSourceMapFile = "Missing source map file",
|
||||
NoopConfigFileForInferredRoot = "Noop Config file for the inferred project root",
|
||||
MissingGeneratedFile = "Missing generated file"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -94,6 +94,7 @@ namespace ts.projectSystem {
|
|||
describe("with main and depedency project", () => {
|
||||
const projectLocation = "/user/username/projects/myproject";
|
||||
const dependecyLocation = `${projectLocation}/dependency`;
|
||||
const dependecyDeclsLocation = `${projectLocation}/decls`;
|
||||
const mainLocation = `${projectLocation}/main`;
|
||||
const dependencyTs: File = {
|
||||
path: `${dependecyLocation}/FnS.ts`,
|
||||
|
@ -106,7 +107,7 @@ export function fn5() { }
|
|||
};
|
||||
const dependencyConfig: File = {
|
||||
path: `${dependecyLocation}/tsconfig.json`,
|
||||
content: JSON.stringify({ compilerOptions: { composite: true, declarationMap: true } })
|
||||
content: JSON.stringify({ compilerOptions: { composite: true, declarationMap: true, declarationDir: "../decls" } })
|
||||
};
|
||||
|
||||
const mainTs: File = {
|
||||
|
@ -117,7 +118,7 @@ export function fn5() { }
|
|||
fn3,
|
||||
fn4,
|
||||
fn5
|
||||
} from '../dependency/fns'
|
||||
} from '../decls/fns'
|
||||
|
||||
fn1();
|
||||
fn2();
|
||||
|
@ -142,9 +143,9 @@ fn5();
|
|||
path: `${projectLocation}/random/tsconfig.json`,
|
||||
content: "{}"
|
||||
};
|
||||
const dtsLocation = `${dependecyLocation}/FnS.d.ts`;
|
||||
const dtsLocation = `${dependecyDeclsLocation}/FnS.d.ts`;
|
||||
const dtsPath = dtsLocation.toLowerCase() as Path;
|
||||
const dtsMapLocation = `${dtsLocation}.map`;
|
||||
const dtsMapLocation = `${dependecyDeclsLocation}/FnS.d.ts.map`;
|
||||
const dtsMapPath = dtsMapLocation.toLowerCase() as Path;
|
||||
|
||||
const files = [dependencyTs, dependencyConfig, mainTs, mainConfig, libFile, randomFile, randomConfig];
|
||||
|
@ -217,7 +218,7 @@ fn5();
|
|||
start: { line: fn + 1, offset: 5 },
|
||||
end: { line: fn + 1, offset: 8 },
|
||||
contextStart: { line: 1, offset: 1 },
|
||||
contextEnd: { line: 7, offset: 27 }
|
||||
contextEnd: { line: 7, offset: 22 }
|
||||
};
|
||||
}
|
||||
function usageSpan(fn: number): protocol.TextSpan {
|
||||
|
@ -287,19 +288,25 @@ fn5();
|
|||
function verifyDocumentPositionMapperUpdates(
|
||||
mainScenario: string,
|
||||
verifier: ReadonlyArray<DocumentPositionMapperVerifier>,
|
||||
closedInfos: ReadonlyArray<string>) {
|
||||
closedInfos: ReadonlyArray<string>,
|
||||
withRefs: boolean) {
|
||||
|
||||
const openFiles = verifier.map(v => v.openFile);
|
||||
const expectedProjectActualFiles = verifier.map(v => v.expectedProjectActualFiles);
|
||||
const actionGetters = verifier.map(v => v.actionGetter);
|
||||
const openFileLastLines = verifier.map(v => v.openFileLastLine);
|
||||
|
||||
const configFiles = openFiles.map(openFile => `${getDirectoryPath(openFile.path)}/tsconfig.json`);
|
||||
const openInfos = openFiles.map(f => f.path);
|
||||
// When usage and dependency are used, dependency config is part of closedInfo so ignore
|
||||
const otherWatchedFiles = verifier.length > 1 ? [configFiles[0]] : configFiles;
|
||||
const otherWatchedFiles = withRefs && verifier.length > 1 ? [configFiles[0]] : configFiles;
|
||||
function openTsFile(onHostCreate?: (host: TestServerHost) => void) {
|
||||
const host = createHost(files, [mainConfig.path]);
|
||||
if (!withRefs) {
|
||||
// Erase project reference
|
||||
host.writeFile(mainConfig.path, JSON.stringify({
|
||||
compilerOptions: { composite: true, declarationMap: true }
|
||||
}));
|
||||
}
|
||||
if (onHostCreate) {
|
||||
onHostCreate(host);
|
||||
}
|
||||
|
@ -336,7 +343,7 @@ fn5();
|
|||
);
|
||||
}
|
||||
|
||||
function verifyInfosWhenNoDtsFile(session: TestSession, host: TestServerHost, dependencyTsAndMapOk?: true) {
|
||||
function verifyInfosWhenNoDtsFile(session: TestSession, host: TestServerHost, watchDts: boolean, dependencyTsAndMapOk?: true) {
|
||||
const dtsMapClosedInfo = firstDefined(closedInfos, f => f.toLowerCase() === dtsMapPath ? f : undefined);
|
||||
const dtsClosedInfo = firstDefined(closedInfos, f => f.toLowerCase() === dtsPath ? f : undefined);
|
||||
verifyInfosWithRandom(
|
||||
|
@ -344,8 +351,7 @@ fn5();
|
|||
host,
|
||||
openInfos,
|
||||
closedInfos.filter(f => (dependencyTsAndMapOk || f !== dtsMapClosedInfo) && f !== dtsClosedInfo && (dependencyTsAndMapOk || f !== dependencyTs.path)),
|
||||
// When project actual file contains dts, it needs to be watched
|
||||
dtsClosedInfo && expectedProjectActualFiles.some(expectedProjectActualFiles => expectedProjectActualFiles.some(f => f.toLowerCase() === dtsPath)) ?
|
||||
dtsClosedInfo && watchDts ?
|
||||
otherWatchedFiles.concat(dtsClosedInfo) :
|
||||
otherWatchedFiles
|
||||
);
|
||||
|
@ -361,22 +367,22 @@ fn5();
|
|||
}
|
||||
}
|
||||
|
||||
function action(actionGetter: SessionActionGetter, fn: number, session: TestSession) {
|
||||
const { reqName, request, expectedResponse, expectedResponseNoMap, expectedResponseNoDts } = actionGetter(fn);
|
||||
function action(verifier: DocumentPositionMapperVerifier, fn: number, session: TestSession) {
|
||||
const { reqName, request, expectedResponse, expectedResponseNoMap, expectedResponseNoDts } = verifier.actionGetter(fn);
|
||||
const { response } = session.executeCommandSeq(request);
|
||||
return { reqName, response, expectedResponse, expectedResponseNoMap, expectedResponseNoDts };
|
||||
return { reqName, response, expectedResponse, expectedResponseNoMap, expectedResponseNoDts, verifier };
|
||||
}
|
||||
|
||||
function firstAction(session: TestSession) {
|
||||
actionGetters.forEach(actionGetter => action(actionGetter, 1, session));
|
||||
verifier.forEach(v => action(v, 1, session));
|
||||
}
|
||||
|
||||
function verifyAllFnActionWorker(session: TestSession, verifyAction: (result: ReturnType<typeof action>, dtsInfo: server.ScriptInfo | undefined, isFirst: boolean) => void, dtsAbsent?: true) {
|
||||
// action
|
||||
let isFirst = true;
|
||||
for (const actionGetter of actionGetters) {
|
||||
for (const v of verifier) {
|
||||
for (let fn = 1; fn <= 5; fn++) {
|
||||
const result = action(actionGetter, fn, session);
|
||||
const result = action(v, fn, session);
|
||||
const dtsInfo = session.getProjectService().filenameToScriptInfo.get(dtsPath);
|
||||
if (dtsAbsent) {
|
||||
assert.isUndefined(dtsInfo);
|
||||
|
@ -449,9 +455,17 @@ fn5();
|
|||
dependencyTsAndMapOk?: true
|
||||
) {
|
||||
// action
|
||||
verifyAllFnActionWorker(session, ({ reqName, response, expectedResponse, expectedResponseNoDts }) => {
|
||||
verifyAllFnActionWorker(session, ({ reqName, response, expectedResponse, expectedResponseNoDts, verifier }) => {
|
||||
assert.deepEqual(response, expectedResponseNoDts || expectedResponse, `Failed on ${reqName}`);
|
||||
verifyInfosWhenNoDtsFile(session, host, dependencyTsAndMapOk);
|
||||
verifyInfosWhenNoDtsFile(
|
||||
session,
|
||||
host,
|
||||
// Even when project actual file contains dts, its not watched because the dts is in another folder and module resolution just fails
|
||||
// instead of succeeding to source file and then mapping using project reference (When using usage location)
|
||||
// But watched if sourcemapper is in source project since we need to keep track of dts to update the source mapper for any potential usages
|
||||
verifier.expectedProjectActualFiles.every(f => f.toLowerCase() !== dtsPath),
|
||||
dependencyTsAndMapOk,
|
||||
);
|
||||
}, /*dtsAbsent*/ true);
|
||||
}
|
||||
|
||||
|
@ -535,7 +549,11 @@ fn5();
|
|||
// Collecting at this point retains dependency.d.ts and map watcher
|
||||
closeFilesForSession([randomFile], session);
|
||||
openFilesForSession([randomFile], session);
|
||||
verifyInfosWhenNoDtsFile(session, host);
|
||||
verifyInfosWhenNoDtsFile(
|
||||
session,
|
||||
host,
|
||||
!!forEach(verifier, v => v.expectedProjectActualFiles.every(f => f.toLowerCase() !== dtsPath))
|
||||
);
|
||||
|
||||
// Closing open file, removes dependencies too
|
||||
closeFilesForSession([...openFiles, randomFile], session);
|
||||
|
@ -616,7 +634,7 @@ fn5();
|
|||
"when dependency file's map changes",
|
||||
host => host.writeFile(
|
||||
dtsMapLocation,
|
||||
`{"version":3,"file":"FnS.d.ts","sourceRoot":"","sources":["FnS.ts"],"names":[],"mappings":"AAAA,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,eAAO,MAAM,CAAC,KAAK,CAAC"}`
|
||||
`{"version":3,"file":"FnS.d.ts","sourceRoot":"","sources":["../dependency/FnS.ts"],"names":[],"mappings":"AAAA,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,eAAO,MAAM,CAAC,KAAK,CAAC"}`
|
||||
),
|
||||
/*afterActionDocumentPositionMapperNotEquals*/ true
|
||||
);
|
||||
|
@ -635,44 +653,58 @@ fn5();
|
|||
);
|
||||
}
|
||||
|
||||
const usageVerifier: DocumentPositionMapperVerifier = {
|
||||
openFile: mainTs,
|
||||
expectedProjectActualFiles: [mainTs.path, libFile.path, mainConfig.path, dtsPath],
|
||||
actionGetter: gotoDefintinionFromMainTs,
|
||||
openFileLastLine: 14
|
||||
};
|
||||
describe("from project that uses dependency", () => {
|
||||
const closedInfos = [dependencyTs.path, dependencyConfig.path, libFile.path, dtsPath, dtsMapLocation];
|
||||
verifyDocumentPositionMapperUpdates(
|
||||
"can go to definition correctly",
|
||||
[usageVerifier],
|
||||
closedInfos
|
||||
);
|
||||
});
|
||||
function verifyScenarios(withRefs: boolean) {
|
||||
describe(withRefs ? "when main tsconfig has project reference" : "when main tsconfig doesnt have project reference", () => {
|
||||
const usageVerifier: DocumentPositionMapperVerifier = {
|
||||
openFile: mainTs,
|
||||
expectedProjectActualFiles: [mainTs.path, libFile.path, mainConfig.path, dtsPath],
|
||||
actionGetter: gotoDefintinionFromMainTs,
|
||||
openFileLastLine: 14
|
||||
};
|
||||
describe("from project that uses dependency", () => {
|
||||
const closedInfos = withRefs ?
|
||||
[dependencyTs.path, dependencyConfig.path, libFile.path, dtsPath, dtsMapLocation] :
|
||||
[dependencyTs.path, libFile.path, dtsPath, dtsMapLocation];
|
||||
verifyDocumentPositionMapperUpdates(
|
||||
"can go to definition correctly",
|
||||
[usageVerifier],
|
||||
closedInfos,
|
||||
withRefs
|
||||
);
|
||||
});
|
||||
|
||||
const definingVerifier: DocumentPositionMapperVerifier = {
|
||||
openFile: dependencyTs,
|
||||
expectedProjectActualFiles: [dependencyTs.path, libFile.path, dependencyConfig.path],
|
||||
actionGetter: renameFromDependencyTs,
|
||||
openFileLastLine: 6
|
||||
};
|
||||
describe("from defining project", () => {
|
||||
const closedInfos = [libFile.path, dtsLocation, dtsMapLocation];
|
||||
verifyDocumentPositionMapperUpdates(
|
||||
"rename locations from dependency",
|
||||
[definingVerifier],
|
||||
closedInfos
|
||||
);
|
||||
});
|
||||
const definingVerifier: DocumentPositionMapperVerifier = {
|
||||
openFile: dependencyTs,
|
||||
expectedProjectActualFiles: [dependencyTs.path, libFile.path, dependencyConfig.path],
|
||||
actionGetter: renameFromDependencyTs,
|
||||
openFileLastLine: 6,
|
||||
};
|
||||
describe("from defining project", () => {
|
||||
const closedInfos = [libFile.path, dtsLocation, dtsMapLocation];
|
||||
verifyDocumentPositionMapperUpdates(
|
||||
"rename locations from dependency",
|
||||
[definingVerifier],
|
||||
closedInfos,
|
||||
withRefs
|
||||
);
|
||||
});
|
||||
|
||||
describe("when opening depedency and usage project", () => {
|
||||
const closedInfos = [libFile.path, dtsPath, dtsMapLocation, dependencyConfig.path];
|
||||
verifyDocumentPositionMapperUpdates(
|
||||
"goto Definition in usage and rename locations from defining project",
|
||||
[usageVerifier, { ...definingVerifier, actionGetter: renameFromDependencyTsWithBothProjectsOpen }],
|
||||
closedInfos
|
||||
);
|
||||
});
|
||||
describe("when opening depedency and usage project", () => {
|
||||
const closedInfos = withRefs ?
|
||||
[libFile.path, dtsPath, dtsMapLocation, dependencyConfig.path] :
|
||||
[libFile.path, dtsPath, dtsMapLocation];
|
||||
verifyDocumentPositionMapperUpdates(
|
||||
"goto Definition in usage and rename locations from defining project",
|
||||
[usageVerifier, { ...definingVerifier, actionGetter: renameFromDependencyTsWithBothProjectsOpen }],
|
||||
closedInfos,
|
||||
withRefs
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
verifyScenarios(/*withRefs*/ false);
|
||||
verifyScenarios(/*withRefs*/ true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -8397,6 +8397,7 @@ declare namespace ts.server {
|
|||
private program;
|
||||
private externalFiles;
|
||||
private missingFilesMap;
|
||||
private generatedFilesMap;
|
||||
private plugins;
|
||||
private lastFileExceededProgramSize;
|
||||
protected languageService: LanguageService;
|
||||
|
@ -8509,6 +8510,9 @@ declare namespace ts.server {
|
|||
private detachScriptInfoFromProject;
|
||||
private addMissingFileWatcher;
|
||||
private isWatchedMissingFile;
|
||||
private createGeneratedFileWatcher;
|
||||
private isValidGeneratedFileWatcher;
|
||||
private clearGeneratedFileWatch;
|
||||
getScriptInfoForNormalizedPath(fileName: NormalizedPath): ScriptInfo | undefined;
|
||||
getScriptInfo(uncheckedFileName: string): ScriptInfo | undefined;
|
||||
filesToString(writeProjectFileNames: boolean): string;
|
||||
|
|
Loading…
Reference in a new issue