From ffe2c8049d41db0efd90f21699b241afa00c7206 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Wed, 8 Nov 2017 13:16:41 -0800 Subject: [PATCH 1/7] Add watch host in parallel to language service so as to move to new api --- src/instances.ts | 15 ++++-- src/interfaces.ts | 4 ++ src/servicesHost.ts | 116 +++++++++++++++++++++++++++++++++++++++++++- src/utils.ts | 9 ++++ 4 files changed, 138 insertions(+), 6 deletions(-) diff --git a/src/instances.ts b/src/instances.ts index 67ce6fcc6..5fdf70719 100644 --- a/src/instances.ts +++ b/src/instances.ts @@ -9,7 +9,7 @@ import { EOL, dtsDtsxRegex } from './constants'; import { getCompilerOptions, getCompiler } from './compilerSetup'; import { hasOwnProperty, makeError, formatErrors, registerWebpackErrors } from './utils'; import * as logger from './logger'; -import { makeServicesHost } from './servicesHost'; +import { makeServicesHost, makeWatchHost } from './servicesHost'; import { makeWatchRun } from './watch-run'; import { LoaderOptions, @@ -90,10 +90,10 @@ function successfulTypeScriptInstance( // quick return for transpiling // we do need to check for any issues with TS options though const program = compiler!.createProgram([], compilerOptions); - const diagnostics = program.getOptionsDiagnostics(); // happypack does not have _module.errors - see https://github.com/TypeStrong/ts-loader/issues/336 if (!loaderOptions.happyPackMode) { + const diagnostics = program.getOptionsDiagnostics(); registerWebpackErrors( loader._module.errors, formatErrors(diagnostics, loaderOptions, colors, compiler!, {file: configFilePath || 'tsconfig.json'})); @@ -151,8 +151,15 @@ function successfulTypeScriptInstance( colors }; - const servicesHost = makeServicesHost(scriptRegex, log, loader, instance, loaderOptions.appendTsSuffixTo, loaderOptions.appendTsxSuffixTo); - instance.languageService = compiler.createLanguageService(servicesHost, compiler.createDocumentRegistry()); + if (compiler.createWatch) { + // If there is api available for watch, use it instead of language service + const watchHost = makeWatchHost(scriptRegex, log, loader, instance, loaderOptions.appendTsSuffixTo, loaderOptions.appendTsxSuffixTo); + instance.watchMode = compiler.createWatch(watchHost); + } + //else { + const servicesHost = makeServicesHost(scriptRegex, log, loader, instance, loaderOptions.appendTsSuffixTo, loaderOptions.appendTsxSuffixTo); + instance.languageService = compiler.createLanguageService(servicesHost, compiler.createDocumentRegistry()); + //} loader._compiler.plugin("after-compile", makeAfterCompile(instance, configFilePath)); loader._compiler.plugin("watch-run", makeWatchRun(instance)); diff --git a/src/interfaces.ts b/src/interfaces.ts index 36be8953a..3888963d1 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -233,6 +233,10 @@ export interface TSInstance { filesWithErrors?: TSFiles; transformers: typescript.CustomTransformers; colors: Chalk; + + watchMode?: typescript.WatchOfFilesAndCompilerOptions; + builderState?: typescript.BuilderState; + program?: typescript.Program; } export interface LoaderOptionsCache { diff --git a/src/servicesHost.ts b/src/servicesHost.ts index f49c6cac2..58884545d 100644 --- a/src/servicesHost.ts +++ b/src/servicesHost.ts @@ -5,7 +5,7 @@ import * as semver from 'semver'; import * as constants from './constants'; import * as logger from './logger'; import { makeResolver } from './resolver'; -import { appendSuffixesIfMatch, readFile } from './utils'; +import { appendSuffixesIfMatch, readFile, noop, notImplemented } from './utils'; import { ModuleResolutionHost, ResolvedModule, @@ -116,7 +116,7 @@ export function makeServicesHost( compiler.resolveTypeReferenceDirective(directive, containingFile, compilerOptions, moduleResolutionHost).resolvedTypeReferenceDirective), */ - resolveModuleNames: (moduleNames: string[], containingFile: string) => + resolveModuleNames: (moduleNames, containingFile) => resolveModuleNames( resolveSync, moduleResolutionHost, appendTsSuffixTo, appendTsxSuffixTo, scriptRegex, instance, moduleNames, containingFile, resolutionStrategy), @@ -127,6 +127,118 @@ export function makeServicesHost( return servicesHost; } +/** + * Create the TypeScript Watch host + */ +export function makeWatchHost( + scriptRegex: RegExp, + log: logger.Logger, + loader: Webpack, + instance: TSInstance, + appendTsSuffixTo: RegExp[], + appendTsxSuffixTo: RegExp[] +): typescript.WatchOfFilesAndCompilerOptionsHost { + const { compiler, compilerOptions, files } = instance; + + const newLine = + compilerOptions.newLine === constants.CarriageReturnLineFeedCode ? constants.CarriageReturnLineFeed : + compilerOptions.newLine === constants.LineFeedCode ? constants.LineFeed : + constants.EOL; + + // make a (sync) resolver that follows webpack's rules + const resolveSync = makeResolver(loader.options); + + const readFileWithFallback = compiler.sys === undefined || compiler.sys.readFile === undefined + ? readFile + : (path: string, encoding?: string | undefined): string | undefined => compiler.sys.readFile(path, encoding) || readFile(path, encoding); + + const moduleResolutionHost: ModuleResolutionHost = { + fileExists, + readFile: readFileWithFallback + }; + + // loader.context seems to work fine on Linux / Mac regardless causes problems for @types resolution on Windows for TypeScript < 2.3 + const getCurrentDirectory = (compiler!.version && semver.gte(compiler!.version, '2.3.0')) + ? () => loader.context + : () => process.cwd(); + + const resolutionStrategy = (compiler!.version && semver.gte(compiler!.version, '2.4.0')) + ? resolutionStrategyTS24AndAbove + : resolutionStrategyTS23AndBelow; + + const system: typescript.System = { + args: [], + newLine, + useCaseSensitiveFileNames: compiler.sys.useCaseSensitiveFileNames, + + getCurrentDirectory, + getExecutingFilePath: () => compiler.sys.getExecutingFilePath(), + + readFile: readFileWithFallback, + fileExists, + directoryExists: s => compiler.sys.directoryExists(path.normalize(s)), + getDirectories: s => compiler.sys.getDirectories(path.normalize(s)), + readDirectory: (s, extensions, exclude, include, depth) => compiler.sys.readDirectory(path.normalize(s), extensions, exclude, include, depth), + + resolvePath: s => compiler.sys.resolvePath(path.normalize(s)), + + write: s => log.logInfo(s), + + // All write operations are noop and we will deal with them separately + createDirectory: notImplemented, + writeFile: notImplemented, + + createHash, + + exit: noop, + + watchFile, + watchDirectory + + }; + //getCustomTransformers: () => instance.transformers + + const builderOptions: typescript.BuilderOptions = { + computeHash: s => createHash(s), + getCanonicalFileName: system.useCaseSensitiveFileNames ? (s => s) : (s => s.toLowerCase()) + }; + const watchHost: typescript.WatchOfFilesAndCompilerOptionsHost = { + rootFiles: Object.keys(files).filter(filePath => filePath.match(scriptRegex)), + options: compilerOptions, + moduleNameResolver: (moduleNames, containingFile) => + resolveModuleNames( + resolveSync, moduleResolutionHost, appendTsSuffixTo, appendTsxSuffixTo, scriptRegex, instance, + moduleNames, containingFile, resolutionStrategy), + system, + beforeProgramCreate: noop, + afterProgramCreate: (_host, program) => { + instance.program = program; + instance.builderState = compiler.createBuilderState(program, builderOptions, instance.builderState); + } + }; + return watchHost; + + function fileExists(s: string) { + s = path.normalize(s); + return !!files.hasOwnProperty(s) || compiler.sys.fileExists(s); + } + + function createHash(s: string) { + return compiler.sys.createHash ? compiler.sys.createHash(s) : s; + } + + function watchFile(_path: string, _callback: typescript.FileWatcherCallback, _pollingInterval?: number): typescript.FileWatcher { + return { + close: noop + }; + } + function watchDirectory(_path: string, _callback: typescript.DirectoryWatcherCallback, _recursive?: boolean): typescript.FileWatcher { + return { + close: noop + }; + } +} + function resolveModuleNames( resolveSync: ResolveSync, moduleResolutionHost: ModuleResolutionHost, diff --git a/src/utils.ts b/src/utils.ts index b6912f9a2..bb53a5267 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -118,6 +118,15 @@ export function appendSuffixesIfMatch(suffixDict: { [suffix: string]: RegExp[] } return path; } +/** Does nothing. */ +export function noop(_?: {} | null | undefined): void { } // tslint:disable-line no-empty +/** Throws an error because a function is not implemented. */ +export function notImplemented(): never { + throw new Error("Not implemented"); +} + +export const emptyArray: never[] = [] as never[]; + /** * Recursively collect all possible dependants of passed file */ From b8eb1a0b0f6d22a169a1d1d77aa5fba09b4d05c0 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Wed, 8 Nov 2017 14:43:54 -0800 Subject: [PATCH 2/7] Emit using watch mode if possible --- src/index.ts | 43 +++++++++++++++--- src/instances.ts | 16 ++++++- src/interfaces.ts | 13 +++++- src/servicesHost.ts | 104 ++++++++++++++++++++++++++++++++++++++------ src/utils.ts | 12 ++++- src/watch-run.ts | 5 ++- 6 files changed, 170 insertions(+), 23 deletions(-) diff --git a/src/index.ts b/src/index.ts index be3f3f2ff..f587f911d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ import * as path from 'path'; import * as loaderUtils from 'loader-utils'; +import * as typescript from 'typescript'; import { getTypeScriptInstance } from './instances'; import { appendSuffixesIfMatch, arrify, formatErrors, hasOwnProperty, registerWebpackErrors } from './utils'; @@ -165,16 +166,38 @@ function makeLoaderOptions(instanceName: string, configFileOptions: Partial{ version: 0 }; + file = instance.otherFiles[filePath]; + if (file !== undefined) { + delete instance.otherFiles[filePath]; + instance.files[filePath] = file; + } + else { + fileWatcherEventKind = instance.compiler.FileWatcherEventKind.Created; + file = instance.files[filePath] = { version: 0 }; + } + instance.changedFilesList = true; + } + + if (contents === undefined) { + fileWatcherEventKind === instance.compiler.FileWatcherEventKind.Deleted; } if (file.text !== contents) { file.version++; file.text = contents; instance.version!++; + if (instance.watchHost && fileWatcherEventKind === undefined) { + instance.watchHost.invokeFileWatcher(filePath, instance.compiler.FileWatcherEventKind.Changed); + } + } + + if (instance.watchHost && fileWatcherEventKind !== undefined) { + instance.watchHost.invokeFileWatcher(filePath, fileWatcherEventKind); + instance.watchHost.invokeDirectoryWatcher(path.dirname(filePath), filePath); } // push this file to modified files hash. @@ -191,8 +214,18 @@ function getEmit( instance: TSInstance, loader: Webpack ) { - // Emit Javascript - const output = instance.languageService!.getEmitOutput(filePath); + let outputFiles: typescript.OutputFile[]; + if (instance.program) { + outputFiles = []; + const writeFile = (fileName: string, text: string, writeByteOrderMark: boolean) => + outputFiles.push({ name: fileName, writeByteOrderMark, text }); + const sourceFile = instance.program.getSourceFile(rawFilePath); + instance.program.emit(sourceFile, writeFile, /*cancellationToken*/ undefined, /*emitOnlyDtsFiles*/ false, instance.transformers); + } + else { + // Emit Javascript + ({ outputFiles } = instance.languageService!.getEmitOutput(filePath)); + } loader.clearDependencies(); loader.addDependency(rawFilePath); @@ -216,10 +249,10 @@ function getEmit( .concat(additionalDependencies) .map(defFilePath => defFilePath + '@' + (instance.files[defFilePath] || { version: '?' }).version); - const outputFile = output.outputFiles.filter(outputFile => outputFile.name.match(constants.jsJsx)).pop(); + const outputFile = outputFiles.filter(outputFile => outputFile.name.match(constants.jsJsx)).pop(); const outputText = (outputFile) ? outputFile.text : undefined; - const sourceMapFile = output.outputFiles.filter(outputFile => outputFile.name.match(constants.jsJsxMap)).pop(); + const sourceMapFile = outputFiles.filter(outputFile => outputFile.name.match(constants.jsJsxMap)).pop(); const sourceMapText = (sourceMapFile) ? sourceMapFile.text : undefined; return { outputText, sourceMapText }; diff --git a/src/instances.ts b/src/instances.ts index 5fdf70719..bd0c62a74 100644 --- a/src/instances.ts +++ b/src/instances.ts @@ -34,6 +34,15 @@ export function getTypeScriptInstance( loader: Webpack ): { instance?: TSInstance, error?: WebpackError } { if (hasOwnProperty(instances, loaderOptions.instance)) { + const instance = instances[loaderOptions.instance]; + if (instance && instance.watchHost) { + if (instance.changedFilesList) { + instance.watchHost.updateRootFileNames(); + } + else if (instance.watchMode) { + instance.watchMode.synchronizeProgram(); + } + } return { instance: instances[loaderOptions.instance] }; } @@ -83,6 +92,7 @@ function successfulTypeScriptInstance( const compilerOptions = getCompilerOptions(configParseResult); const files: TSFiles = {}; + const otherFiles: TSFiles = {}; const getCustomTransformers = loaderOptions.getCustomTransformers || Function.prototype; @@ -99,11 +109,12 @@ function successfulTypeScriptInstance( formatErrors(diagnostics, loaderOptions, colors, compiler!, {file: configFilePath || 'tsconfig.json'})); } - const instance = { + const instance: TSInstance = { compiler, compilerOptions, loaderOptions, - files, + files, + otherFiles, dependencyGraph: {}, reverseDependencyGraph: {}, transformers: getCustomTransformers(), @@ -142,6 +153,7 @@ function successfulTypeScriptInstance( compilerOptions, loaderOptions, files, + otherFiles, languageService: null, version: 0, transformers: getCustomTransformers(), diff --git a/src/interfaces.ts b/src/interfaces.ts index 3888963d1..0a93fa0a8 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -214,6 +214,12 @@ export interface ModuleResolutionHost { readFile(fileName: string, encoding?: string | undefined): string | undefined; } +export interface WatchHost extends typescript.WatchOfFilesAndCompilerOptionsHost { + invokeFileWatcher(fileName: string, eventKind: typescript.FileWatcherEventKind): void; + invokeDirectoryWatcher(directory: string, fileAddedOrRemoved: string): void; + updateRootFileNames(): void; +} + export interface TSInstance { compiler: typeof typescript; compilerOptions: typescript.CompilerOptions; @@ -234,9 +240,14 @@ export interface TSInstance { transformers: typescript.CustomTransformers; colors: Chalk; + otherFiles: TSFiles; + + watchHost?: WatchHost; watchMode?: typescript.WatchOfFilesAndCompilerOptions; - builderState?: typescript.BuilderState; program?: typescript.Program; + builderState?: typescript.BuilderState; + + changedFilesList?: boolean; } export interface LoaderOptionsCache { diff --git a/src/servicesHost.ts b/src/servicesHost.ts index 58884545d..d5a09902f 100644 --- a/src/servicesHost.ts +++ b/src/servicesHost.ts @@ -5,8 +5,9 @@ import * as semver from 'semver'; import * as constants from './constants'; import * as logger from './logger'; import { makeResolver } from './resolver'; -import { appendSuffixesIfMatch, readFile, noop, notImplemented } from './utils'; +import { appendSuffixesIfMatch, readFile, noop, notImplemented, unorderedRemoveItem } from './utils'; import { + WatchHost, ModuleResolutionHost, ResolvedModule, ResolveSync, @@ -137,8 +138,8 @@ export function makeWatchHost( instance: TSInstance, appendTsSuffixTo: RegExp[], appendTsxSuffixTo: RegExp[] -): typescript.WatchOfFilesAndCompilerOptionsHost { - const { compiler, compilerOptions, files } = instance; +) { + const { compiler, compilerOptions, files, otherFiles } = instance; const newLine = compilerOptions.newLine === constants.CarriageReturnLineFeedCode ? constants.CarriageReturnLineFeed : @@ -166,6 +167,11 @@ export function makeWatchHost( ? resolutionStrategyTS24AndAbove : resolutionStrategyTS23AndBelow; + type WatchCallbacks = { [fileName: string]: T[] | undefined }; + const watchedFiles: WatchCallbacks = {}; + const watchedDirectories: WatchCallbacks = {}; + const watchedDirectoriesRecursive: WatchCallbacks = {}; + const system: typescript.System = { args: [], newLine, @@ -174,7 +180,7 @@ export function makeWatchHost( getCurrentDirectory, getExecutingFilePath: () => compiler.sys.getExecutingFilePath(), - readFile: readFileWithFallback, + readFile: readFileWithCachingText, fileExists, directoryExists: s => compiler.sys.directoryExists(path.normalize(s)), getDirectories: s => compiler.sys.getDirectories(path.normalize(s)), @@ -196,14 +202,13 @@ export function makeWatchHost( watchDirectory }; - //getCustomTransformers: () => instance.transformers const builderOptions: typescript.BuilderOptions = { computeHash: s => createHash(s), getCanonicalFileName: system.useCaseSensitiveFileNames ? (s => s) : (s => s.toLowerCase()) }; - const watchHost: typescript.WatchOfFilesAndCompilerOptionsHost = { - rootFiles: Object.keys(files).filter(filePath => filePath.match(scriptRegex)), + const watchHost: WatchHost = { + rootFiles: getRootFileNames(), options: compilerOptions, moduleNameResolver: (moduleNames, containingFile) => resolveModuleNames( @@ -214,10 +219,34 @@ export function makeWatchHost( afterProgramCreate: (_host, program) => { instance.program = program; instance.builderState = compiler.createBuilderState(program, builderOptions, instance.builderState); + }, + invokeFileWatcher, + invokeDirectoryWatcher, + updateRootFileNames: () => { + instance.changedFilesList = false; + if (instance.watchMode) { + instance.watchMode.updateRootFileNames(getRootFileNames()); + } } }; return watchHost; + function getRootFileNames() { + return Object.keys(files).filter(filePath => filePath.match(scriptRegex)); + } + + function readFileWithCachingText(fileName: string, encoding?: string) { + fileName = path.normalize(fileName); + let file = files[fileName] || otherFiles[fileName]; + if (file !== undefined) { + return file.text; + } + const text = readFileWithFallback(fileName, encoding); + if (text === undefined) { return undefined; } + otherFiles[fileName] = { version: 0, text }; + return text; + } + function fileExists(s: string) { s = path.normalize(s); return !!files.hasOwnProperty(s) || compiler.sys.fileExists(s); @@ -227,16 +256,65 @@ export function makeWatchHost( return compiler.sys.createHash ? compiler.sys.createHash(s) : s; } - function watchFile(_path: string, _callback: typescript.FileWatcherCallback, _pollingInterval?: number): typescript.FileWatcher { - return { - close: noop - }; + function invokeWatcherCallbacks(callbacks: typescript.FileWatcherCallback[] | undefined, fileName: string, eventKind: typescript.FileWatcherEventKind): void; + function invokeWatcherCallbacks(callbacks: typescript.DirectoryWatcherCallback[] | undefined, fileName: string): void; + function invokeWatcherCallbacks(callbacks: typescript.FileWatcherCallback[] | typescript.DirectoryWatcherCallback[] | undefined, fileName: string, eventKind?: typescript.FileWatcherEventKind) { + if (callbacks) { + // The array copy is made to ensure that even if one of the callback removes the callbacks, + // we dont miss any callbacks following it + const cbs = callbacks.slice(); + for (const cb of cbs) { + cb(fileName, eventKind as typescript.FileWatcherEventKind); + } + } } - function watchDirectory(_path: string, _callback: typescript.DirectoryWatcherCallback, _recursive?: boolean): typescript.FileWatcher { + + function invokeFileWatcher(fileName: string, eventKind: typescript.FileWatcherEventKind) { + fileName = path.normalize(fileName); + invokeWatcherCallbacks(watchedFiles[fileName], fileName, eventKind); + } + + function invokeDirectoryWatcher(directory: string, fileAddedOrRemoved: string) { + directory = path.normalize(directory); + invokeWatcherCallbacks(watchedDirectories[directory], fileAddedOrRemoved); + invokeRecursiveDirectoryWatcher(directory, fileAddedOrRemoved); + } + + function invokeRecursiveDirectoryWatcher(directory: string, fileAddedOrRemoved: string) { + directory = path.normalize(directory); + invokeWatcherCallbacks(watchedDirectoriesRecursive[directory], fileAddedOrRemoved); + const basePath = path.dirname(directory); + if (directory !== basePath) { + invokeRecursiveDirectoryWatcher(basePath, fileAddedOrRemoved); + } + } + + function createWatcher(file: string, callbacks: WatchCallbacks, callback: T): typescript.FileWatcher { + file = path.normalize(file); + const existing = callbacks[file]; + if (existing) { + existing.push(callback); + } + else { + callbacks[file] = [callback]; + } return { - close: noop + close: () => { + const existing = callbacks[file]; + if (existing) { + unorderedRemoveItem(existing, callback); + } + } }; } + + function watchFile(fileName: string, callback: typescript.FileWatcherCallback, _pollingInterval?: number) { + return createWatcher(fileName, watchedFiles, callback); + } + + function watchDirectory(fileName: string, callback: typescript.DirectoryWatcherCallback, recursive?: boolean) { + return createWatcher(fileName, recursive ? watchedDirectoriesRecursive : watchedDirectories, callback); + } } function resolveModuleNames( diff --git a/src/utils.ts b/src/utils.ts index bb53a5267..822c39c39 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -125,7 +125,17 @@ export function notImplemented(): never { throw new Error("Not implemented"); } -export const emptyArray: never[] = [] as never[]; +export function unorderedRemoveItem(array: T[], item: T): boolean { + for (let i = 0; i < array.length; i++) { + if (array[i] === item) { + // Fill in the "hole" left at `index`. + array[i] = array[array.length - 1]; + array.pop(); + return true; + } + } + return false; +} /** * Recursively collect all possible dependants of passed file diff --git a/src/watch-run.ts b/src/watch-run.ts index 90ec1917a..44952064c 100644 --- a/src/watch-run.ts +++ b/src/watch-run.ts @@ -29,12 +29,15 @@ export function makeWatchRun( .forEach(filePath => { lastTimes[filePath] = times[filePath]; filePath = path.normalize(filePath); - const file = instance.files[filePath]; + const file = instance.files[filePath] || instance.otherFiles[filePath]; if (file !== undefined) { file.text = readFile(filePath) || ''; file.version++; instance.version!++; instance.modifiedFiles![filePath] = file; + if (instance.watchHost) { + instance.watchHost.invokeFileWatcher(filePath, instance.compiler.FileWatcherEventKind.Changed); + } } }); cb(); From fa66eae633f90f53d7d5bc7248ad389620e26092 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Wed, 8 Nov 2017 15:55:49 -0800 Subject: [PATCH 3/7] Use the watch mode to report errors --- src/after-compile.ts | 36 +++++++++++++++++++++++------------- src/index.ts | 15 ++------------- src/instances.ts | 15 +++++++++++++++ src/interfaces.ts | 3 --- src/servicesHost.ts | 5 ----- src/watch-run.ts | 1 + 6 files changed, 41 insertions(+), 34 deletions(-) diff --git a/src/after-compile.ts b/src/after-compile.ts index e84f7e878..f2d79fe58 100644 --- a/src/after-compile.ts +++ b/src/after-compile.ts @@ -1,5 +1,4 @@ import * as path from 'path'; -import * as typescript from 'typescript'; import { collectAllDependants, formatErrors, hasOwnProperty, registerWebpackErrors } from './utils'; import * as constants from './constants'; @@ -10,6 +9,7 @@ import { WebpackError, WebpackModule } from './interfaces'; +import { getEmitOutput } from './instances'; export function makeAfterCompile( instance: TSInstance, @@ -38,7 +38,7 @@ export function makeAfterCompile( const filesWithErrors: TSFiles = {}; provideErrorsToWebpack(filesToCheckForErrors, filesWithErrors, compilation, modules, instance); - provideDeclarationFilesToWebpack(filesToCheckForErrors, instance.languageService!, compilation); + provideDeclarationFilesToWebpack(filesToCheckForErrors, instance, compilation); instance.filesWithErrors = filesWithErrors; instance.modifiedFiles = null; @@ -60,11 +60,13 @@ function provideCompilerOptionDiagnosticErrorsToWebpack( configFilePath: string | undefined ) { if (getCompilerOptionDiagnostics) { - const { languageService, loaderOptions, compiler } = instance; + const { languageService, loaderOptions, compiler, program } = instance; registerWebpackErrors( compilation.errors, formatErrors( - languageService!.getCompilerOptionsDiagnostics(), + program ? + program.getOptionsDiagnostics() : + languageService!.getCompilerOptionsDiagnostics(), loaderOptions, instance.colors, compiler, { file: configFilePath || 'tsconfig.json' })); } @@ -99,18 +101,23 @@ function determineFilesToCheckForErrors( checkAllFilesForErrors: boolean, instance: TSInstance ) { - const { files, modifiedFiles, filesWithErrors } = instance + const { files, modifiedFiles, filesWithErrors, otherFiles } = instance // calculate array of files to check let filesToCheckForErrors: TSFiles = {}; if (checkAllFilesForErrors) { // check all files on initial run - filesToCheckForErrors = files; + Object.keys(files).forEach(fileName => { + filesToCheckForErrors[fileName] = files[fileName]; + }); + Object.keys(otherFiles).forEach(fileName => { + filesToCheckForErrors[fileName] = otherFiles[fileName]; + }); } else if (modifiedFiles !== null && modifiedFiles !== undefined) { // check all modified files, and all dependants Object.keys(modifiedFiles).forEach(modifiedFileName => { collectAllDependants(instance.reverseDependencyGraph, modifiedFileName) .forEach(fileName => { - filesToCheckForErrors[fileName] = files[fileName]; + filesToCheckForErrors[fileName] = files[fileName] || otherFiles[fileName]; }); }); } @@ -131,16 +138,19 @@ function provideErrorsToWebpack( modules: Modules, instance: TSInstance ) { - const { compiler, languageService, files, loaderOptions, compilerOptions } = instance; + const { compiler, program, languageService, files, loaderOptions, compilerOptions, otherFiles } = instance; let filePathRegex = !!compilerOptions.checkJs ? constants.dtsTsTsxJsJsxRegex : constants.dtsTsTsxRegex; Object.keys(filesToCheckForErrors) .filter(filePath => filePath.match(filePathRegex)) .forEach(filePath => { - const errors = languageService!.getSyntacticDiagnostics(filePath).concat(languageService!.getSemanticDiagnostics(filePath)); + const sourceFile = program && program.getSourceFile(filePath); + const errors = program ? + program.getSyntacticDiagnostics(sourceFile).concat(program.getSemanticDiagnostics(sourceFile)) : + languageService!.getSyntacticDiagnostics(filePath).concat(languageService!.getSemanticDiagnostics(filePath)); if (errors.length > 0) { - filesWithErrors[filePath] = files[filePath]; + filesWithErrors[filePath] = files[filePath] || otherFiles[filePath]; } // if we have access to a webpack module, use that @@ -168,14 +178,14 @@ function provideErrorsToWebpack( */ function provideDeclarationFilesToWebpack( filesToCheckForErrors: TSFiles, - languageService: typescript.LanguageService, + instance: TSInstance, compilation: WebpackCompilation ) { Object.keys(filesToCheckForErrors) .filter(filePath => filePath.match(constants.tsTsxRegex)) .forEach(filePath => { - const output = languageService.getEmitOutput(filePath); - const declarationFile = output.outputFiles.filter(outputFile => outputFile.name.match(constants.dtsDtsxRegex)).pop(); + const outputFiles = getEmitOutput(instance, filePath); + const declarationFile = outputFiles.filter(outputFile => outputFile.name.match(constants.dtsDtsxRegex)).pop(); if (declarationFile !== undefined) { const assetPath = path.relative(compilation.compiler.context, declarationFile.name); compilation.assets[assetPath] = { diff --git a/src/index.ts b/src/index.ts index f587f911d..6e5e0c88a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ import * as path from 'path'; import * as loaderUtils from 'loader-utils'; import * as typescript from 'typescript'; -import { getTypeScriptInstance } from './instances'; +import { getTypeScriptInstance, getEmitOutput } from './instances'; import { appendSuffixesIfMatch, arrify, formatErrors, hasOwnProperty, registerWebpackErrors } from './utils'; import * as constants from './constants'; import { @@ -214,18 +214,7 @@ function getEmit( instance: TSInstance, loader: Webpack ) { - let outputFiles: typescript.OutputFile[]; - if (instance.program) { - outputFiles = []; - const writeFile = (fileName: string, text: string, writeByteOrderMark: boolean) => - outputFiles.push({ name: fileName, writeByteOrderMark, text }); - const sourceFile = instance.program.getSourceFile(rawFilePath); - instance.program.emit(sourceFile, writeFile, /*cancellationToken*/ undefined, /*emitOnlyDtsFiles*/ false, instance.transformers); - } - else { - // Emit Javascript - ({ outputFiles } = instance.languageService!.getEmitOutput(filePath)); - } + const outputFiles = getEmitOutput(instance, filePath); loader.clearDependencies(); loader.addDependency(rawFilePath); diff --git a/src/instances.ts b/src/instances.ts index bd0c62a74..d94070cbe 100644 --- a/src/instances.ts +++ b/src/instances.ts @@ -178,3 +178,18 @@ function successfulTypeScriptInstance( return { instance }; } + +export function getEmitOutput(instance: TSInstance, filePath: string) { + if (instance.program) { + const outputFiles: typescript.OutputFile[] = []; + const writeFile = (fileName: string, text: string, writeByteOrderMark: boolean) => + outputFiles.push({ name: fileName, writeByteOrderMark, text }); + const sourceFile = instance.program.getSourceFile(filePath); + instance.program.emit(sourceFile, writeFile, /*cancellationToken*/ undefined, /*emitOnlyDtsFiles*/ false, instance.transformers); + return outputFiles; + } + else { + // Emit Javascript + return instance.languageService!.getEmitOutput(filePath).outputFiles; + } +} diff --git a/src/interfaces.ts b/src/interfaces.ts index 0a93fa0a8..fa3fbac84 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -241,12 +241,9 @@ export interface TSInstance { colors: Chalk; otherFiles: TSFiles; - watchHost?: WatchHost; watchMode?: typescript.WatchOfFilesAndCompilerOptions; program?: typescript.Program; - builderState?: typescript.BuilderState; - changedFilesList?: boolean; } diff --git a/src/servicesHost.ts b/src/servicesHost.ts index d5a09902f..9515ebc6a 100644 --- a/src/servicesHost.ts +++ b/src/servicesHost.ts @@ -203,10 +203,6 @@ export function makeWatchHost( }; - const builderOptions: typescript.BuilderOptions = { - computeHash: s => createHash(s), - getCanonicalFileName: system.useCaseSensitiveFileNames ? (s => s) : (s => s.toLowerCase()) - }; const watchHost: WatchHost = { rootFiles: getRootFileNames(), options: compilerOptions, @@ -218,7 +214,6 @@ export function makeWatchHost( beforeProgramCreate: noop, afterProgramCreate: (_host, program) => { instance.program = program; - instance.builderState = compiler.createBuilderState(program, builderOptions, instance.builderState); }, invokeFileWatcher, invokeDirectoryWatcher, diff --git a/src/watch-run.ts b/src/watch-run.ts index 44952064c..06fefae00 100644 --- a/src/watch-run.ts +++ b/src/watch-run.ts @@ -13,6 +13,7 @@ import { export function makeWatchRun( instance: TSInstance ) { + // Called Before starting compilation after watch const lastTimes = {}; let startTime : number | null = null; return (watching: WebpackWatching, cb: () => void) => { From ff7ec658623b7485baf58b01719e7599b17fbc44 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Wed, 8 Nov 2017 16:08:37 -0800 Subject: [PATCH 4/7] Use watch Mode if available and dont create language service --- src/instances.ts | 11 ++++++----- src/interfaces.ts | 2 +- src/servicesHost.ts | 4 ++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/instances.ts b/src/instances.ts index d94070cbe..971ce78d0 100644 --- a/src/instances.ts +++ b/src/instances.ts @@ -39,8 +39,8 @@ export function getTypeScriptInstance( if (instance.changedFilesList) { instance.watchHost.updateRootFileNames(); } - else if (instance.watchMode) { - instance.watchMode.synchronizeProgram(); + else if (instance.watchOfFilesAndCompilerOptions) { + instance.watchOfFilesAndCompilerOptions.synchronizeProgram(); } } return { instance: instances[loaderOptions.instance] }; @@ -164,14 +164,15 @@ function successfulTypeScriptInstance( }; if (compiler.createWatch) { + console.log("Using watch api"); // If there is api available for watch, use it instead of language service const watchHost = makeWatchHost(scriptRegex, log, loader, instance, loaderOptions.appendTsSuffixTo, loaderOptions.appendTsxSuffixTo); - instance.watchMode = compiler.createWatch(watchHost); + instance.watchOfFilesAndCompilerOptions = compiler.createWatch(watchHost); } - //else { + else { const servicesHost = makeServicesHost(scriptRegex, log, loader, instance, loaderOptions.appendTsSuffixTo, loaderOptions.appendTsxSuffixTo); instance.languageService = compiler.createLanguageService(servicesHost, compiler.createDocumentRegistry()); - //} + } loader._compiler.plugin("after-compile", makeAfterCompile(instance, configFilePath)); loader._compiler.plugin("watch-run", makeWatchRun(instance)); diff --git a/src/interfaces.ts b/src/interfaces.ts index fa3fbac84..6f80162c3 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -242,7 +242,7 @@ export interface TSInstance { otherFiles: TSFiles; watchHost?: WatchHost; - watchMode?: typescript.WatchOfFilesAndCompilerOptions; + watchOfFilesAndCompilerOptions?: typescript.WatchOfFilesAndCompilerOptions; program?: typescript.Program; changedFilesList?: boolean; } diff --git a/src/servicesHost.ts b/src/servicesHost.ts index 9515ebc6a..423a33650 100644 --- a/src/servicesHost.ts +++ b/src/servicesHost.ts @@ -219,8 +219,8 @@ export function makeWatchHost( invokeDirectoryWatcher, updateRootFileNames: () => { instance.changedFilesList = false; - if (instance.watchMode) { - instance.watchMode.updateRootFileNames(getRootFileNames()); + if (instance.watchOfFilesAndCompilerOptions) { + instance.watchOfFilesAndCompilerOptions.updateRootFileNames(getRootFileNames()); } } }; From 259dbb3dce0512e9920c72f2dc0005b39cd4ba5c Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Wed, 6 Dec 2017 14:22:42 -0800 Subject: [PATCH 5/7] Update with reworked api --- src/instances.ts | 5 +++-- src/interfaces.ts | 2 +- src/servicesHost.ts | 52 +++++++++++++++------------------------------ src/utils.ts | 7 ------ 4 files changed, 21 insertions(+), 45 deletions(-) diff --git a/src/instances.ts b/src/instances.ts index 971ce78d0..857b8718b 100644 --- a/src/instances.ts +++ b/src/instances.ts @@ -39,8 +39,8 @@ export function getTypeScriptInstance( if (instance.changedFilesList) { instance.watchHost.updateRootFileNames(); } - else if (instance.watchOfFilesAndCompilerOptions) { - instance.watchOfFilesAndCompilerOptions.synchronizeProgram(); + if (instance.watchOfFilesAndCompilerOptions) { + instance.program = instance.watchOfFilesAndCompilerOptions.getProgram(); } } return { instance: instances[loaderOptions.instance] }; @@ -168,6 +168,7 @@ function successfulTypeScriptInstance( // If there is api available for watch, use it instead of language service const watchHost = makeWatchHost(scriptRegex, log, loader, instance, loaderOptions.appendTsSuffixTo, loaderOptions.appendTsxSuffixTo); instance.watchOfFilesAndCompilerOptions = compiler.createWatch(watchHost); + instance.program = instance.watchOfFilesAndCompilerOptions.getProgram(); } else { const servicesHost = makeServicesHost(scriptRegex, log, loader, instance, loaderOptions.appendTsSuffixTo, loaderOptions.appendTsxSuffixTo); diff --git a/src/interfaces.ts b/src/interfaces.ts index 6f80162c3..0ad51269d 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -214,7 +214,7 @@ export interface ModuleResolutionHost { readFile(fileName: string, encoding?: string | undefined): string | undefined; } -export interface WatchHost extends typescript.WatchOfFilesAndCompilerOptionsHost { +export interface WatchHost extends typescript.WatchCompilerHostOfFilesAndCompilerOptions { invokeFileWatcher(fileName: string, eventKind: typescript.FileWatcherEventKind): void; invokeDirectoryWatcher(directory: string, fileAddedOrRemoved: string): void; updateRootFileNames(): void; diff --git a/src/servicesHost.ts b/src/servicesHost.ts index 423a33650..29839618c 100644 --- a/src/servicesHost.ts +++ b/src/servicesHost.ts @@ -5,7 +5,7 @@ import * as semver from 'semver'; import * as constants from './constants'; import * as logger from './logger'; import { makeResolver } from './resolver'; -import { appendSuffixesIfMatch, readFile, noop, notImplemented, unorderedRemoveItem } from './utils'; +import { appendSuffixesIfMatch, readFile, unorderedRemoveItem } from './utils'; import { WatchHost, ModuleResolutionHost, @@ -172,49 +172,31 @@ export function makeWatchHost( const watchedDirectories: WatchCallbacks = {}; const watchedDirectoriesRecursive: WatchCallbacks = {}; - const system: typescript.System = { - args: [], - newLine, - useCaseSensitiveFileNames: compiler.sys.useCaseSensitiveFileNames, + const watchHost: WatchHost = { + rootFiles: getRootFileNames(), + options: compilerOptions, + useCaseSensitiveFileNames: () => compiler.sys.useCaseSensitiveFileNames, + getNewLine: () => newLine, getCurrentDirectory, - getExecutingFilePath: () => compiler.sys.getExecutingFilePath(), + getDefaultLibFileName, - readFile: readFileWithCachingText, fileExists, + readFile: readFileWithCachingText, directoryExists: s => compiler.sys.directoryExists(path.normalize(s)), getDirectories: s => compiler.sys.getDirectories(path.normalize(s)), readDirectory: (s, extensions, exclude, include, depth) => compiler.sys.readDirectory(path.normalize(s), extensions, exclude, include, depth), - - resolvePath: s => compiler.sys.resolvePath(path.normalize(s)), - - write: s => log.logInfo(s), - - // All write operations are noop and we will deal with them separately - createDirectory: notImplemented, - writeFile: notImplemented, - - createHash, - - exit: noop, + realpath: s => compiler.sys.resolvePath(path.normalize(s)), + trace: s => log.logInfo(s), watchFile, - watchDirectory + watchDirectory, - }; - - const watchHost: WatchHost = { - rootFiles: getRootFileNames(), - options: compilerOptions, - moduleNameResolver: (moduleNames, containingFile) => + resolveModuleNames: (moduleNames, containingFile) => resolveModuleNames( resolveSync, moduleResolutionHost, appendTsSuffixTo, appendTsxSuffixTo, scriptRegex, instance, moduleNames, containingFile, resolutionStrategy), - system, - beforeProgramCreate: noop, - afterProgramCreate: (_host, program) => { - instance.program = program; - }, + invokeFileWatcher, invokeDirectoryWatcher, updateRootFileNames: () => { @@ -226,6 +208,10 @@ export function makeWatchHost( }; return watchHost; + function getDefaultLibFileName(options: typescript.CompilerOptions) { + return path.join(path.dirname(compiler.sys.getExecutingFilePath()), compiler.getDefaultLibFileName(options)); + } + function getRootFileNames() { return Object.keys(files).filter(filePath => filePath.match(scriptRegex)); } @@ -247,10 +233,6 @@ export function makeWatchHost( return !!files.hasOwnProperty(s) || compiler.sys.fileExists(s); } - function createHash(s: string) { - return compiler.sys.createHash ? compiler.sys.createHash(s) : s; - } - function invokeWatcherCallbacks(callbacks: typescript.FileWatcherCallback[] | undefined, fileName: string, eventKind: typescript.FileWatcherEventKind): void; function invokeWatcherCallbacks(callbacks: typescript.DirectoryWatcherCallback[] | undefined, fileName: string): void; function invokeWatcherCallbacks(callbacks: typescript.FileWatcherCallback[] | typescript.DirectoryWatcherCallback[] | undefined, fileName: string, eventKind?: typescript.FileWatcherEventKind) { diff --git a/src/utils.ts b/src/utils.ts index 822c39c39..7cf3040f2 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -118,13 +118,6 @@ export function appendSuffixesIfMatch(suffixDict: { [suffix: string]: RegExp[] } return path; } -/** Does nothing. */ -export function noop(_?: {} | null | undefined): void { } // tslint:disable-line no-empty -/** Throws an error because a function is not implemented. */ -export function notImplemented(): never { - throw new Error("Not implemented"); -} - export function unorderedRemoveItem(array: T[], item: T): boolean { for (let i = 0; i < array.length; i++) { if (array[i] === item) { From b2d6c2abad7d6349a880e4c121d5b67378deb683 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 7 Dec 2017 19:51:40 -0800 Subject: [PATCH 6/7] Update to WatchProgram api --- src/instances.ts | 4 ++-- src/interfaces.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/instances.ts b/src/instances.ts index 857b8718b..fb9dd7629 100644 --- a/src/instances.ts +++ b/src/instances.ts @@ -163,11 +163,11 @@ function successfulTypeScriptInstance( colors }; - if (compiler.createWatch) { + if (compiler.createWatchProgram) { console.log("Using watch api"); // If there is api available for watch, use it instead of language service const watchHost = makeWatchHost(scriptRegex, log, loader, instance, loaderOptions.appendTsSuffixTo, loaderOptions.appendTsxSuffixTo); - instance.watchOfFilesAndCompilerOptions = compiler.createWatch(watchHost); + instance.watchOfFilesAndCompilerOptions = compiler.createWatchProgram(watchHost); instance.program = instance.watchOfFilesAndCompilerOptions.getProgram(); } else { diff --git a/src/interfaces.ts b/src/interfaces.ts index 0ad51269d..c1d686742 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -242,7 +242,7 @@ export interface TSInstance { otherFiles: TSFiles; watchHost?: WatchHost; - watchOfFilesAndCompilerOptions?: typescript.WatchOfFilesAndCompilerOptions; + watchOfFilesAndCompilerOptions?: typescript.WatchOfFilesAndCompilerOptions; program?: typescript.Program; changedFilesList?: boolean; } From 42e04a5aa1cc782cfc65bc7d372a21b2292c444c Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 19 Jan 2018 15:37:43 -0800 Subject: [PATCH 7/7] Use createProgram api of the watch compiler host --- src/instances.ts | 4 ++-- src/interfaces.ts | 4 ++-- src/servicesHost.ts | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/instances.ts b/src/instances.ts index fb9dd7629..d3976bf81 100644 --- a/src/instances.ts +++ b/src/instances.ts @@ -40,7 +40,7 @@ export function getTypeScriptInstance( instance.watchHost.updateRootFileNames(); } if (instance.watchOfFilesAndCompilerOptions) { - instance.program = instance.watchOfFilesAndCompilerOptions.getProgram(); + instance.program = instance.watchOfFilesAndCompilerOptions.getProgram().getProgram(); } } return { instance: instances[loaderOptions.instance] }; @@ -168,7 +168,7 @@ function successfulTypeScriptInstance( // If there is api available for watch, use it instead of language service const watchHost = makeWatchHost(scriptRegex, log, loader, instance, loaderOptions.appendTsSuffixTo, loaderOptions.appendTsxSuffixTo); instance.watchOfFilesAndCompilerOptions = compiler.createWatchProgram(watchHost); - instance.program = instance.watchOfFilesAndCompilerOptions.getProgram(); + instance.program = instance.watchOfFilesAndCompilerOptions.getProgram().getProgram(); } else { const servicesHost = makeServicesHost(scriptRegex, log, loader, instance, loaderOptions.appendTsSuffixTo, loaderOptions.appendTsxSuffixTo); diff --git a/src/interfaces.ts b/src/interfaces.ts index c1d686742..a2c19ad82 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -214,7 +214,7 @@ export interface ModuleResolutionHost { readFile(fileName: string, encoding?: string | undefined): string | undefined; } -export interface WatchHost extends typescript.WatchCompilerHostOfFilesAndCompilerOptions { +export interface WatchHost extends typescript.WatchCompilerHostOfFilesAndCompilerOptions { invokeFileWatcher(fileName: string, eventKind: typescript.FileWatcherEventKind): void; invokeDirectoryWatcher(directory: string, fileAddedOrRemoved: string): void; updateRootFileNames(): void; @@ -242,7 +242,7 @@ export interface TSInstance { otherFiles: TSFiles; watchHost?: WatchHost; - watchOfFilesAndCompilerOptions?: typescript.WatchOfFilesAndCompilerOptions; + watchOfFilesAndCompilerOptions?: typescript.WatchOfFilesAndCompilerOptions; program?: typescript.Program; changedFilesList?: boolean; } diff --git a/src/servicesHost.ts b/src/servicesHost.ts index 29839618c..c25a0e8cd 100644 --- a/src/servicesHost.ts +++ b/src/servicesHost.ts @@ -204,7 +204,8 @@ export function makeWatchHost( if (instance.watchOfFilesAndCompilerOptions) { instance.watchOfFilesAndCompilerOptions.updateRootFileNames(getRootFileNames()); } - } + }, + createProgram: compiler.createAbstractBuilder }; return watchHost;