diff --git a/packages/ts-plugin/src/index.ts b/packages/ts-plugin/src/index.ts index 92f4853..56f8631 100644 --- a/packages/ts-plugin/src/index.ts +++ b/packages/ts-plugin/src/index.ts @@ -20,6 +20,16 @@ const boot: tslib.server.PluginModuleFactory = ({ typescript: ts }) => { const syndicateRootDirs: Set = new Set(); + function shouldExpand(fileName: string): boolean { + let result = false; + syndicateRootDirs.forEach(d => { + if (fileName.startsWith(d)) { + result = true; + } + }); + return result; + } + function getInfo(fileName: string): SyndicateInfo | undefined { return syndicateInfo.get(fileName); } @@ -132,111 +142,125 @@ const boot: tslib.server.PluginModuleFactory = ({ typescript: ts }) => { return withPositions(fileName, [position], kNoInfo, kNoPosition, ([f]) => k(f)); } - function hookHost(host0: ts.CompilerHost | undefined, - options: ts.CompilerOptions) - { - const host = (host0 === void 0) ? ts.createCompilerHost(options, true) : host0; + function compileWithDiagnostics( + name: string, + source: string, + makeSourceFile: (expandedText: string) => ts.SourceFile, + ): { + expandedText: string, + targetToSourceMap: Syntax.SpanIndex, + sourceToTargetMap: Syntax.SpanIndex, + sourceFile: ts.SourceFile, + diagnostics: ts.DiagnosticWithLocation[], + } { + const diagnostics: Array<{ message: string, start?: Syntax.Pos, end?: Syntax.Pos }> = []; + // console.log(`Syndicate expanding ${name}: ${JSON.stringify(source)}`); + console.log(`Syndicate expanding ${name}`); + const { text: expandedText, targetToSourceMap, sourceToTargetMap } = compile({ + source, + name, + typescript: true, + emitError: (message, start, end) => { + console.error(`${Syntax.formatPos(start)}-${Syntax.formatPos(end)}: ${message}`); + diagnostics.push({ message, start, end }); + }, + }); + const sourceFile = makeSourceFile(expandedText); + return { + expandedText, + targetToSourceMap, + sourceToTargetMap, + sourceFile, + diagnostics: diagnostics.map(({ message, start, end }) => ({ + category: ts.DiagnosticCategory.Error, + code: 99999, + file: sourceFile, + start: start?.pos ?? 0, + length: Math.max(0, (end?.pos ?? 0) - (start?.pos ?? 0)), + messageText: message, + })), + }; + } - if ('Syndicate_hooked' in host) { - console.warn('Syndicate plugin refusing to hook CompilerHost twice'); - } else { - (host as any).Syndicate_hooked = true; - - const oldGetSourceFile = host.getSourceFile; - host.getSourceFile = getSourceFile; - - function getSourceFile(fileName: string, - languageVersion: ts.ScriptTarget, - onError?: ((message: string) => void), - shouldCreateNewSourceFile?: boolean): ts.SourceFile | undefined - { - let shouldExpand = false; - syndicateRootDirs.forEach(d => { - if (fileName.startsWith(d)) { - shouldExpand = true; - } - }); - if (shouldExpand) { - try { - const inputText = host.readFile(fileName); - if (inputText === void 0) { - onError?.(`Could not read input file ${fileName}`); - return undefined; - } - const diagnostics: - Array<{ message: string, start?: Syntax.Pos, end?: Syntax.Pos }> = []; - console.log('Syndicate compiling', fileName); - const { text: expandedText, targetToSourceMap, sourceToTargetMap } = compile({ - source: inputText, - name: fileName, - typescript: true, - emitError: (message, start, end) => { - console.error(`${Syntax.formatPos(start)}-${Syntax.formatPos(end)}: ${message}`); - diagnostics.push({ message, start, end }); - }, - }); - const sf = ts.createSourceFile(fileName, expandedText, languageVersion, true); - syndicateInfo.set(fileName, { - sourceFile: sf, - originalSource: inputText, - diagnostics: diagnostics.map(({ message, start, end }) => ({ - category: ts.DiagnosticCategory.Error, - code: 99999, - file: sf, - start: start?.pos ?? 0, - length: Math.max(0, (end?.pos ?? 0) - (start?.pos ?? 0)), - messageText: message, - })), - targetToSourceMap, - sourceToTargetMap, - }); - (sf as any).version = crypto.createHash('sha256').update(expandedText).digest('hex'); - return sf; - } catch (e) { - console.error(e); - onError?.((e as any).message ?? ''); - return undefined; - } - } else { - return oldGetSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile); - } - } - } - - return host; + function expandFile( + fileName: string, + inputText: string, + makeSourceFile: (expandedText: string) => ts.SourceFile, + ): ts.SourceFile { + const { expandedText, sourceFile, targetToSourceMap, sourceToTargetMap, diagnostics } = + compileWithDiagnostics(fileName, inputText, makeSourceFile); + const info = { + sourceFile, + originalSource: inputText, + diagnostics, + targetToSourceMap, + sourceToTargetMap, + }; + syndicateInfo.set(fileName, info); + (sourceFile as any).version = + crypto.createHash('sha256').update(expandedText).digest('hex'); + return sourceFile; } { - const oldCreateProgram = ts.createProgram; + const oldCreateSourceFile = ts.createSourceFile; - function createProgram(createProgramOptions: ts.CreateProgramOptions): ts.Program; - function createProgram(rootNames: readonly string[], - options: ts.CompilerOptions, - host?: ts.CompilerHost, - oldProgram?: ts.Program, - configFileParsingDiagnostics?: readonly ts.Diagnostic[]): ts.Program; - function createProgram(rootNamesOrOptions: readonly string[] | ts.CreateProgramOptions, - options?: ts.CompilerOptions, - host?: ts.CompilerHost, - oldProgram?: ts.Program, - configFileParsingDiagnostics?: readonly ts.Diagnostic[]) - : ts.Program - { - if (Array.isArray(rootNamesOrOptions)) { - const rootNames = rootNamesOrOptions; - host = hookHost(host, options!); - return oldCreateProgram(rootNames, options!, host, oldProgram, configFileParsingDiagnostics); + function createSourceFile( + fileName: string, + sourceText: string, + languageVersionOrOptions: ts.ScriptTarget | ts.CreateSourceFileOptions, + setParentNodes?: boolean, + scriptKind?: ts.ScriptKind, + ): ts.SourceFile { + const delegate = (text: string) => oldCreateSourceFile( + fileName, + text, + languageVersionOrOptions, + setParentNodes, + scriptKind); + if (shouldExpand(fileName)) { + // console.log('createSourceFile', fileName, sourceText); + return expandFile(fileName, sourceText, delegate); } else { - const createProgramOptions = rootNamesOrOptions as ts.CreateProgramOptions; - createProgramOptions.host = - hookHost(createProgramOptions.host, createProgramOptions.options); - return oldCreateProgram(createProgramOptions); + return delegate(sourceText); } } - ts.createProgram = createProgram; + ts.createSourceFile = createSourceFile; } + class SyndicateScriptInfo extends ts.server.ScriptInfo { + constructor( + host: ts.server.ServerHost, + fileName: ts.server.NormalizedPath, + scriptKind: ts.ScriptKind, + hasMixedContent: boolean, + path: ts.Path, + initialVersion?: ts.server.ScriptInfoVersion, + ) { + if (shouldExpand(fileName)) { + console.log('SyndicateScriptInfo constructor', fileName); + } + super(host, fileName, scriptKind, hasMixedContent, path, initialVersion); + } + + editContent(start: number, end: number, newText: string): void { + // console.log('SyndicateScriptInfo.editContent', this.fileName, start, end, newText); + withFileName(this.fileName, + () => super.editContent(start, end, newText), + (fx) => { + const info = fx.info; + const revised = info.originalSource.substring(0, start) + + newText + + info.originalSource.substring(end); + info.originalSource = revised; + this.open(revised); + }); + } + } + + ts.server.ScriptInfo = SyndicateScriptInfo; + class SyndicateLanguageService implements ts.LanguageService { readonly inner: ts.LanguageService; @@ -747,6 +771,31 @@ const boot: tslib.server.PluginModuleFactory = ({ typescript: ts }) => { if (options.rootDir === void 0 && options.rootDirs === void 0) { syndicateRootDirs.add(finalSlash(path.resolve('.'))); } + // This next little block is required to flush out previously-created ScriptInfo + // instances for our project, since they were created before our plugin was even + // loaded and our plugin monkey-patches ScriptInfo to SyndicateScriptInfo. + { + const infos: Map = + (createInfo.project.projectService as any).filenameToScriptInfo; + const oldInfos = Array.from(infos.values()); + infos.clear(); + for (const info of oldInfos) { + // console.log('---', info.fileName); + // console.log(JSON.stringify({ + // oldText: (info as any).textStorage.text, + // }, void 0, 2)); + const newInfo = new SyndicateScriptInfo( + (info as any).host, + info.fileName, + info.scriptKind, + info.hasMixedContent, + info.path, + (info as any).textStorage.version); + (newInfo as any).textStorage = (info as any).textStorage; + infos.set(info.fileName, newInfo); + createInfo.project.getSourceFile(info.fileName as any); + } + } return new SyndicateLanguageService(createInfo.languageService); } }