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 be3f3f2ff..6e5e0c88a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,8 @@ 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 { @@ -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,7 @@ function getEmit( instance: TSInstance, loader: Webpack ) { - // Emit Javascript - const output = instance.languageService!.getEmitOutput(filePath); + const outputFiles = getEmitOutput(instance, filePath); loader.clearDependencies(); loader.addDependency(rawFilePath); @@ -216,10 +238,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 67ce6fcc6..d3976bf81 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, @@ -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(); + } + if (instance.watchOfFilesAndCompilerOptions) { + instance.program = instance.watchOfFilesAndCompilerOptions.getProgram().getProgram(); + } + } 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; @@ -90,20 +100,21 @@ 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'})); } - 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(), @@ -151,11 +163,35 @@ function successfulTypeScriptInstance( colors }; - const servicesHost = makeServicesHost(scriptRegex, log, loader, instance, loaderOptions.appendTsSuffixTo, loaderOptions.appendTsxSuffixTo); - instance.languageService = compiler.createLanguageService(servicesHost, compiler.createDocumentRegistry()); + 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.createWatchProgram(watchHost); + instance.program = instance.watchOfFilesAndCompilerOptions.getProgram().getProgram(); + } + 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)); 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 36be8953a..a2c19ad82 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.WatchCompilerHostOfFilesAndCompilerOptions { + invokeFileWatcher(fileName: string, eventKind: typescript.FileWatcherEventKind): void; + invokeDirectoryWatcher(directory: string, fileAddedOrRemoved: string): void; + updateRootFileNames(): void; +} + export interface TSInstance { compiler: typeof typescript; compilerOptions: typescript.CompilerOptions; @@ -233,6 +239,12 @@ export interface TSInstance { filesWithErrors?: TSFiles; transformers: typescript.CustomTransformers; colors: Chalk; + + otherFiles: TSFiles; + watchHost?: WatchHost; + watchOfFilesAndCompilerOptions?: typescript.WatchOfFilesAndCompilerOptions; + program?: typescript.Program; + changedFilesList?: boolean; } export interface LoaderOptionsCache { diff --git a/src/servicesHost.ts b/src/servicesHost.ts index f49c6cac2..c25a0e8cd 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 } from './utils'; +import { appendSuffixesIfMatch, readFile, unorderedRemoveItem } from './utils'; import { + WatchHost, ModuleResolutionHost, ResolvedModule, ResolveSync, @@ -116,7 +117,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 +128,173 @@ 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[] +) { + const { compiler, compilerOptions, files, otherFiles } = 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; + + type WatchCallbacks = { [fileName: string]: T[] | undefined }; + const watchedFiles: WatchCallbacks = {}; + const watchedDirectories: WatchCallbacks = {}; + const watchedDirectoriesRecursive: WatchCallbacks = {}; + + const watchHost: WatchHost = { + rootFiles: getRootFileNames(), + options: compilerOptions, + + useCaseSensitiveFileNames: () => compiler.sys.useCaseSensitiveFileNames, + getNewLine: () => newLine, + getCurrentDirectory, + getDefaultLibFileName, + + 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), + realpath: s => compiler.sys.resolvePath(path.normalize(s)), + trace: s => log.logInfo(s), + + watchFile, + watchDirectory, + + resolveModuleNames: (moduleNames, containingFile) => + resolveModuleNames( + resolveSync, moduleResolutionHost, appendTsSuffixTo, appendTsxSuffixTo, scriptRegex, instance, + moduleNames, containingFile, resolutionStrategy), + + invokeFileWatcher, + invokeDirectoryWatcher, + updateRootFileNames: () => { + instance.changedFilesList = false; + if (instance.watchOfFilesAndCompilerOptions) { + instance.watchOfFilesAndCompilerOptions.updateRootFileNames(getRootFileNames()); + } + }, + createProgram: compiler.createAbstractBuilder + }; + 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)); + } + + 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); + } + + 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 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: () => { + 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( resolveSync: ResolveSync, moduleResolutionHost: ModuleResolutionHost, diff --git a/src/utils.ts b/src/utils.ts index b6912f9a2..7cf3040f2 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -118,6 +118,18 @@ export function appendSuffixesIfMatch(suffixDict: { [suffix: string]: RegExp[] } return path; } +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..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) => { @@ -29,12 +30,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();