From 69c6026e239e30f885eac6aaf1c3612af30c574e Mon Sep 17 00:00:00 2001 From: John Reilly Date: Wed, 26 Oct 2016 18:27:07 +0100 Subject: [PATCH 01/23] Started refactor again --- .gitignore | 5 +- .npmignore | 1 + .travis.yml | 4 + appveyor.yml | 2 +- index.js | 3 + index.ts | 768 ------------------ package.json | 7 +- src/after-compile.ts | 164 ++++ src/config.ts | 92 +++ src/constants.ts | 5 + src/index.ts | 297 +++++++ src/interfaces.ts | 156 ++++ src/logger.ts | 67 ++ resolver.ts => src/resolver.ts | 7 +- src/servicesHost.ts | 118 +++ src/tsconfig.json | 9 + src/tslint.json | 27 + {typings => src/typings}/arrify/arrify.d.ts | 0 {typings => src/typings}/colors/colors.d.ts | 0 .../typings}/loaderUtils/loaderUtils.d.ts | 0 {typings => src/typings}/node/node.d.ts | 0 .../typings}/objectAssign/objectAssign.d.ts | 0 src/utils.ts | 61 ++ src/watch-run.ts | 34 + .../dependencyErrors/tsconfig.json | 3 +- test/comparison-tests/errors/tsconfig.json | 3 +- .../ignoreDiagnostics/tsconfig.json | 3 +- .../importsWatch/tsconfig.json | 3 +- test/comparison-tests/node/app.ts | 2 +- .../node/expectedOutput-2.0/bundle.js | 2 +- .../expectedOutput-2.0/bundle.transpiled.js | 2 +- .../expectedOutput-2.0/output.transpiled.txt | 4 +- .../node/expectedOutput-2.0/output.txt | 6 +- test/comparison-tests/node/tsconfig.json | 3 +- test/comparison-tests/nolib/tsconfig.json | 3 +- test/comparison-tests/npmLink/tsconfig.json | 3 +- .../replacement/tsconfig.json | 3 +- .../simpleDependency/tsconfig.json | 3 +- .../comparison-tests/sourceMaps/tsconfig.json | 3 +- .../typeSystemWatch/tsconfig.json | 4 +- .../simpleDependency/tsconfig.json | 5 +- 41 files changed, 1073 insertions(+), 809 deletions(-) create mode 100644 index.js delete mode 100644 index.ts create mode 100644 src/after-compile.ts create mode 100644 src/config.ts create mode 100644 src/constants.ts create mode 100644 src/index.ts create mode 100644 src/interfaces.ts create mode 100644 src/logger.ts rename resolver.ts => src/resolver.ts (93%) create mode 100644 src/servicesHost.ts create mode 100644 src/tsconfig.json create mode 100644 src/tslint.json rename {typings => src/typings}/arrify/arrify.d.ts (100%) rename {typings => src/typings}/colors/colors.d.ts (100%) rename {typings => src/typings}/loaderUtils/loaderUtils.d.ts (100%) rename {typings => src/typings}/node/node.d.ts (100%) rename {typings => src/typings}/objectAssign/objectAssign.d.ts (100%) create mode 100644 src/utils.ts create mode 100644 src/watch-run.ts diff --git a/.gitignore b/.gitignore index a213b918c..884610dd2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ /*.js +!index.js +/src/*.js /*.d.ts /*.log *.js.map @@ -8,4 +10,5 @@ npm-debug.log /test/execution-tests/**/typings !/test/**/expectedOutput-*/** /node_modules -!build.js \ No newline at end of file +/dist +!build.js diff --git a/.npmignore b/.npmignore index 534661f31..d40cd89f4 100644 --- a/.npmignore +++ b/.npmignore @@ -1,4 +1,5 @@ *.ts test .* +src typings \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 1dacf8ecf..899e5cddb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,13 @@ language: node_js node_js: + - "4.0" + - "5.0" - "6.0" sudo: false install: + - npm install npm -g - npm install + - npm run build - npm install $TYPESCRIPT env: - TYPESCRIPT=typescript@1.6.2 diff --git a/appveyor.yml b/appveyor.yml index 2736ac3e3..a91901261 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -9,10 +9,10 @@ environment: install: - ps: Install-Product node $env:nodejs_version - npm install + - npm run build - npm install %TYPESCRIPT% test_script: - node --version - npm --version - - npm run build - npm test build: off diff --git a/index.js b/index.js new file mode 100644 index 000000000..8a47bdc5c --- /dev/null +++ b/index.js @@ -0,0 +1,3 @@ +var loader = require('./dist'); + +module.exports = loader; \ No newline at end of file diff --git a/index.ts b/index.ts deleted file mode 100644 index c6b676235..000000000 --- a/index.ts +++ /dev/null @@ -1,768 +0,0 @@ -/// -/// -/// -/// -/// -import typescript = require('typescript'); -import path = require('path'); -import fs = require('fs'); -import os = require('os'); -import loaderUtils = require('loader-utils'); -import objectAssign = require('object-assign'); -import arrify = require('arrify'); -import makeResolver = require('./resolver'); -var Console = require('console').Console; -var semver = require('semver') -require('colors'); - -const stderrConsole = new Console(process.stderr); -const stdoutConsole = new Console(process.stdout); - -var pushArray = function(arr, toPush) { - Array.prototype.splice.apply(arr, [0, 0].concat(toPush)); -} - -function hasOwnProperty(obj, property) { - return Object.prototype.hasOwnProperty.call(obj, property) -} - -enum LogLevel { - INFO = 1, - WARN = 2, - ERROR = 3 -} - -interface LoaderOptions { - silent: boolean; - logLevel: string; - logInfoToStdOut: boolean; - instance: string; - compiler: string; - configFileName: string; - transpileOnly: boolean; - ignoreDiagnostics: number[]; - compilerOptions: typescript.CompilerOptions; -} - -interface TSFile { - text: string; - version: number; -} - -interface TSFiles { - [fileName: string]: TSFile; -} - -interface DependencyGraph { - [index: string]: string[] -} - -interface ReverseDependencyGraph { - [index: string]: { - [index: string]: boolean - } -} - -interface TSInstance { - compiler: typeof typescript; - compilerOptions: typescript.CompilerOptions; - loaderOptions: LoaderOptions; - files: TSFiles; - languageService?: typescript.LanguageService; - version?: number; - dependencyGraph: DependencyGraph; - reverseDependencyGraph: ReverseDependencyGraph; - modifiedFiles?: TSFiles; - filesWithErrors?: TSFiles; -} - -interface TSInstances { - [name: string]: TSInstance; -} - -interface WebpackError { - module?: any; - file?: string; - message: string; - rawMessage: string; - location?: {line: number, character: number}; - loaderSource: string; -} - -interface ResolvedModule { - resolvedFileName: string; - resolvedModule?: ResolvedModule; - isExternalLibraryImport?: boolean; -} - -interface TSCompatibleCompiler { - // typescript@next 1.7+ - readConfigFile(fileName: string, readFile: (path: string) => string): { - config?: any; - error?: typescript.Diagnostic; - }; - // typescript@latest 1.6.2 - readConfigFile(fileName: string): { - config?: any; - error?: typescript.Diagnostic; - }; - // typescript@next 1.8+ - parseJsonConfigFileContent?(json: any, host: typescript.ParseConfigHost, basePath: string): typescript.ParsedCommandLine; - // typescript@latest 1.6.2 - parseConfigFile?(json: any, host: typescript.ParseConfigHost, basePath: string): typescript.ParsedCommandLine; -} - -var instances = {}; -var webpackInstances = []; -let scriptRegex = /\.tsx?$/i; - -// Take TypeScript errors, parse them and format to webpack errors -// Optionally adds a file name -function formatErrors(diagnostics: typescript.Diagnostic[], instance: TSInstance, merge?: any): WebpackError[] { - return diagnostics - .filter(diagnostic => instance.loaderOptions.ignoreDiagnostics.indexOf(diagnostic.code) == -1) - .map(diagnostic => { - var errorCategory = instance.compiler.DiagnosticCategory[diagnostic.category].toLowerCase(); - var errorCategoryAndCode = errorCategory + ' TS' + diagnostic.code + ': '; - - var messageText = errorCategoryAndCode + instance.compiler.flattenDiagnosticMessageText(diagnostic.messageText, os.EOL); - if (diagnostic.file) { - var lineChar = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); - return { - message: `${'('.white}${(lineChar.line+1).toString().cyan},${(lineChar.character+1).toString().cyan}): ${messageText.red}`, - rawMessage: messageText, - location: {line: lineChar.line+1, character: lineChar.character+1}, - loaderSource: 'ts-loader' - }; - } - else { - return { - message:`${messageText.red}`, - rawMessage: messageText, - loaderSource: 'ts-loader' - }; - } - }) - .map(error => objectAssign(error, merge)); -} - -// The tsconfig.json is found using the same method as `tsc`, starting in the current directory -// and continuing up the parent directory chain. -function findConfigFile(compiler: typeof typescript, searchPath: string, configFileName: string): string { - while (true) { - var fileName = path.join(searchPath, configFileName); - if (compiler.sys.fileExists(fileName)) { - return fileName; - } - var parentPath = path.dirname(searchPath); - if (parentPath === searchPath) { - break; - } - searchPath = parentPath; - } - return undefined; -} - -// The loader is executed once for each file seen by webpack. However, we need to keep -// a persistent instance of TypeScript that contains all of the files in the program -// along with definition files and options. This function either creates an instance -// or returns the existing one. Multiple instances are possible by using the -// `instance` property. -function ensureTypeScriptInstance(loaderOptions: LoaderOptions, loader: any): { instance?: TSInstance, error?: WebpackError } { - function log(...messages: string[]): void { - logToConsole(loaderOptions.logInfoToStdOut ? stdoutConsole : stderrConsole, messages); - } - - function logToConsole(logConsole:any, messages: string[]): void { - if (!loaderOptions.silent) { - console.log.apply(logConsole, messages); - } - } - - function logInfo(...messages: string[]): void { - if (LogLevel[loaderOptions.logLevel] <= LogLevel.INFO) { - logToConsole(loaderOptions.logInfoToStdOut ? stdoutConsole : stderrConsole, messages); - } - } - - function logError(...messages: string[]): void { - if (LogLevel[loaderOptions.logLevel] <= LogLevel.ERROR) { - logToConsole(stderrConsole, messages); - } - } - - function logWarning(...messages: string[]): void { - if (LogLevel[loaderOptions.logLevel] <= LogLevel.WARN) { - logToConsole(stderrConsole, messages); - } - } - - if (hasOwnProperty(instances, loaderOptions.instance)) { - return { instance: instances[loaderOptions.instance] }; - } - - try { - var compiler: typeof typescript = require(loaderOptions.compiler); - } - catch (e) { - let message = loaderOptions.compiler == 'typescript' - ? 'Could not load TypeScript. Try installing with `npm install typescript`. If TypeScript is installed globally, try using `npm link typescript`.' - : `Could not load TypeScript compiler with NPM package name \`${loaderOptions.compiler}\`. Are you sure it is correctly installed?` - return { error: { - message: message.red, - rawMessage: message, - loaderSource: 'ts-loader' - } }; - } - - var motd = `ts-loader: Using ${loaderOptions.compiler}@${compiler.version}`, - compilerCompatible = false; - if (loaderOptions.compiler == 'typescript') { - if (compiler.version && semver.gte(compiler.version, '1.6.2-0')) { - // don't log yet in this case, if a tsconfig.json exists we want to combine the message - compilerCompatible = true; - } - else { - logError(`${motd}. This version is incompatible with ts-loader. Please upgrade to the latest version of TypeScript.`.red); - } - } - else { - logWarning(`${motd}. This version may or may not be compatible with ts-loader.`.yellow); - } - - var files = {}; - var instance: TSInstance = instances[loaderOptions.instance] = { - compiler, - compilerOptions: null, - loaderOptions, - files, - languageService: null, - version: 0, - dependencyGraph: {}, - reverseDependencyGraph: {}, - modifiedFiles: null - }; - - var compilerOptions: typescript.CompilerOptions = { - skipDefaultLibCheck: true, - suppressOutputPathCheck: true // This is why: https://github.com/Microsoft/TypeScript/issues/7363 - }; - - // Load any available tsconfig.json file - var filesToLoad = []; - var configFilePath = findConfigFile(compiler, path.dirname(loader.resourcePath), loaderOptions.configFileName); - var configFile: { - config?: any; - error?: typescript.Diagnostic; - }; - if (configFilePath) { - if (compilerCompatible) logInfo(`${motd} and ${configFilePath}`.green) - else logInfo(`ts-loader: Using config file at ${configFilePath}`.green) - - // HACK: relies on the fact that passing an extra argument won't break - // the old API that has a single parameter - configFile = (compiler).readConfigFile( - configFilePath, - compiler.sys.readFile - ); - - if (configFile.error) { - var configFileError = formatErrors([configFile.error], instance, {file: configFilePath })[0]; - return { error: configFileError } - } - } - else { - if (compilerCompatible) logInfo(motd.green) - - configFile = { - config: { - compilerOptions: {}, - files: [] - } - } - } - - configFile.config.compilerOptions = objectAssign({}, - configFile.config.compilerOptions, - loaderOptions.compilerOptions); - - // do any necessary config massaging - if (loaderOptions.transpileOnly) { - configFile.config.compilerOptions.isolatedModules = true; - } - - // if allowJs is set then we should accept js(x) files - if (configFile.config.compilerOptions.allowJs) { - scriptRegex = /\.tsx?$|\.jsx?$/i; - } - - var configParseResult; - if (typeof (compiler).parseJsonConfigFileContent === 'function') { - // parseConfigFile was renamed between 1.6.2 and 1.7 - configParseResult = (compiler).parseJsonConfigFileContent( - configFile.config, - compiler.sys, - path.dirname(configFilePath || '') - ); - } else { - configParseResult = (compiler).parseConfigFile( - configFile.config, - compiler.sys, - path.dirname(configFilePath || '') - ); - } - - if (configParseResult.errors.length) { - pushArray( - loader._module.errors, - formatErrors(configParseResult.errors, instance, { file: configFilePath })); - - return { error: { - file: configFilePath, - message: 'error while parsing tsconfig.json'.red, - rawMessage: 'error while parsing tsconfig.json', - loaderSource: 'ts-loader' - }}; - } - - instance.compilerOptions = objectAssign(compilerOptions, configParseResult.options); - filesToLoad = configParseResult.fileNames; - - // if `module` is not specified and not using ES6 target, default to CJS module output - if (compilerOptions.module == null && compilerOptions.target !== 2 /* ES6 */) { - compilerOptions.module = 1 /* CommonJS */ - } - // special handling for TS 1.6 and target: es6 - else if (compilerCompatible && semver.lt(compiler.version, '1.7.3-0') && compilerOptions.target == 2 /* ES6 */) { - compilerOptions.module = 0 /* None */; - } - - if (loaderOptions.transpileOnly) { - // quick return for transpiling - // we do need to check for any issues with TS options though - var program = compiler.createProgram([], compilerOptions), - diagnostics = program.getOptionsDiagnostics(); - - pushArray( - loader._module.errors, - formatErrors(diagnostics, instance, {file: configFilePath || 'tsconfig.json'})); - - return { instance: instances[loaderOptions.instance] = { compiler, compilerOptions, loaderOptions, files, dependencyGraph: {}, reverseDependencyGraph: {} }}; - } - - // Load initial files (core lib files, any files specified in tsconfig.json) - let filePath: string; - try { - filesToLoad.forEach(fp => { - filePath = path.normalize(fp); - files[filePath] = { - text: fs.readFileSync(filePath, 'utf-8'), - version: 0 - }; - }); - } - catch (exc) { - let filePathError = `A file specified in tsconfig.json could not be found: ${ filePath }`; - return { error: { - message: filePathError.red, - rawMessage: filePathError, - loaderSource: 'ts-loader' - }}; - } - - let newLine = - compilerOptions.newLine === 0 /* CarriageReturnLineFeed */ ? '\r\n' : - compilerOptions.newLine === 1 /* LineFeed */ ? '\n' : - os.EOL; - - // make a (sync) resolver that follows webpack's rules - let resolver = makeResolver(loader.options); - - var readFile = function(fileName) { - fileName = path.normalize(fileName); - try { - return fs.readFileSync(fileName, {encoding: 'utf8'}) - } - catch (e) { - return; - } - } - - var moduleResolutionHost = { - fileExists: (fileName: string) => readFile(fileName) !== undefined, - readFile: (fileName: string) => readFile(fileName) - }; - - // Create the TypeScript language service - var servicesHost = { - getProjectVersion: () => instance.version+'', - getScriptFileNames: () => Object.keys(files).filter(filePath => scriptRegex.test(filePath)), - getScriptVersion: fileName => { - fileName = path.normalize(fileName); - return files[fileName] && files[fileName].version.toString(); - }, - getScriptSnapshot: fileName => { - // This is called any time TypeScript needs a file's text - // We either load from memory or from disk - fileName = path.normalize(fileName); - var file = files[fileName]; - - if (!file) { - let text = readFile(fileName); - if (text == null) return; - - file = files[fileName] = { version: 0, text } - } - - return compiler.ScriptSnapshot.fromString(file.text); - }, - /** - * getDirectories is also required for full import and type reference completions. - * Without it defined, certain completions will not be provided - */ - getDirectories: typescript.sys ? (typescript.sys).getDirectories : undefined, - - /** - * For @types expansion, these two functions are needed. - */ - directoryExists: typescript.sys ? (typescript.sys).directoryExists : undefined, - getCurrentDirectory: () => process.cwd(), - - getCompilationSettings: () => compilerOptions, - getDefaultLibFileName: options => compiler.getDefaultLibFilePath(options), - getNewLine: () => newLine, - log: log, - resolveModuleNames: (moduleNames: string[], containingFile: string) => { - let resolvedModules: ResolvedModule[] = []; - - for (let moduleName of moduleNames) { - let resolvedFileName: string; - let resolutionResult: any; - - try { - resolvedFileName = resolver.resolveSync(path.normalize(path.dirname(containingFile)), moduleName) - - if (!resolvedFileName.match(scriptRegex)) resolvedFileName = null; - else resolutionResult = { resolvedFileName }; - } - catch (e) { resolvedFileName = null } - - let tsResolution = compiler.resolveModuleName(moduleName, containingFile, compilerOptions, moduleResolutionHost); - - if (tsResolution.resolvedModule) { - if (resolvedFileName) { - if (resolvedFileName == tsResolution.resolvedModule.resolvedFileName) { - resolutionResult.isExternalLibraryImport = tsResolution.resolvedModule.isExternalLibraryImport; - } - } - else resolutionResult = tsResolution.resolvedModule; - } - - resolvedModules.push(resolutionResult); - } - - let importedFiles = resolvedModules.filter(m => m != null).map(m => m.resolvedFileName); - instance.dependencyGraph[path.normalize(containingFile)] = importedFiles; - importedFiles.forEach(importedFileName => { - if (!instance.reverseDependencyGraph[importedFileName]) { - instance.reverseDependencyGraph[importedFileName] = {} - } - instance.reverseDependencyGraph[importedFileName][path.normalize(containingFile)] = true - }) - - - return resolvedModules; - } - }; - - var languageService = instance.languageService = compiler.createLanguageService(servicesHost, compiler.createDocumentRegistry()); - - var getCompilerOptionDiagnostics = true; - var checkAllFilesForErrors = true; - - loader._compiler.plugin("after-compile", (compilation, callback) => { - // Don't add errors for child compilations - if (compilation.compiler.isChild()) { - callback(); - return; - } - - // handle all other errors. The basic approach here to get accurate error - // reporting is to start with a "blank slate" each compilation and gather - // all errors from all files. Since webpack tracks errors in a module from - // compilation-to-compilation, and since not every module always runs through - // the loader, we need to detect and remove any pre-existing errors. - - function removeTSLoaderErrors(errors: WebpackError[]) { - let index = -1, length = errors.length; - while (++index < length) { - if (errors[index].loaderSource == 'ts-loader') { - errors.splice(index--, 1); - length--; - } - } - } - - /** - * Recursive collect all possible dependats of passed file - */ - function collectAllDependants(fileName: string, collected: any = {}): string[] { - let result = {} - result[fileName] = true - collected[fileName] = true - if (instance.reverseDependencyGraph[fileName]) { - Object.keys(instance.reverseDependencyGraph[fileName]).forEach(dependantFileName => { - if (!collected[dependantFileName]) { - collectAllDependants(dependantFileName, collected).forEach(fName => result[fName] = true) - } - }) - } - return Object.keys(result) - } - - removeTSLoaderErrors(compilation.errors); - - // handle compiler option errors after the first compile - if (getCompilerOptionDiagnostics) { - getCompilerOptionDiagnostics = false; - pushArray( - compilation.errors, - formatErrors(languageService.getCompilerOptionsDiagnostics(), instance, {file: configFilePath || 'tsconfig.json'})); - } - - // build map of all modules based on normalized filename - // this is used for quick-lookup when trying to find modules - // based on filepath - let modules = {}; - compilation.modules.forEach(module => { - if (module.resource) { - let modulePath = path.normalize(module.resource); - if (hasOwnProperty(modules, modulePath)) { - let existingModules = modules[modulePath]; - if (existingModules.indexOf(module) == -1) { - existingModules.push(module); - } - } - else { - modules[modulePath] = [module]; - } - } - }) - - // gather all errors from TypeScript and output them to webpack - let filesWithErrors: TSFiles = {} - // calculate array of files to check - let filesToCheckForErrors: TSFiles = null - if (checkAllFilesForErrors) { - // check all files on initial run - filesToCheckForErrors = instance.files - checkAllFilesForErrors = false - } else { - filesToCheckForErrors = {} - // check all modified files, and all dependants - Object.keys(instance.modifiedFiles).forEach(modifiedFileName => { - collectAllDependants(modifiedFileName).forEach(fName => { - filesToCheckForErrors[fName] = instance.files[fName] - }) - }) - } - // re-check files with errors from previous build - if (instance.filesWithErrors) { - Object.keys(instance.filesWithErrors).forEach(fileWithErrorName => - filesToCheckForErrors[fileWithErrorName] = instance.filesWithErrors[fileWithErrorName] - ) - } - - Object.keys(filesToCheckForErrors) - .filter(filePath => !!filePath.match(/(\.d)?\.ts(x?)$/)) - .forEach(filePath => { - let errors = languageService.getSyntacticDiagnostics(filePath).concat(languageService.getSemanticDiagnostics(filePath)); - if (errors.length > 0) { - if (null === filesWithErrors) { - filesWithErrors = {} - } - filesWithErrors[filePath] = instance.files[filePath] - } - - // if we have access to a webpack module, use that - if (hasOwnProperty(modules, filePath)) { - let associatedModules = modules[filePath]; - - associatedModules.forEach(module => { - // remove any existing errors - removeTSLoaderErrors(module.errors); - - // append errors - let formattedErrors = formatErrors(errors, instance, { module }); - pushArray(module.errors, formattedErrors); - pushArray(compilation.errors, formattedErrors); - }) - } - // otherwise it's a more generic error - else { - pushArray(compilation.errors, formatErrors(errors, instance, {file: filePath})); - } - }); - - - // gather all declaration files from TypeScript and output them to webpack - Object.keys(filesToCheckForErrors) - .filter(filePath => !!filePath.match(/\.ts(x?)$/)) - .forEach(filePath => { - let output = languageService.getEmitOutput(filePath); - let declarationFile = output.outputFiles.filter(filePath => !!filePath.name.match(/\.d.ts$/)).pop(); - if (declarationFile) { - let assetPath = path.relative(compilation.compiler.context, declarationFile.name); - compilation.assets[assetPath] = { - source: () => declarationFile.text, - size: () => declarationFile.text.length - }; - } - }); - - instance.filesWithErrors = filesWithErrors; - instance.modifiedFiles = null; - callback(); - }); - - // manually update changed files - loader._compiler.plugin("watch-run", (watching, cb) => { - var mtimes = watching.compiler.watchFileSystem.watcher.mtimes; - if (null === instance.modifiedFiles) { - instance.modifiedFiles = {} - } - - Object.keys(mtimes) - .filter(filePath => !!filePath.match(/\.tsx?$|\.jsx?$/)) - .forEach(filePath => { - filePath = path.normalize(filePath); - var file = instance.files[filePath]; - if (file) { - file.text = readFile(filePath) || ''; - file.version++; - instance.version++; - instance.modifiedFiles[filePath] = file; - } - }); - cb() - }) - - return { instance }; -} - -function loader(contents) { - this.cacheable && this.cacheable(); - var callback = this.async(); - var filePath = path.normalize(this.resourcePath); - - var queryOptions = loaderUtils.parseQuery(this.query); - var configFileOptions = this.options.ts || {}; - - var options = objectAssign({}, { - silent: false, - logLevel: 'INFO', - logInfoToStdOut: false, - instance: 'default', - compiler: 'typescript', - configFileName: 'tsconfig.json', - transpileOnly: false, - compilerOptions: {} - }, configFileOptions, queryOptions); - options.ignoreDiagnostics = arrify(options.ignoreDiagnostics).map(Number); - options.logLevel = options.logLevel.toUpperCase(); - - // differentiate the TypeScript instance based on the webpack instance - var webpackIndex = webpackInstances.indexOf(this._compiler); - if (webpackIndex == -1) { - webpackIndex = webpackInstances.push(this._compiler)-1; - } - options.instance = webpackIndex + '_' + options.instance; - - var { instance, error } = ensureTypeScriptInstance(options, this); - - if (error) { - callback(error) - return; - } - - // Update file contents - var file = instance.files[filePath] - if (!file) { - file = instance.files[filePath] = { version: 0 }; - } - - if (file.text !== contents) { - file.version++; - file.text = contents; - instance.version++; - } - - // push this file to modified files hash. - if (!instance.modifiedFiles) { - instance.modifiedFiles = {} - } - instance.modifiedFiles[filePath] = file; - - var outputText: string, sourceMapText: string, diagnostics: typescript.Diagnostic[] = []; - - if (options.transpileOnly) { - var fileName = path.basename(filePath); - var transpileResult = instance.compiler.transpileModule(contents, { - compilerOptions: instance.compilerOptions, - reportDiagnostics: true, - fileName - }); - - ({ outputText, sourceMapText, diagnostics } = transpileResult); - - pushArray(this._module.errors, formatErrors(diagnostics, instance, {module: this._module})); - } - else { - let langService = instance.languageService; - - // Emit Javascript - var output = langService.getEmitOutput(filePath); - - // Make this file dependent on *all* definition files in the program - this.clearDependencies(); - this.addDependency(filePath); - - let allDefinitionFiles = Object.keys(instance.files).filter(filePath => /\.d\.ts$/.test(filePath)); - allDefinitionFiles.forEach(this.addDependency.bind(this)); - - // Additionally make this file dependent on all imported files - let additionalDependencies = instance.dependencyGraph[filePath]; - if (additionalDependencies) { - additionalDependencies.forEach(this.addDependency.bind(this)) - } - - this._module.meta.tsLoaderDefinitionFileVersions = allDefinitionFiles - .concat(additionalDependencies) - .map(filePath => filePath+'@'+(instance.files[filePath] || {version: '?'}).version); - - var outputFile = output.outputFiles.filter(file => !!file.name.match(/\.js(x?)$/)).pop(); - if (outputFile) { outputText = outputFile.text } - - var sourceMapFile = output.outputFiles.filter(file => !!file.name.match(/\.js(x?)\.map$/)).pop(); - if (sourceMapFile) { sourceMapText = sourceMapFile.text } - } - - if (outputText == null) throw new Error(`Typescript emitted no output for ${filePath}`); - - if (sourceMapText) { - var sourceMap = JSON.parse(sourceMapText); - sourceMap.sources = [loaderUtils.getRemainingRequest(this)]; - sourceMap.file = filePath; - sourceMap.sourcesContent = [contents]; - outputText = outputText.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, ''); - } - - // Make sure webpack is aware that even though the emitted JavaScript may be the same as - // a previously cached version the TypeScript may be different and therefore should be - // treated as new - this._module.meta.tsLoaderFileVersion = file.version; - - callback(null, outputText, sourceMap) -} - -export = loader; diff --git a/package.json b/package.json index 881e361fa..f446c3328 100644 --- a/package.json +++ b/package.json @@ -4,11 +4,10 @@ "description": "TypeScript loader for webpack", "main": "index.js", "scripts": { - "build": "tsc", + "build": "tsc --version && tsc --project \"./src\"", "comparison-tests": "npm link ./test/comparison-tests/testLib && node test/comparison-tests/run-tests.js", "execution-tests": "node test/execution-tests/run-tests.js", - "test": "node test/run-tests.js", - "prepublish": "npm run build" + "test": "node test/run-tests.js" }, "repository": { "type": "git", @@ -61,6 +60,6 @@ "rimraf": "^2.4.2", "typescript": "^2.0.3", "typings": "^1.4.0", - "webpack": "^1.13.3" + "webpack": "^1.11.0" } } diff --git a/src/after-compile.ts b/src/after-compile.ts new file mode 100644 index 000000000..ad8198691 --- /dev/null +++ b/src/after-compile.ts @@ -0,0 +1,164 @@ +import typescript = require('typescript'); +import interfaces = require('./interfaces'); +import path = require('path'); +import utils = require('./utils'); + +function makeAfterCompile( + instance: interfaces.TSInstance, + compiler: typeof typescript, + servicesHost: typescript.LanguageServiceHost, + configFilePath: string +) { + const languageService = instance.languageService = compiler.createLanguageService(servicesHost, compiler.createDocumentRegistry()); + + let getCompilerOptionDiagnostics = true; + let checkAllFilesForErrors = true; + + return (compilation: interfaces.WebpackCompilation, callback: () => void) => { + // Don't add errors for child compilations + if (compilation.compiler.isChild()) { + callback(); + return; + } + + removeTSLoaderErrors(compilation.errors); + + // handle compiler option errors after the first compile + if (getCompilerOptionDiagnostics) { + getCompilerOptionDiagnostics = false; + utils.pushArray( + compilation.errors, + utils.formatErrors(languageService.getCompilerOptionsDiagnostics(), instance, { file: configFilePath || 'tsconfig.json' })); + } + + // build map of all modules based on normalized filename + // this is used for quick-lookup when trying to find modules + // based on filepath + let modules: { [modulePath: string]: interfaces.WebpackModule[] } = {}; + compilation.modules.forEach(module => { + if (module.resource) { + let modulePath = path.normalize(module.resource); + if (utils.hasOwnProperty(modules, modulePath)) { + let existingModules = modules[modulePath]; + if (existingModules.indexOf(module) === -1) { + existingModules.push(module); + } + } + else { + modules[modulePath] = [module]; + } + } + }); + + // gather all errors from TypeScript and output them to webpack + let filesWithErrors: interfaces.TSFiles = {}; + // calculate array of files to check + let filesToCheckForErrors: interfaces.TSFiles = null; + if (checkAllFilesForErrors) { + // check all files on initial run + filesToCheckForErrors = instance.files; + checkAllFilesForErrors = false; + } else { + filesToCheckForErrors = {}; + // check all modified files, and all dependants + Object.keys(instance.modifiedFiles).forEach(modifiedFileName => { + collectAllDependants(instance, modifiedFileName).forEach(fName => { + filesToCheckForErrors[fName] = instance.files[fName]; + }); + }); + } + // re-check files with errors from previous build + if (instance.filesWithErrors) { + Object.keys(instance.filesWithErrors).forEach(fileWithErrorName => + filesToCheckForErrors[fileWithErrorName] = instance.filesWithErrors[fileWithErrorName] + ); + } + + Object.keys(filesToCheckForErrors) + .filter(filePath => !!filePath.match(/(\.d)?\.ts(x?)$/)) + .forEach(filePath => { + let errors = languageService.getSyntacticDiagnostics(filePath).concat(languageService.getSemanticDiagnostics(filePath)); + if (errors.length > 0) { + if (null === filesWithErrors) { + filesWithErrors = {}; + } + filesWithErrors[filePath] = instance.files[filePath]; + } + + // if we have access to a webpack module, use that + if (utils.hasOwnProperty(modules, filePath)) { + let associatedModules = modules[filePath]; + + associatedModules.forEach(module => { + // remove any existing errors + removeTSLoaderErrors(module.errors); + + // append errors + let formattedErrors = utils.formatErrors(errors, instance, { module }); + utils.pushArray(module.errors, formattedErrors); + utils.pushArray(compilation.errors, formattedErrors); + }); + } + // otherwise it's a more generic error + else { + utils.pushArray(compilation.errors, utils.formatErrors(errors, instance, { file: filePath })); + } + }); + + + // gather all declaration files from TypeScript and output them to webpack + Object.keys(filesToCheckForErrors) + .filter(filePath => !!filePath.match(/\.ts(x?)$/)) + .forEach(filePath => { + let output = languageService.getEmitOutput(filePath); + let declarationFile = output.outputFiles.filter(filePath => !!filePath.name.match(/\.d.ts$/)).pop(); + if (declarationFile) { + let assetPath = path.relative(compilation.compiler.context, declarationFile.name); + compilation.assets[assetPath] = { + source: () => declarationFile.text, + size: () => declarationFile.text.length, + }; + } + }); + + instance.filesWithErrors = filesWithErrors; + instance.modifiedFiles = null; + callback(); + } +} + +/** + * handle all other errors. The basic approach here to get accurate error + * reporting is to start with a "blank slate" each compilation and gather + * all errors from all files. Since webpack tracks errors in a module from + * compilation-to-compilation, and since not every module always runs through + * the loader, we need to detect and remove any pre-existing errors. + */ +function removeTSLoaderErrors(errors: interfaces.WebpackError[]) { + let index = -1, length = errors.length; + while (++index < length) { + if (errors[index].loaderSource === 'ts-loader') { + errors.splice(index--, 1); + length--; + } + } +} + +/** + * Recursively collect all possible dependants of passed file + */ +function collectAllDependants(instance: interfaces.TSInstance, fileName: string, collected: any = {}): string[] { + let result = {}; + result[fileName] = true; + collected[fileName] = true; + if (instance.reverseDependencyGraph[fileName]) { + Object.keys(instance.reverseDependencyGraph[fileName]).forEach(dependantFileName => { + if (!collected[dependantFileName]) { + collectAllDependants(instance, dependantFileName, collected).forEach(fName => result[fName] = true); + } + }); + } + return Object.keys(result); +} + +export = makeAfterCompile; \ No newline at end of file diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 000000000..d18282cde --- /dev/null +++ b/src/config.ts @@ -0,0 +1,92 @@ +import objectAssign = require('object-assign'); +import typescript = require('typescript'); +import path = require('path'); + +import interfaces = require('./interfaces'); +import logger = require('./logger'); +import utils = require('./utils'); + +interface ConfigFile { + config?: any; + error?: typescript.Diagnostic; +} + +function getConfigFile( + compiler: typeof typescript, + loader: any, + loaderOptions: interfaces.LoaderOptions, + compilerCompatible: boolean, + log: logger.Logger, + compilerDetailsLogMessage: string, + instance: interfaces.TSInstance +) { + const configFilePath = findConfigFile(compiler, path.dirname(loader.resourcePath), loaderOptions.configFileName); + let configFileError: interfaces.WebpackError; + let configFile: ConfigFile; + + if (configFilePath) { + if (compilerCompatible) { + log.logInfo(`${compilerDetailsLogMessage} and ${configFilePath}`.green); + } else { + log.logInfo(`ts-loader: Using config file at ${configFilePath}`.green); + } + + // HACK: relies on the fact that passing an extra argument won't break + // the old API that has a single parameter + configFile = (compiler).readConfigFile( + configFilePath, + compiler.sys.readFile + ); + + if (configFile.error) { + configFileError = utils.formatErrors([configFile.error], instance, { file: configFilePath })[0]; + } + } else { + if (compilerCompatible) { log.logInfo(compilerDetailsLogMessage.green); } + + configFile = { + config: { + compilerOptions: {}, + files: [], + }, + }; + } + + if (!configFileError) { + configFile.config.compilerOptions = objectAssign({}, + configFile.config.compilerOptions, + loaderOptions.compilerOptions); + + // do any necessary config massaging + if (loaderOptions.transpileOnly) { + configFile.config.compilerOptions.isolatedModules = true; + } + } + + return { + configFilePath, + configFile, + configFileError + }; +} + +/** + * The tsconfig.json is found using the same method as `tsc`, starting in the current directory + * and continuing up the parent directory chain. + */ +function findConfigFile(compiler: typeof typescript, searchPath: string, configFileName: string): string { + while (true) { + const fileName = path.join(searchPath, configFileName); + if (compiler.sys.fileExists(fileName)) { + return fileName; + } + const parentPath = path.dirname(searchPath); + if (parentPath === searchPath) { + break; + } + searchPath = parentPath; + } + return undefined; +} + +export = getConfigFile; \ No newline at end of file diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 000000000..4b81de541 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,5 @@ +import os = require('os'); + +export const EOL = os.EOL; +export const CarriageReturnLineFeed = '\r\n'; +export const LineFeed = '\n'; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 000000000..dd8e83346 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,297 @@ +import typescript = require('typescript'); +import path = require('path'); +import fs = require('fs'); +import loaderUtils = require('loader-utils'); +import objectAssign = require('object-assign'); +import arrify = require('arrify'); +const semver = require('semver'); +require('colors'); + +import afterCompile = require('./after-compile'); +import getConfigFile = require('./config'); +import interfaces = require('./interfaces'); +import constants = require('./constants'); +import utils = require('./utils'); +import logger = require('./logger'); +import makeServicesHost = require('./servicesHost'); +import watchRun = require('./watch-run'); + +let instances = {}; +let webpackInstances: any = []; +let scriptRegex = /\.tsx?$/i; + +/** + * The loader is executed once for each file seen by webpack. However, we need to keep + * a persistent instance of TypeScript that contains all of the files in the program + * along with definition files and options. This function either creates an instance + * or returns the existing one. Multiple instances are possible by using the + * `instance` property. + */ +function ensureTypeScriptInstance(loaderOptions: interfaces.LoaderOptions, loader: any): { instance?: interfaces.TSInstance, error?: interfaces.WebpackError } { + if (utils.hasOwnProperty(instances, loaderOptions.instance)) { + return { instance: instances[loaderOptions.instance] }; + } + + let compiler: typeof typescript; + try { + compiler = require(loaderOptions.compiler); + } catch (e) { + let message = loaderOptions.compiler === 'typescript' + ? 'Could not load TypeScript. Try installing with `npm install typescript`. If TypeScript is installed globally, try using `npm link typescript`.' + : `Could not load TypeScript compiler with NPM package name \`${loaderOptions.compiler}\`. Are you sure it is correctly installed?`; + return { error: { + message: message.red, + rawMessage: message, + loaderSource: 'ts-loader', + } }; + } + + const log = logger.makeLogger(loaderOptions); + const compilerDetailsLogMessage = `ts-loader: Using ${loaderOptions.compiler}@${compiler.version}`; + let compilerCompatible = false; + if (loaderOptions.compiler === 'typescript') { + if (compiler.version && semver.gte(compiler.version, '1.6.2-0')) { + // don't log yet in this case, if a tsconfig.json exists we want to combine the message + compilerCompatible = true; + } else { + log.logError(`${compilerDetailsLogMessage}. This version is incompatible with ts-loader. Please upgrade to the latest version of TypeScript.`.red); + } + } else { + log.logWarning(`${compilerDetailsLogMessage}. This version may or may not be compatible with ts-loader.`.yellow); + } + + const files: interfaces.TSFiles = {}; + const instance: interfaces.TSInstance = instances[loaderOptions.instance] = { + compiler, + compilerOptions: null, + loaderOptions, + files, + languageService: null, + version: 0, + dependencyGraph: {}, + reverseDependencyGraph: {}, + modifiedFiles: null, + }; + + const compilerOptions: typescript.CompilerOptions = { + skipDefaultLibCheck: true, + suppressOutputPathCheck: true, // This is why: https://github.com/Microsoft/TypeScript/issues/7363 + }; + + // Load any available tsconfig.json file + let filesToLoad: string[] = []; + + const { + configFilePath, + configFile, + configFileError + } = getConfigFile(compiler, loader, loaderOptions, compilerCompatible, log, compilerDetailsLogMessage, instance); + + if (configFileError) { + return { error: configFileError }; + } + + // if allowJs is set then we should accept js(x) files + if (configFile.config.compilerOptions.allowJs) { + scriptRegex = /\.tsx?$|\.jsx?$/i; + } + + let configParseResult: typescript.ParsedCommandLine; + if (typeof ( compiler).parseJsonConfigFileContent === 'function') { + // parseConfigFile was renamed between 1.6.2 and 1.7 + configParseResult = ( compiler).parseJsonConfigFileContent( + configFile.config, + compiler.sys, + path.dirname(configFilePath || '') + ); + } else { + configParseResult = ( compiler).parseConfigFile( + configFile.config, + compiler.sys, + path.dirname(configFilePath || '') + ); + } + + if (configParseResult.errors.length) { + utils.pushArray( + loader._module.errors, + utils.formatErrors(configParseResult.errors, instance, { file: configFilePath })); + + return { error: { + file: configFilePath, + message: 'error while parsing tsconfig.json'.red, + rawMessage: 'error while parsing tsconfig.json', + loaderSource: 'ts-loader', + }}; + } + + instance.compilerOptions = objectAssign(compilerOptions, configParseResult.options); + filesToLoad = configParseResult.fileNames; + + // if `module` is not specified and not using ES6 target, default to CJS module output + if ((!compilerOptions.module) && compilerOptions.target !== 2 /* ES6 */) { + compilerOptions.module = 1; /* CommonJS */ + } else if (compilerCompatible && semver.lt(compiler.version, '1.7.3-0') && compilerOptions.target === 2 /* ES6 */) { + // special handling for TS 1.6 and target: es6 + compilerOptions.module = 0 /* None */; + } + + if (loaderOptions.transpileOnly) { + // 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(); + + utils.pushArray( + loader._module.errors, + utils.formatErrors(diagnostics, instance, {file: configFilePath || 'tsconfig.json'})); + + return { instance: instances[loaderOptions.instance] = { compiler, compilerOptions, loaderOptions, files, dependencyGraph: {}, reverseDependencyGraph: {} }}; + } + + // Load initial files (core lib files, any files specified in tsconfig.json) + let filePath: string; + try { + filesToLoad.forEach(fp => { + filePath = path.normalize(fp); + files[filePath] = { + text: fs.readFileSync(filePath, 'utf-8'), + version: 0, + }; + }); + } catch (exc) { + let filePathError = `A file specified in tsconfig.json could not be found: ${ filePath }`; + return { error: { + message: filePathError.red, + rawMessage: filePathError, + loaderSource: 'ts-loader', + }}; + } + + const servicesHost = makeServicesHost(files, scriptRegex, log, loader, compilerOptions, instance, compiler, configFilePath); + + loader._compiler.plugin("after-compile", afterCompile(instance, compiler, servicesHost, configFilePath)); + loader._compiler.plugin("watch-run", watchRun(instance)); + + return { instance }; +} + +function loader(contents: string) { + this.cacheable && this.cacheable(); + const callback = this.async(); + const filePath = path.normalize(this.resourcePath); + + const queryOptions = loaderUtils.parseQuery(this.query); + const configFileOptions = this.options.ts || {}; + + const options = objectAssign({}, { + silent: false, + logLevel: 'INFO', + logInfoToStdOut: false, + instance: 'default', + compiler: 'typescript', + configFileName: 'tsconfig.json', + transpileOnly: false, + compilerOptions: {}, + }, configFileOptions, queryOptions); + options.ignoreDiagnostics = arrify(options.ignoreDiagnostics).map(Number); + options.logLevel = options.logLevel.toUpperCase(); + + // differentiate the TypeScript instance based on the webpack instance + let webpackIndex = webpackInstances.indexOf(this._compiler); + if (webpackIndex === -1) { + webpackIndex = webpackInstances.push(this._compiler) - 1; + } + options.instance = webpackIndex + '_' + options.instance; + + const { instance, error } = ensureTypeScriptInstance(options, this); + + if (error) { + callback(error); + return; + } + + // Update file contents + let file = instance.files[filePath]; + if (!file) { + file = instance.files[filePath] = { version: 0 }; + } + + if (file.text !== contents) { + file.version++; + file.text = contents; + instance.version++; + } + + // push this file to modified files hash. + if (!instance.modifiedFiles) { + instance.modifiedFiles = {}; + } + instance.modifiedFiles[filePath] = file; + + let outputText: string, sourceMapText: string, diagnostics: typescript.Diagnostic[] = []; + + if (options.transpileOnly) { + const fileName = path.basename(filePath); + const transpileResult = instance.compiler.transpileModule(contents, { + compilerOptions: instance.compilerOptions, + reportDiagnostics: true, + fileName, + }); + + ({ outputText, sourceMapText, diagnostics } = transpileResult); + + utils.pushArray(this._module.errors, utils.formatErrors(diagnostics, instance, {module: this._module})); + } + else { + let langService = instance.languageService; + + // Emit Javascript + const output = langService.getEmitOutput(filePath); + + // Make this file dependent on *all* definition files in the program + this.clearDependencies(); + this.addDependency(filePath); + + let allDefinitionFiles = Object.keys(instance.files).filter(filePath => /\.d\.ts$/.test(filePath)); + allDefinitionFiles.forEach(this.addDependency.bind(this)); + + // Additionally make this file dependent on all imported files + let additionalDependencies = instance.dependencyGraph[filePath]; + if (additionalDependencies) { + additionalDependencies.forEach(this.addDependency.bind(this)); + } + + this._module.meta.tsLoaderDefinitionFileVersions = allDefinitionFiles + .concat(additionalDependencies) + .map(filePath => filePath + '@' + (instance.files[filePath] || {version: '?'}).version); + + const outputFile = output.outputFiles.filter(file => !!file.name.match(/\.js(x?)$/)).pop(); + if (outputFile) { outputText = outputFile.text; } + + const sourceMapFile = output.outputFiles.filter(file => !!file.name.match(/\.js(x?)\.map$/)).pop(); + if (sourceMapFile) { sourceMapText = sourceMapFile.text; } + } + + if (outputText === null || outputText === undefined) { + throw new Error(`Typescript emitted no output for ${filePath}`); + } + + let sourceMap: { sources: any[], file: string; sourcesContent: string[] }; + if (sourceMapText) { + sourceMap = JSON.parse(sourceMapText); + sourceMap.sources = [loaderUtils.getRemainingRequest(this)]; + sourceMap.file = filePath; + sourceMap.sourcesContent = [contents]; + outputText = outputText.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, ''); + } + + // Make sure webpack is aware that even though the emitted JavaScript may be the same as + // a previously cached version the TypeScript may be different and therefore should be + // treated as new + this._module.meta.tsLoaderFileVersion = file.version; + + callback(null, outputText, sourceMap); +} + +export = loader; diff --git a/src/interfaces.ts b/src/interfaces.ts new file mode 100644 index 000000000..c777bc4ca --- /dev/null +++ b/src/interfaces.ts @@ -0,0 +1,156 @@ +import typescript = require('typescript'); + +export interface WebpackError { + module?: any; + file?: string; + message: string; + rawMessage: string; + location?: { line: number, character: number }; + loaderSource: string; +} + +/** + * webpack/lib/Compilation.js + */ +export interface WebpackCompilation { + compiler: WebpackCompiler; + errors: WebpackError[]; + modules: WebpackModule[]; + assets: { + [index: string]: { + size: () => number; + source: () => string; + } + }; +} + +/** + * webpack/lib/Compiler.js + */ +export interface WebpackCompiler { + isChild(): boolean; + context: string; // a guess + watchFileSystem: WebpackNodeWatchFileSystem; +} + +export interface WebpackModule { + resource: string; + errors: WebpackError[]; +} + +export interface WebpackNodeWatchFileSystem { + watcher: { + mtimes: number; // a guess + } +} + +export interface WebpackWatching { + compiler: WebpackCompiler; // a guess +} + +export interface Resolve { + /** Replace modules by other modules or paths. */ + alias?: { [key: string]: string; }; + /** + * The directory (absolute path) that contains your modules. + * May also be an array of directories. + * This setting should be used to add individual directories to the search path. */ + root?: string | string[]; + /** + * An array of directory names to be resolved to the current directory as well as its ancestors, and searched for modules. + * This functions similarly to how node finds “node_modules” directories. + * For example, if the value is ["mydir"], webpack will look in “./mydir”, “../mydir”, “../../mydir”, etc. + */ + modulesDirectories?: string[]; + /** + * A directory (or array of directories absolute paths), + * in which webpack should look for modules that weren’t found in resolve.root or resolve.modulesDirectories. + */ + fallback?: string | string[]; + /** + * An array of extensions that should be used to resolve modules. + * For example, in order to discover CoffeeScript files, your array should contain the string ".coffee". + */ + extensions?: string[]; + /** Check these fields in the package.json for suitable files. */ + packageMains?: (string | string[])[]; + /** Check this field in the package.json for an object. Key-value-pairs are threaded as aliasing according to this spec */ + packageAlias?: (string | string[])[]; + /** + * Enable aggressive but unsafe caching for the resolving of a part of your files. + * Changes to cached paths may cause failure (in rare cases). An array of RegExps, only a RegExp or true (all files) is expected. + * If the resolved path matches, it’ll be cached. + */ + unsafeCache?: RegExp | RegExp[] | boolean; +} + +export interface TSInstance { + compiler: typeof typescript; + compilerOptions: typescript.CompilerOptions; + loaderOptions: LoaderOptions; + files: TSFiles; + languageService?: typescript.LanguageService; + version?: number; + dependencyGraph: DependencyGraph; + reverseDependencyGraph: ReverseDependencyGraph; + modifiedFiles?: TSFiles; + filesWithErrors?: TSFiles; +} + +export interface TSInstances { + [name: string]: TSInstance; +} + +interface DependencyGraph { + [index: string]: string[] +} + +interface ReverseDependencyGraph { + [index: string]: { + [index: string]: boolean + } +} + +export interface LoaderOptions { + silent: boolean; + logLevel: string; + logInfoToStdOut: boolean; + instance: string; + compiler: string; + configFileName: string; + transpileOnly: boolean; + ignoreDiagnostics: number[]; + compilerOptions: typescript.CompilerOptions; +} + +export interface TSFile { + text: string; + version: number; +} + +export interface TSFiles { + [fileName: string]: TSFile; +} + +export interface ResolvedModule { + resolvedFileName: string; + resolvedModule?: ResolvedModule; + isExternalLibraryImport?: boolean; +} + +export interface TSCompatibleCompiler { + // typescript@next 1.7+ + readConfigFile(fileName: string, readFile: (path: string) => string): { + config?: any; + error?: typescript.Diagnostic; + }; + // typescript@latest 1.6.2 + readConfigFile(fileName: string): { + config?: any; + error?: typescript.Diagnostic; + }; + // typescript@next 1.8+ + parseJsonConfigFileContent?(json: any, host: typescript.ParseConfigHost, basePath: string): typescript.ParsedCommandLine; + // typescript@latest 1.6.2 + parseConfigFile?(json: any, host: typescript.ParseConfigHost, basePath: string): typescript.ParsedCommandLine; +} diff --git a/src/logger.ts b/src/logger.ts new file mode 100644 index 000000000..b5a1d1f31 --- /dev/null +++ b/src/logger.ts @@ -0,0 +1,67 @@ +import interfaces = require('./interfaces'); +var Console = require('console').Console; + +const stderrConsole = new Console(process.stderr); +const stdoutConsole = new Console(process.stdout); + +enum LogLevel { + INFO = 1, + WARN = 2, + ERROR = 3 +} + +interface InternalLoggerFunc { + (whereToLog: any, messages: string[]): void +} + +const doNothingLogger = (...messages: string[]) => {}; + +function makeLoggerFunc(loaderOptions: interfaces.LoaderOptions) { + return loaderOptions.silent + ? (whereToLog: any, messages: string[]) => {} + : (whereToLog: any, messages: string[]) => console.log.apply(whereToLog, messages); +} + +function makeExternalLogger(loaderOptions: interfaces.LoaderOptions, logger: InternalLoggerFunc) { + const output = loaderOptions.logInfoToStdOut ? stdoutConsole : stderrConsole; + return (...messages: string[]) => logger(output, messages); +} + +function makeLogInfo(loaderOptions: interfaces.LoaderOptions, logger: InternalLoggerFunc) { + return LogLevel[loaderOptions.logLevel] <= LogLevel.INFO + ? (...messages: string[]) => logger(loaderOptions.logInfoToStdOut ? stdoutConsole : stderrConsole, messages) + : doNothingLogger +} + +function makeLogError(loaderOptions: interfaces.LoaderOptions, logger: InternalLoggerFunc) { + return LogLevel[loaderOptions.logLevel] <= LogLevel.ERROR + ? (...messages: string[]) => logger(stderrConsole, messages) + : doNothingLogger +} + +function makeLogWarning(loaderOptions: interfaces.LoaderOptions, logger: InternalLoggerFunc) { + return LogLevel[loaderOptions.logLevel] <= LogLevel.WARN + ? (...messages: string[]) => logger(stderrConsole, messages) + : doNothingLogger +} + +interface LoggerFunc { + (...messages: string[]): void +} + +export interface Logger { + log: LoggerFunc; + logInfo: LoggerFunc; + logWarning: LoggerFunc; + logError: LoggerFunc; +} + +export function makeLogger(loaderOptions: interfaces.LoaderOptions): Logger { + const logger = makeLoggerFunc(loaderOptions); + return { + log: makeExternalLogger(loaderOptions, logger), + logInfo: makeLogInfo(loaderOptions, logger), + logWarning: makeLogWarning(loaderOptions, logger), + logError: makeLogError(loaderOptions, logger) + } +} diff --git a/resolver.ts b/src/resolver.ts similarity index 93% rename from resolver.ts rename to src/resolver.ts index 1f66ec317..656055296 100644 --- a/resolver.ts +++ b/src/resolver.ts @@ -1,8 +1,7 @@ // This file serves as a hacky workaround for the lack of "resolveSync" support in webpack. // We make our own resolver using a sync file system but using the same plugins & options // that webpack does. - -/// +import interfaces = require('./interfaces'); var Resolver = require("enhanced-resolve/lib/Resolver"); var SyncNodeJsInputFileSystem = require("enhanced-resolve/lib/SyncNodeJsInputFileSystem"); @@ -19,7 +18,7 @@ var DirectoryDescriptionFileFieldAliasPlugin = require("enhanced-resolve/lib/Dir var FileAppendPlugin = require("enhanced-resolve/lib/FileAppendPlugin"); var ResultSymlinkPlugin = require("enhanced-resolve/lib/ResultSymlinkPlugin"); -function makeRootPlugin(name, root) { +function makeRootPlugin(name: string, root: string | string[]) { if(typeof root === "string") return new ModulesInRootPlugin(name, root); else if(Array.isArray(root)) { @@ -32,7 +31,7 @@ function makeRootPlugin(name, root) { return function() {}; } -function makeResolver(options) { +function makeResolver(options: { resolve: interfaces.Resolve }) { let fileSystem = new CachedInputFileSystem(new SyncNodeJsInputFileSystem(), 60000); let resolver = new Resolver(fileSystem); diff --git a/src/servicesHost.ts b/src/servicesHost.ts new file mode 100644 index 000000000..d91854570 --- /dev/null +++ b/src/servicesHost.ts @@ -0,0 +1,118 @@ +import typescript = require('typescript'); +import constants = require('./constants'); +import interfaces = require('./interfaces'); +import logger = require('./logger'); +import path = require('path'); +import makeResolver = require('./resolver'); +import utils = require('./utils'); + +/** + * Create the TypeScript language service + */ +function makeServicesHost( + files: interfaces.TSFiles, + scriptRegex: RegExp, + log: logger.Logger, + loader: any, //TODO: not any + compilerOptions: typescript.CompilerOptions, + instance: interfaces.TSInstance, + compiler: typeof typescript, + configFilePath: string +) { + const newLine = + compilerOptions.newLine === 0 /* CarriageReturnLineFeed */ ? constants.CarriageReturnLineFeed : + compilerOptions.newLine === 1 /* LineFeed */ ? constants.LineFeed : + constants.EOL; + + // make a (sync) resolver that follows webpack's rules + const resolver = makeResolver(loader.options); + + const moduleResolutionHost = { + fileExists: (fileName: string) => utils.readFile(fileName) !== undefined, + readFile: (fileName: string) => utils.readFile(fileName), + }; + + return { + getProjectVersion: () => `${instance.version}`, + getScriptFileNames: () => Object.keys(files).filter(filePath => scriptRegex.test(filePath)), + getScriptVersion: (fileName: string) => { + fileName = path.normalize(fileName); + return files[fileName] && files[fileName].version.toString(); + }, + getScriptSnapshot: (fileName: string) => { + // This is called any time TypeScript needs a file's text + // We either load from memory or from disk + fileName = path.normalize(fileName); + let file = files[fileName]; + + if (!file) { + let text = utils.readFile(fileName); + if (!text) { return; } + + file = files[fileName] = { version: 0, text }; + } + + return compiler.ScriptSnapshot.fromString(file.text); + }, + /** + * getDirectories is also required for full import and type reference completions. + * Without it defined, certain completions will not be provided + */ + getDirectories: typescript.sys ? (typescript.sys).getDirectories : undefined, + + /** + * For @types expansion, these two functions are needed. + */ + directoryExists: typescript.sys ? (typescript.sys).directoryExists : undefined, + getCurrentDirectory: () => process.cwd(), + + getCompilationSettings: () => compilerOptions, + getDefaultLibFileName: (options: typescript.CompilerOptions) => compiler.getDefaultLibFilePath(options), + getNewLine: () => newLine, + log: log.log, + resolveModuleNames: (moduleNames: string[], containingFile: string) => { + let resolvedModules: interfaces.ResolvedModule[] = []; + + for (let moduleName of moduleNames) { + let resolvedFileName: string; + let resolutionResult: any; + + try { + resolvedFileName = resolver.resolveSync(path.normalize(path.dirname(containingFile)), moduleName); + + if (!resolvedFileName.match(scriptRegex)) resolvedFileName = null; + else resolutionResult = { resolvedFileName }; + } + catch (e) { resolvedFileName = null; } + + let tsResolution = compiler.resolveModuleName(moduleName, containingFile, compilerOptions, moduleResolutionHost); + + if (tsResolution.resolvedModule) { + if (resolvedFileName) { + if (resolvedFileName === tsResolution.resolvedModule.resolvedFileName) { + resolutionResult.isExternalLibraryImport = tsResolution.resolvedModule.isExternalLibraryImport; + } + } + else resolutionResult = tsResolution.resolvedModule; + } + + resolvedModules.push(resolutionResult); + } + + const importedFiles = resolvedModules + .filter(m => m !== null && m !== undefined) + .map(m => m.resolvedFileName); + instance.dependencyGraph[path.normalize(containingFile)] = importedFiles; + importedFiles.forEach(importedFileName => { + if (!instance.reverseDependencyGraph[importedFileName]) { + instance.reverseDependencyGraph[importedFileName] = {}; + } + instance.reverseDependencyGraph[importedFileName][path.normalize(containingFile)] = true; + }); + + return resolvedModules; + }, + }; +} + +export = makeServicesHost; \ No newline at end of file diff --git a/src/tsconfig.json b/src/tsconfig.json new file mode 100644 index 000000000..91402039e --- /dev/null +++ b/src/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "noImplicitAny": true, + "suppressImplicitAnyIndexErrors": true, + "module": "commonjs", + "moduleResolution": "node", + "outDir": "../dist" + } +} \ No newline at end of file diff --git a/src/tslint.json b/src/tslint.json new file mode 100644 index 000000000..7e468b8a9 --- /dev/null +++ b/src/tslint.json @@ -0,0 +1,27 @@ +{ + "extends": "tslint:latest", + "rules": { + "max-line-length": [false, 140], + "object-literal-sort-keys": false, + "member-ordering": [true, + "public-before-private", + "static-before-instance", + "variables-before-functions" + ], + "no-unused-variable": false, + "no-var-requires": false, + "quotemark": [ + "single" + ], + "no-trailing-comma": true, + "triple-equals": [ + true + ], + "variable-name": [true, + "ban-keywords", + "check-format", + "allow-leading-underscore", + "allow-pascal-case" + ] + } +} diff --git a/typings/arrify/arrify.d.ts b/src/typings/arrify/arrify.d.ts similarity index 100% rename from typings/arrify/arrify.d.ts rename to src/typings/arrify/arrify.d.ts diff --git a/typings/colors/colors.d.ts b/src/typings/colors/colors.d.ts similarity index 100% rename from typings/colors/colors.d.ts rename to src/typings/colors/colors.d.ts diff --git a/typings/loaderUtils/loaderUtils.d.ts b/src/typings/loaderUtils/loaderUtils.d.ts similarity index 100% rename from typings/loaderUtils/loaderUtils.d.ts rename to src/typings/loaderUtils/loaderUtils.d.ts diff --git a/typings/node/node.d.ts b/src/typings/node/node.d.ts similarity index 100% rename from typings/node/node.d.ts rename to src/typings/node/node.d.ts diff --git a/typings/objectAssign/objectAssign.d.ts b/src/typings/objectAssign/objectAssign.d.ts similarity index 100% rename from typings/objectAssign/objectAssign.d.ts rename to src/typings/objectAssign/objectAssign.d.ts diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 000000000..a456c0019 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,61 @@ +import typescript = require('typescript'); +import path = require('path'); +import fs = require('fs'); +import objectAssign = require('object-assign'); +import constants = require('./constants'); +import interfaces = require('./interfaces'); + +export function pushArray(arr: T[], toPush: any) { + Array.prototype.splice.apply(arr, [0, 0].concat(toPush)); +} + +export function hasOwnProperty(obj: T, property: string) { + return Object.prototype.hasOwnProperty.call(obj, property); +} + +/** + * Take TypeScript errors, parse them and format to webpack errors + * Optionally adds a file name + */ +export function formatErrors( + diagnostics: typescript.Diagnostic[], + instance: interfaces.TSInstance, + merge?: any): interfaces.WebpackError[] { + + return diagnostics + .filter(diagnostic => instance.loaderOptions.ignoreDiagnostics.indexOf(diagnostic.code) === -1) + .map(diagnostic => { + const errorCategory = instance.compiler.DiagnosticCategory[diagnostic.category].toLowerCase(); + const errorCategoryAndCode = errorCategory + ' TS' + diagnostic.code + ': '; + + const messageText = errorCategoryAndCode + instance.compiler.flattenDiagnosticMessageText(diagnostic.messageText, constants.EOL); + if (diagnostic.file) { + const lineChar = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); + return { + message: `${'('.white}${(lineChar.line + 1).toString().cyan},${(lineChar.character + 1).toString().cyan}): ${messageText.red}`, + rawMessage: messageText, + location: {line: lineChar.line + 1, character: lineChar.character + 1}, + loaderSource: 'ts-loader' + }; + } + else { + return { + message: `${messageText.red}`, + rawMessage: messageText, + loaderSource: 'ts-loader' + }; + } + }) + .map(error => objectAssign(error, merge)); +} + +export function readFile(fileName: string) { + fileName = path.normalize(fileName); + try { + return fs.readFileSync(fileName, {encoding: 'utf8'}); + } catch (e) { + return; + } +} + + diff --git a/src/watch-run.ts b/src/watch-run.ts new file mode 100644 index 000000000..35d828c5c --- /dev/null +++ b/src/watch-run.ts @@ -0,0 +1,34 @@ +import path = require('path'); +import utils = require('./utils'); +import interfaces = require('./interfaces'); + +/** + * Make function which will manually update changed files + */ +function makeWatchRun( + instance: interfaces.TSInstance +) { + + return (watching: interfaces.WebpackWatching, cb: () => void) => { + const mtimes = watching.compiler.watchFileSystem.watcher.mtimes; + if (null === instance.modifiedFiles) { + instance.modifiedFiles = {}; + } + + Object.keys(mtimes) + .filter(filePath => !!filePath.match(/\.tsx?$|\.jsx?$/)) + .forEach(filePath => { + filePath = path.normalize(filePath); + const file = instance.files[filePath]; + if (file) { + file.text = utils.readFile(filePath) || ''; + file.version++; + instance.version++; + instance.modifiedFiles[filePath] = file; + } + }); + cb(); + }; +} + +export = makeWatchRun; \ No newline at end of file diff --git a/test/comparison-tests/dependencyErrors/tsconfig.json b/test/comparison-tests/dependencyErrors/tsconfig.json index fc16344c1..0efa0a638 100644 --- a/test/comparison-tests/dependencyErrors/tsconfig.json +++ b/test/comparison-tests/dependencyErrors/tsconfig.json @@ -1,5 +1,4 @@ { "compilerOptions": { - }, - "files": [] + } } \ No newline at end of file diff --git a/test/comparison-tests/errors/tsconfig.json b/test/comparison-tests/errors/tsconfig.json index fc16344c1..0efa0a638 100644 --- a/test/comparison-tests/errors/tsconfig.json +++ b/test/comparison-tests/errors/tsconfig.json @@ -1,5 +1,4 @@ { "compilerOptions": { - }, - "files": [] + } } \ No newline at end of file diff --git a/test/comparison-tests/ignoreDiagnostics/tsconfig.json b/test/comparison-tests/ignoreDiagnostics/tsconfig.json index fc16344c1..0efa0a638 100644 --- a/test/comparison-tests/ignoreDiagnostics/tsconfig.json +++ b/test/comparison-tests/ignoreDiagnostics/tsconfig.json @@ -1,5 +1,4 @@ { "compilerOptions": { - }, - "files": [] + } } \ No newline at end of file diff --git a/test/comparison-tests/importsWatch/tsconfig.json b/test/comparison-tests/importsWatch/tsconfig.json index fc16344c1..0efa0a638 100644 --- a/test/comparison-tests/importsWatch/tsconfig.json +++ b/test/comparison-tests/importsWatch/tsconfig.json @@ -1,5 +1,4 @@ { "compilerOptions": { - }, - "files": [] + } } \ No newline at end of file diff --git a/test/comparison-tests/node/app.ts b/test/comparison-tests/node/app.ts index a3fa3509c..08e21d426 100644 --- a/test/comparison-tests/node/app.ts +++ b/test/comparison-tests/node/app.ts @@ -1 +1 @@ -/// \ No newline at end of file +/// \ No newline at end of file diff --git a/test/comparison-tests/node/expectedOutput-2.0/bundle.js b/test/comparison-tests/node/expectedOutput-2.0/bundle.js index 19d798610..a3d17262f 100644 --- a/test/comparison-tests/node/expectedOutput-2.0/bundle.js +++ b/test/comparison-tests/node/expectedOutput-2.0/bundle.js @@ -44,7 +44,7 @@ /* 0 */ /***/ function(module, exports) { - /// + /// /***/ } diff --git a/test/comparison-tests/node/expectedOutput-2.0/bundle.transpiled.js b/test/comparison-tests/node/expectedOutput-2.0/bundle.transpiled.js index c38f39d99..df06de007 100644 --- a/test/comparison-tests/node/expectedOutput-2.0/bundle.transpiled.js +++ b/test/comparison-tests/node/expectedOutput-2.0/bundle.transpiled.js @@ -44,7 +44,7 @@ /* 0 */ /***/ function(module, exports) { - "use strict";/// + "use strict";/// /***/ } diff --git a/test/comparison-tests/node/expectedOutput-2.0/output.transpiled.txt b/test/comparison-tests/node/expectedOutput-2.0/output.transpiled.txt index 2db3f5850..f412fc3dc 100644 --- a/test/comparison-tests/node/expectedOutput-2.0/output.transpiled.txt +++ b/test/comparison-tests/node/expectedOutput-2.0/output.transpiled.txt @@ -1,4 +1,4 @@ Asset Size Chunks Chunk Names bundle.js 1.46 kB 0 [emitted] main -chunk {0} bundle.js (main) 68 bytes [rendered] - [0] ./.test/node/app.ts 68 bytes {0} [built] \ No newline at end of file +chunk {0} bundle.js (main) 72 bytes [rendered] + [0] ./.test/node/app.ts 72 bytes {0} [built] \ No newline at end of file diff --git a/test/comparison-tests/node/expectedOutput-2.0/output.txt b/test/comparison-tests/node/expectedOutput-2.0/output.txt index 3d487cb03..2ee0a5e06 100644 --- a/test/comparison-tests/node/expectedOutput-2.0/output.txt +++ b/test/comparison-tests/node/expectedOutput-2.0/output.txt @@ -1,4 +1,4 @@ Asset Size Chunks Chunk Names -bundle.js 1.44 kB 0 [emitted] main -chunk {0} bundle.js (main) 55 bytes [rendered] - [0] ./.test/node/app.ts 55 bytes {0} [built] \ No newline at end of file +bundle.js 1.45 kB 0 [emitted] main +chunk {0} bundle.js (main) 59 bytes [rendered] + [0] ./.test/node/app.ts 59 bytes {0} [built] \ No newline at end of file diff --git a/test/comparison-tests/node/tsconfig.json b/test/comparison-tests/node/tsconfig.json index fc16344c1..0efa0a638 100644 --- a/test/comparison-tests/node/tsconfig.json +++ b/test/comparison-tests/node/tsconfig.json @@ -1,5 +1,4 @@ { "compilerOptions": { - }, - "files": [] + } } \ No newline at end of file diff --git a/test/comparison-tests/nolib/tsconfig.json b/test/comparison-tests/nolib/tsconfig.json index ac89b38f8..db841f124 100644 --- a/test/comparison-tests/nolib/tsconfig.json +++ b/test/comparison-tests/nolib/tsconfig.json @@ -1,6 +1,5 @@ { "compilerOptions": { "noLib": true - }, - "files": [] + } } \ No newline at end of file diff --git a/test/comparison-tests/npmLink/tsconfig.json b/test/comparison-tests/npmLink/tsconfig.json index fa51fe925..f28c8052c 100644 --- a/test/comparison-tests/npmLink/tsconfig.json +++ b/test/comparison-tests/npmLink/tsconfig.json @@ -1,6 +1,5 @@ { "compilerOptions": { "module": "commonjs" - }, - "files": [] + } } \ No newline at end of file diff --git a/test/comparison-tests/replacement/tsconfig.json b/test/comparison-tests/replacement/tsconfig.json index fc16344c1..0efa0a638 100644 --- a/test/comparison-tests/replacement/tsconfig.json +++ b/test/comparison-tests/replacement/tsconfig.json @@ -1,5 +1,4 @@ { "compilerOptions": { - }, - "files": [] + } } \ No newline at end of file diff --git a/test/comparison-tests/simpleDependency/tsconfig.json b/test/comparison-tests/simpleDependency/tsconfig.json index fc16344c1..0efa0a638 100644 --- a/test/comparison-tests/simpleDependency/tsconfig.json +++ b/test/comparison-tests/simpleDependency/tsconfig.json @@ -1,5 +1,4 @@ { "compilerOptions": { - }, - "files": [] + } } \ No newline at end of file diff --git a/test/comparison-tests/sourceMaps/tsconfig.json b/test/comparison-tests/sourceMaps/tsconfig.json index 96c4e44fd..40470b346 100644 --- a/test/comparison-tests/sourceMaps/tsconfig.json +++ b/test/comparison-tests/sourceMaps/tsconfig.json @@ -1,6 +1,5 @@ { "compilerOptions": { "sourceMap": true - }, - "files": [] + } } \ No newline at end of file diff --git a/test/comparison-tests/typeSystemWatch/tsconfig.json b/test/comparison-tests/typeSystemWatch/tsconfig.json index fc16344c1..e2edf650a 100644 --- a/test/comparison-tests/typeSystemWatch/tsconfig.json +++ b/test/comparison-tests/typeSystemWatch/tsconfig.json @@ -1,5 +1,7 @@ { "compilerOptions": { }, - "files": [] + "files": [ + "app.ts" + ] } \ No newline at end of file diff --git a/test/execution-tests/simpleDependency/tsconfig.json b/test/execution-tests/simpleDependency/tsconfig.json index b71270932..fc16344c1 100644 --- a/test/execution-tests/simpleDependency/tsconfig.json +++ b/test/execution-tests/simpleDependency/tsconfig.json @@ -1,4 +1,5 @@ { "compilerOptions": { - } -} + }, + "files": [] +} \ No newline at end of file From 0b18e9befd79139876175bdfa7ab93e23278e0cd Mon Sep 17 00:00:00 2001 From: John Reilly Date: Wed, 26 Oct 2016 20:10:35 +0100 Subject: [PATCH 02/23] Update tsconfig.json --- test/execution-tests/simpleDependency/tsconfig.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/execution-tests/simpleDependency/tsconfig.json b/test/execution-tests/simpleDependency/tsconfig.json index fc16344c1..b71270932 100644 --- a/test/execution-tests/simpleDependency/tsconfig.json +++ b/test/execution-tests/simpleDependency/tsconfig.json @@ -1,5 +1,4 @@ { "compilerOptions": { - }, - "files": [] -} \ No newline at end of file + } +} From 2d0f5c2742a8c62764b36db76c03957467749198 Mon Sep 17 00:00:00 2001 From: John Reilly Date: Wed, 26 Oct 2016 21:38:55 +0100 Subject: [PATCH 03/23] split out compilersetup and makeerror --- .gitignore | 3 ++- src/compilerSetup.ts | 40 +++++++++++++++++++++++++++++++++++++ src/index.ts | 47 ++++++++++---------------------------------- src/utils.ts | 32 +++++++++++++++++++----------- 4 files changed, 73 insertions(+), 49 deletions(-) create mode 100644 src/compilerSetup.ts diff --git a/.gitignore b/.gitignore index 884610dd2..65b17e037 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,8 @@ *.js.map bundle.js npm-debug.log -.test/ +/.test/ +/.vscode/ /test/execution-tests/**/typings !/test/**/expectedOutput-*/** /node_modules diff --git a/src/compilerSetup.ts b/src/compilerSetup.ts new file mode 100644 index 000000000..82360ed3a --- /dev/null +++ b/src/compilerSetup.ts @@ -0,0 +1,40 @@ +import typescript = require('typescript'); +const semver = require('semver'); + +import interfaces = require('./interfaces'); +import logger = require('./logger'); + +export function getCompiler( + loaderOptions: interfaces.LoaderOptions, + log: logger.Logger +) { + let compiler: typeof typescript; + let errorMessage: string; + let compilerDetailsLogMessage: string; + let compilerCompatible = false; + + try { + compiler = require(loaderOptions.compiler); + } catch (e) { + errorMessage = loaderOptions.compiler === 'typescript' + ? 'Could not load TypeScript. Try installing with `npm install typescript`. If TypeScript is installed globally, try using `npm link typescript`.' + : `Could not load TypeScript compiler with NPM package name \`${loaderOptions.compiler}\`. Are you sure it is correctly installed?`; + } + + if (!errorMessage) { + compilerDetailsLogMessage = `ts-loader: Using ${loaderOptions.compiler}@${compiler.version}`; + compilerCompatible = false; + if (loaderOptions.compiler === 'typescript') { + if (compiler.version && semver.gte(compiler.version, '1.6.2-0')) { + // don't log yet in this case, if a tsconfig.json exists we want to combine the message + compilerCompatible = true; + } else { + log.logError(`${compilerDetailsLogMessage}. This version is incompatible with ts-loader. Please upgrade to the latest version of TypeScript.`.red); + } + } else { + log.logWarning(`${compilerDetailsLogMessage}. This version may or may not be compatible with ts-loader.`.yellow); + } + } + + return { compiler, compilerCompatible, compilerDetailsLogMessage, errorMessage }; +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index dd8e83346..02aa45816 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,7 @@ require('colors'); import afterCompile = require('./after-compile'); import getConfigFile = require('./config'); +import compilerSetup = require('./compilerSetup'); import interfaces = require('./interfaces'); import constants = require('./constants'); import utils = require('./utils'); @@ -31,33 +32,13 @@ function ensureTypeScriptInstance(loaderOptions: interfaces.LoaderOptions, loade if (utils.hasOwnProperty(instances, loaderOptions.instance)) { return { instance: instances[loaderOptions.instance] }; } + + const log = logger.makeLogger(loaderOptions); - let compiler: typeof typescript; - try { - compiler = require(loaderOptions.compiler); - } catch (e) { - let message = loaderOptions.compiler === 'typescript' - ? 'Could not load TypeScript. Try installing with `npm install typescript`. If TypeScript is installed globally, try using `npm link typescript`.' - : `Could not load TypeScript compiler with NPM package name \`${loaderOptions.compiler}\`. Are you sure it is correctly installed?`; - return { error: { - message: message.red, - rawMessage: message, - loaderSource: 'ts-loader', - } }; - } + const { compiler, compilerCompatible, compilerDetailsLogMessage, errorMessage } = compilerSetup.getCompiler(loaderOptions, log); - const log = logger.makeLogger(loaderOptions); - const compilerDetailsLogMessage = `ts-loader: Using ${loaderOptions.compiler}@${compiler.version}`; - let compilerCompatible = false; - if (loaderOptions.compiler === 'typescript') { - if (compiler.version && semver.gte(compiler.version, '1.6.2-0')) { - // don't log yet in this case, if a tsconfig.json exists we want to combine the message - compilerCompatible = true; - } else { - log.logError(`${compilerDetailsLogMessage}. This version is incompatible with ts-loader. Please upgrade to the latest version of TypeScript.`.red); - } - } else { - log.logWarning(`${compilerDetailsLogMessage}. This version may or may not be compatible with ts-loader.`.yellow); + if (errorMessage) { + return { error: utils.makeError({ rawMessage: errorMessage }) }; } const files: interfaces.TSFiles = {}; @@ -117,12 +98,7 @@ function ensureTypeScriptInstance(loaderOptions: interfaces.LoaderOptions, loade loader._module.errors, utils.formatErrors(configParseResult.errors, instance, { file: configFilePath })); - return { error: { - file: configFilePath, - message: 'error while parsing tsconfig.json'.red, - rawMessage: 'error while parsing tsconfig.json', - loaderSource: 'ts-loader', - }}; + return { error: utils.makeError({ rawMessage: 'error while parsing tsconfig.json', file: configFilePath }) }; } instance.compilerOptions = objectAssign(compilerOptions, configParseResult.options); @@ -160,12 +136,9 @@ function ensureTypeScriptInstance(loaderOptions: interfaces.LoaderOptions, loade }; }); } catch (exc) { - let filePathError = `A file specified in tsconfig.json could not be found: ${ filePath }`; - return { error: { - message: filePathError.red, - rawMessage: filePathError, - loaderSource: 'ts-loader', - }}; + return { error: utils.makeError({ + rawMessage: `A file specified in tsconfig.json could not be found: ${ filePath }` + }) }; } const servicesHost = makeServicesHost(files, scriptRegex, log, loader, compilerOptions, instance, compiler, configFilePath); diff --git a/src/utils.ts b/src/utils.ts index a456c0019..5be1a44f1 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -31,31 +31,41 @@ export function formatErrors( const messageText = errorCategoryAndCode + instance.compiler.flattenDiagnosticMessageText(diagnostic.messageText, constants.EOL); if (diagnostic.file) { const lineChar = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); - return { + return makeError({ message: `${'('.white}${(lineChar.line + 1).toString().cyan},${(lineChar.character + 1).toString().cyan}): ${messageText.red}`, rawMessage: messageText, - location: {line: lineChar.line + 1, character: lineChar.character + 1}, - loaderSource: 'ts-loader' - }; + location: { line: lineChar.line + 1, character: lineChar.character + 1 } + }); } else { - return { - message: `${messageText.red}`, - rawMessage: messageText, - loaderSource: 'ts-loader' - }; + return makeError({ rawMessage: messageText }); } }) - .map(error => objectAssign(error, merge)); + .map(error => objectAssign(error, merge)); } export function readFile(fileName: string) { fileName = path.normalize(fileName); try { - return fs.readFileSync(fileName, {encoding: 'utf8'}); + return fs.readFileSync(fileName, { encoding: 'utf8' }); } catch (e) { return; } } +interface MakeError { + rawMessage: string; + message?: string; + location?: { line: number, character: number }; + file?: string; +} + +export function makeError({ rawMessage, message, location, file }: MakeError): interfaces.WebpackError { + const error = { + rawMessage, + message: message || `${rawMessage.red}`, + loaderSource: 'ts-loader' + }; + return objectAssign(error, { location, file }); +} From f3c41964c04efbaa5840e3cf03ed781c7a1cbeac Mon Sep 17 00:00:00 2001 From: John Reilly Date: Thu, 27 Oct 2016 06:20:34 +0100 Subject: [PATCH 04/23] extract configparseresult --- src/config.ts | 26 ++++++++++++++++++++++++-- src/index.ts | 38 +++++++++++--------------------------- 2 files changed, 35 insertions(+), 29 deletions(-) diff --git a/src/config.ts b/src/config.ts index d18282cde..c57f59ee8 100644 --- a/src/config.ts +++ b/src/config.ts @@ -11,7 +11,7 @@ interface ConfigFile { error?: typescript.Diagnostic; } -function getConfigFile( +export function getConfigFile( compiler: typeof typescript, loader: any, loaderOptions: interfaces.LoaderOptions, @@ -89,4 +89,26 @@ function findConfigFile(compiler: typeof typescript, searchPath: string, configF return undefined; } -export = getConfigFile; \ No newline at end of file +export function getConfigParseResult( + compiler: typeof typescript, + configFile: ConfigFile, + configFilePath: string +) { + let configParseResult: typescript.ParsedCommandLine; + if (typeof ( compiler).parseJsonConfigFileContent === 'function') { + // parseConfigFile was renamed between 1.6.2 and 1.7 + configParseResult = ( compiler).parseJsonConfigFileContent( + configFile.config, + compiler.sys, + path.dirname(configFilePath || '') + ); + } else { + configParseResult = ( compiler).parseConfigFile( + configFile.config, + compiler.sys, + path.dirname(configFilePath || '') + ); + } + + return configParseResult; +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 02aa45816..1f71ff798 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,7 +8,7 @@ const semver = require('semver'); require('colors'); import afterCompile = require('./after-compile'); -import getConfigFile = require('./config'); +import config = require('./config'); import compilerSetup = require('./compilerSetup'); import interfaces = require('./interfaces'); import constants = require('./constants'); @@ -19,7 +19,6 @@ import watchRun = require('./watch-run'); let instances = {}; let webpackInstances: any = []; -let scriptRegex = /\.tsx?$/i; /** * The loader is executed once for each file seen by webpack. However, we need to keep @@ -59,39 +58,17 @@ function ensureTypeScriptInstance(loaderOptions: interfaces.LoaderOptions, loade suppressOutputPathCheck: true, // This is why: https://github.com/Microsoft/TypeScript/issues/7363 }; - // Load any available tsconfig.json file - let filesToLoad: string[] = []; - const { configFilePath, configFile, configFileError - } = getConfigFile(compiler, loader, loaderOptions, compilerCompatible, log, compilerDetailsLogMessage, instance); + } = config.getConfigFile(compiler, loader, loaderOptions, compilerCompatible, log, compilerDetailsLogMessage, instance); if (configFileError) { return { error: configFileError }; } - // if allowJs is set then we should accept js(x) files - if (configFile.config.compilerOptions.allowJs) { - scriptRegex = /\.tsx?$|\.jsx?$/i; - } - - let configParseResult: typescript.ParsedCommandLine; - if (typeof ( compiler).parseJsonConfigFileContent === 'function') { - // parseConfigFile was renamed between 1.6.2 and 1.7 - configParseResult = ( compiler).parseJsonConfigFileContent( - configFile.config, - compiler.sys, - path.dirname(configFilePath || '') - ); - } else { - configParseResult = ( compiler).parseConfigFile( - configFile.config, - compiler.sys, - path.dirname(configFilePath || '') - ); - } + const configParseResult = config.getConfigParseResult(compiler, configFile, configFilePath); if (configParseResult.errors.length) { utils.pushArray( @@ -102,7 +79,9 @@ function ensureTypeScriptInstance(loaderOptions: interfaces.LoaderOptions, loade } instance.compilerOptions = objectAssign(compilerOptions, configParseResult.options); - filesToLoad = configParseResult.fileNames; + + // Load any available tsconfig.json file + let filesToLoad = configParseResult.fileNames; // if `module` is not specified and not using ES6 target, default to CJS module output if ((!compilerOptions.module) && compilerOptions.target !== 2 /* ES6 */) { @@ -141,6 +120,11 @@ function ensureTypeScriptInstance(loaderOptions: interfaces.LoaderOptions, loade }) }; } + // if allowJs is set then we should accept js(x) files + const scriptRegex = configFile.config.compilerOptions.allowJs + ? /\.tsx?$|\.jsx?$/i + : /\.tsx?$/i; + const servicesHost = makeServicesHost(files, scriptRegex, log, loader, compilerOptions, instance, compiler, configFilePath); loader._compiler.plugin("after-compile", afterCompile(instance, compiler, servicesHost, configFilePath)); From 1e14d3953222edfa38e24d071e89522990d75f45 Mon Sep 17 00:00:00 2001 From: John Reilly Date: Thu, 27 Oct 2016 06:39:59 +0100 Subject: [PATCH 05/23] ts-lint tweaks --- src/after-compile.ts | 17 ++++++++--------- src/compilerSetup.ts | 4 ++-- src/config.ts | 4 ++-- src/index.ts | 25 +++++++++++++------------ src/tslint.json | 3 ++- src/utils.ts | 7 +++---- src/watch-run.ts | 2 +- 7 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/after-compile.ts b/src/after-compile.ts index ad8198691..e150551cf 100644 --- a/src/after-compile.ts +++ b/src/after-compile.ts @@ -43,8 +43,7 @@ function makeAfterCompile( if (existingModules.indexOf(module) === -1) { existingModules.push(module); } - } - else { + } else { modules[modulePath] = [module]; } } @@ -98,9 +97,8 @@ function makeAfterCompile( utils.pushArray(module.errors, formattedErrors); utils.pushArray(compilation.errors, formattedErrors); }); - } - // otherwise it's a more generic error - else { + } else { + // otherwise it's a more generic error utils.pushArray(compilation.errors, utils.formatErrors(errors, instance, { file: filePath })); } }); @@ -111,7 +109,7 @@ function makeAfterCompile( .filter(filePath => !!filePath.match(/\.ts(x?)$/)) .forEach(filePath => { let output = languageService.getEmitOutput(filePath); - let declarationFile = output.outputFiles.filter(filePath => !!filePath.name.match(/\.d.ts$/)).pop(); + let declarationFile = output.outputFiles.filter(fp => !!fp.name.match(/\.d.ts$/)).pop(); if (declarationFile) { let assetPath = path.relative(compilation.compiler.context, declarationFile.name); compilation.assets[assetPath] = { @@ -124,7 +122,7 @@ function makeAfterCompile( instance.filesWithErrors = filesWithErrors; instance.modifiedFiles = null; callback(); - } + }; } /** @@ -135,7 +133,8 @@ function makeAfterCompile( * the loader, we need to detect and remove any pre-existing errors. */ function removeTSLoaderErrors(errors: interfaces.WebpackError[]) { - let index = -1, length = errors.length; + let index = -1; + let length = errors.length; while (++index < length) { if (errors[index].loaderSource === 'ts-loader') { errors.splice(index--, 1); @@ -161,4 +160,4 @@ function collectAllDependants(instance: interfaces.TSInstance, fileName: string, return Object.keys(result); } -export = makeAfterCompile; \ No newline at end of file +export = makeAfterCompile; diff --git a/src/compilerSetup.ts b/src/compilerSetup.ts index 82360ed3a..85d77be54 100644 --- a/src/compilerSetup.ts +++ b/src/compilerSetup.ts @@ -35,6 +35,6 @@ export function getCompiler( log.logWarning(`${compilerDetailsLogMessage}. This version may or may not be compatible with ts-loader.`.yellow); } } - + return { compiler, compilerCompatible, compilerDetailsLogMessage, errorMessage }; -} \ No newline at end of file +} diff --git a/src/config.ts b/src/config.ts index c57f59ee8..0f1fcc5d6 100644 --- a/src/config.ts +++ b/src/config.ts @@ -33,7 +33,7 @@ export function getConfigFile( // HACK: relies on the fact that passing an extra argument won't break // the old API that has a single parameter - configFile = (compiler).readConfigFile( + configFile = ( compiler).readConfigFile( configFilePath, compiler.sys.readFile ); @@ -111,4 +111,4 @@ export function getConfigParseResult( } return configParseResult; -} \ No newline at end of file +} diff --git a/src/index.ts b/src/index.ts index 1f71ff798..7f46ba3d3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -31,7 +31,7 @@ function ensureTypeScriptInstance(loaderOptions: interfaces.LoaderOptions, loade if (utils.hasOwnProperty(instances, loaderOptions.instance)) { return { instance: instances[loaderOptions.instance] }; } - + const log = logger.makeLogger(loaderOptions); const { compiler, compilerCompatible, compilerDetailsLogMessage, errorMessage } = compilerSetup.getCompiler(loaderOptions, log); @@ -63,7 +63,7 @@ function ensureTypeScriptInstance(loaderOptions: interfaces.LoaderOptions, loade configFile, configFileError } = config.getConfigFile(compiler, loader, loaderOptions, compilerCompatible, log, compilerDetailsLogMessage, instance); - + if (configFileError) { return { error: configFileError }; } @@ -115,14 +115,14 @@ function ensureTypeScriptInstance(loaderOptions: interfaces.LoaderOptions, loade }; }); } catch (exc) { - return { error: utils.makeError({ - rawMessage: `A file specified in tsconfig.json could not be found: ${ filePath }` + return { error: utils.makeError({ + rawMessage: `A file specified in tsconfig.json could not be found: ${ filePath }` }) }; } // if allowJs is set then we should accept js(x) files const scriptRegex = configFile.config.compilerOptions.allowJs - ? /\.tsx?$|\.jsx?$/i + ? /\.tsx?$|\.jsx?$/i : /\.tsx?$/i; const servicesHost = makeServicesHost(files, scriptRegex, log, loader, compilerOptions, instance, compiler, configFilePath); @@ -186,7 +186,9 @@ function loader(contents: string) { } instance.modifiedFiles[filePath] = file; - let outputText: string, sourceMapText: string, diagnostics: typescript.Diagnostic[] = []; + let outputText: string; + let sourceMapText: string; + let diagnostics: typescript.Diagnostic[] = []; if (options.transpileOnly) { const fileName = path.basename(filePath); @@ -199,8 +201,7 @@ function loader(contents: string) { ({ outputText, sourceMapText, diagnostics } = transpileResult); utils.pushArray(this._module.errors, utils.formatErrors(diagnostics, instance, {module: this._module})); - } - else { + } else { let langService = instance.languageService; // Emit Javascript @@ -210,7 +211,7 @@ function loader(contents: string) { this.clearDependencies(); this.addDependency(filePath); - let allDefinitionFiles = Object.keys(instance.files).filter(filePath => /\.d\.ts$/.test(filePath)); + let allDefinitionFiles = Object.keys(instance.files).filter(fp => /\.d\.ts$/.test(fp)); allDefinitionFiles.forEach(this.addDependency.bind(this)); // Additionally make this file dependent on all imported files @@ -221,12 +222,12 @@ function loader(contents: string) { this._module.meta.tsLoaderDefinitionFileVersions = allDefinitionFiles .concat(additionalDependencies) - .map(filePath => filePath + '@' + (instance.files[filePath] || {version: '?'}).version); + .map(fp => fp + '@' + (instance.files[fp] || {version: '?'}).version); - const outputFile = output.outputFiles.filter(file => !!file.name.match(/\.js(x?)$/)).pop(); + const outputFile = output.outputFiles.filter(f => !!f.name.match(/\.js(x?)$/)).pop(); if (outputFile) { outputText = outputFile.text; } - const sourceMapFile = output.outputFiles.filter(file => !!file.name.match(/\.js(x?)\.map$/)).pop(); + const sourceMapFile = output.outputFiles.filter(f => !!f.name.match(/\.js(x?)\.map$/)).pop(); if (sourceMapFile) { sourceMapText = sourceMapFile.text; } } diff --git a/src/tslint.json b/src/tslint.json index 7e468b8a9..58f8f15fd 100644 --- a/src/tslint.json +++ b/src/tslint.json @@ -3,6 +3,7 @@ "rules": { "max-line-length": [false, 140], "object-literal-sort-keys": false, + "interface-name": [true, "never-prefix"], "member-ordering": [true, "public-before-private", "static-before-instance", @@ -13,7 +14,7 @@ "quotemark": [ "single" ], - "no-trailing-comma": true, + "trailing-comma": [false], "triple-equals": [ true ], diff --git a/src/utils.ts b/src/utils.ts index 5be1a44f1..98d2e6e0e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -36,12 +36,11 @@ export function formatErrors( rawMessage: messageText, location: { line: lineChar.line + 1, character: lineChar.character + 1 } }); - } - else { + } else { return makeError({ rawMessage: messageText }); } }) - .map(error => objectAssign(error, merge)); + .map(error => objectAssign(error, merge)); } export function readFile(fileName: string) { @@ -67,5 +66,5 @@ export function makeError({ rawMessage, message, location, file }: MakeError): i loaderSource: 'ts-loader' }; - return objectAssign(error, { location, file }); + return objectAssign(error, { location, file }); } diff --git a/src/watch-run.ts b/src/watch-run.ts index 35d828c5c..556e57010 100644 --- a/src/watch-run.ts +++ b/src/watch-run.ts @@ -31,4 +31,4 @@ function makeWatchRun( }; } -export = makeWatchRun; \ No newline at end of file +export = makeWatchRun; From 8d4ea4f0a405ef814d2591a1ac27966880e8631e Mon Sep 17 00:00:00 2001 From: John Reilly Date: Thu, 27 Oct 2016 07:08:08 +0100 Subject: [PATCH 06/23] extracted getCompilerOptions --- src/compilerSetup.ts | 22 ++++++++++++++++++++++ src/index.ts | 23 ++++------------------- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/src/compilerSetup.ts b/src/compilerSetup.ts index 85d77be54..a61197eb8 100644 --- a/src/compilerSetup.ts +++ b/src/compilerSetup.ts @@ -1,4 +1,5 @@ import typescript = require('typescript'); +import objectAssign = require('object-assign'); const semver = require('semver'); import interfaces = require('./interfaces'); @@ -38,3 +39,24 @@ export function getCompiler( return { compiler, compilerCompatible, compilerDetailsLogMessage, errorMessage }; } + +export function getCompilerOptions( + compilerCompatible: boolean, + compiler: typeof typescript, + configParseResult: typescript.ParsedCommandLine +) { + const compilerOptions = objectAssign({}, configParseResult.options, { + skipDefaultLibCheck: true, + suppressOutputPathCheck: true, // This is why: https://github.com/Microsoft/TypeScript/issues/7363 + }); + + // if `module` is not specified and not using ES6 target, default to CJS module output + if ((!compilerOptions.module) && compilerOptions.target !== 2 /* ES6 */) { + compilerOptions.module = 1; /* CommonJS */ + } else if (compilerCompatible && semver.lt(compiler.version, '1.7.3-0') && compilerOptions.target === 2 /* ES6 */) { + // special handling for TS 1.6 and target: es6 + compilerOptions.module = 0 /* None */; + } + + return compilerOptions; +} diff --git a/src/index.ts b/src/index.ts index 7f46ba3d3..0460948a3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,7 +33,6 @@ function ensureTypeScriptInstance(loaderOptions: interfaces.LoaderOptions, loade } const log = logger.makeLogger(loaderOptions); - const { compiler, compilerCompatible, compilerDetailsLogMessage, errorMessage } = compilerSetup.getCompiler(loaderOptions, log); if (errorMessage) { @@ -53,11 +52,6 @@ function ensureTypeScriptInstance(loaderOptions: interfaces.LoaderOptions, loade modifiedFiles: null, }; - const compilerOptions: typescript.CompilerOptions = { - skipDefaultLibCheck: true, - suppressOutputPathCheck: true, // This is why: https://github.com/Microsoft/TypeScript/issues/7363 - }; - const { configFilePath, configFile, @@ -78,18 +72,8 @@ function ensureTypeScriptInstance(loaderOptions: interfaces.LoaderOptions, loade return { error: utils.makeError({ rawMessage: 'error while parsing tsconfig.json', file: configFilePath }) }; } - instance.compilerOptions = objectAssign(compilerOptions, configParseResult.options); - - // Load any available tsconfig.json file - let filesToLoad = configParseResult.fileNames; - - // if `module` is not specified and not using ES6 target, default to CJS module output - if ((!compilerOptions.module) && compilerOptions.target !== 2 /* ES6 */) { - compilerOptions.module = 1; /* CommonJS */ - } else if (compilerCompatible && semver.lt(compiler.version, '1.7.3-0') && compilerOptions.target === 2 /* ES6 */) { - // special handling for TS 1.6 and target: es6 - compilerOptions.module = 0 /* None */; - } + const compilerOptions = compilerSetup.getCompilerOptions(compilerCompatible, compiler, configParseResult); + instance.compilerOptions = compilerOptions; if (loaderOptions.transpileOnly) { // quick return for transpiling @@ -107,11 +91,12 @@ function ensureTypeScriptInstance(loaderOptions: interfaces.LoaderOptions, loade // Load initial files (core lib files, any files specified in tsconfig.json) let filePath: string; try { + const filesToLoad = configParseResult.fileNames; filesToLoad.forEach(fp => { filePath = path.normalize(fp); files[filePath] = { text: fs.readFileSync(filePath, 'utf-8'), - version: 0, + version: 0 }; }); } catch (exc) { From aff99b4db7765021816b30ee49afc8b222e92475 Mon Sep 17 00:00:00 2001 From: John Reilly Date: Thu, 27 Oct 2016 14:18:49 +0100 Subject: [PATCH 07/23] removed unused tsconfig.json and unused imports/params --- src/index.ts | 4 +--- src/servicesHost.ts | 5 ++--- src/tsconfig.json | 4 ++++ src/utils.ts | 2 +- tsconfig.json | 9 --------- 5 files changed, 8 insertions(+), 16 deletions(-) delete mode 100644 tsconfig.json diff --git a/src/index.ts b/src/index.ts index 0460948a3..e62789db8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,14 +4,12 @@ import fs = require('fs'); import loaderUtils = require('loader-utils'); import objectAssign = require('object-assign'); import arrify = require('arrify'); -const semver = require('semver'); require('colors'); import afterCompile = require('./after-compile'); import config = require('./config'); import compilerSetup = require('./compilerSetup'); import interfaces = require('./interfaces'); -import constants = require('./constants'); import utils = require('./utils'); import logger = require('./logger'); import makeServicesHost = require('./servicesHost'); @@ -110,7 +108,7 @@ function ensureTypeScriptInstance(loaderOptions: interfaces.LoaderOptions, loade ? /\.tsx?$|\.jsx?$/i : /\.tsx?$/i; - const servicesHost = makeServicesHost(files, scriptRegex, log, loader, compilerOptions, instance, compiler, configFilePath); + const servicesHost = makeServicesHost(files, scriptRegex, log, loader, compilerOptions, instance, compiler); loader._compiler.plugin("after-compile", afterCompile(instance, compiler, servicesHost, configFilePath)); loader._compiler.plugin("watch-run", watchRun(instance)); diff --git a/src/servicesHost.ts b/src/servicesHost.ts index d91854570..2741d847d 100644 --- a/src/servicesHost.ts +++ b/src/servicesHost.ts @@ -16,8 +16,7 @@ function makeServicesHost( loader: any, //TODO: not any compilerOptions: typescript.CompilerOptions, instance: interfaces.TSInstance, - compiler: typeof typescript, - configFilePath: string + compiler: typeof typescript ) { const newLine = compilerOptions.newLine === 0 /* CarriageReturnLineFeed */ ? constants.CarriageReturnLineFeed : @@ -47,7 +46,7 @@ function makeServicesHost( if (!file) { let text = utils.readFile(fileName); - if (!text) { return; } + if (!text) { return undefined; } file = files[fileName] = { version: 0, text }; } diff --git a/src/tsconfig.json b/src/tsconfig.json index 91402039e..7fbbcb5e7 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -1,6 +1,10 @@ { "compilerOptions": { "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": false, + "noUnusedLocals": true, + "noUnusedParameters": false, "suppressImplicitAnyIndexErrors": true, "module": "commonjs", "moduleResolution": "node", diff --git a/src/utils.ts b/src/utils.ts index 98d2e6e0e..c4c3c0663 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -48,7 +48,7 @@ export function readFile(fileName: string) { try { return fs.readFileSync(fileName, { encoding: 'utf8' }); } catch (e) { - return; + return undefined; } } diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index 35f96a537..000000000 --- a/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "compilerOptions": { - "module": "commonjs", - "moduleResolution": "node" - }, - "files": [ - "index.ts" - ] -} From 230b667a2c19600c9bd1c3f0cd4ffa395db18bb5 Mon Sep 17 00:00:00 2001 From: John Reilly Date: Thu, 27 Oct 2016 20:48:08 +0100 Subject: [PATCH 08/23] Update .npmignore --- .npmignore | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.npmignore b/.npmignore index d40cd89f4..73a9d504a 100644 --- a/.npmignore +++ b/.npmignore @@ -1,5 +1,4 @@ *.ts +.test test -.* src -typings \ No newline at end of file From 7315184886f46859388d05cf1c02f505ae0b3fed Mon Sep 17 00:00:00 2001 From: John Reilly Date: Thu, 27 Oct 2016 21:11:32 +0100 Subject: [PATCH 09/23] rename pushArray to registerWebpackErrors --- src/after-compile.ts | 8 ++++---- src/index.ts | 6 +++--- src/interfaces.ts | 7 ++++--- src/utils.ts | 4 ++-- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/after-compile.ts b/src/after-compile.ts index e150551cf..02a4313f4 100644 --- a/src/after-compile.ts +++ b/src/after-compile.ts @@ -26,7 +26,7 @@ function makeAfterCompile( // handle compiler option errors after the first compile if (getCompilerOptionDiagnostics) { getCompilerOptionDiagnostics = false; - utils.pushArray( + utils.registerWebpackErrors( compilation.errors, utils.formatErrors(languageService.getCompilerOptionsDiagnostics(), instance, { file: configFilePath || 'tsconfig.json' })); } @@ -94,12 +94,12 @@ function makeAfterCompile( // append errors let formattedErrors = utils.formatErrors(errors, instance, { module }); - utils.pushArray(module.errors, formattedErrors); - utils.pushArray(compilation.errors, formattedErrors); + utils.registerWebpackErrors(module.errors, formattedErrors); + utils.registerWebpackErrors(compilation.errors, formattedErrors); }); } else { // otherwise it's a more generic error - utils.pushArray(compilation.errors, utils.formatErrors(errors, instance, { file: filePath })); + utils.registerWebpackErrors(compilation.errors, utils.formatErrors(errors, instance, { file: filePath })); } }); diff --git a/src/index.ts b/src/index.ts index e62789db8..96710e819 100644 --- a/src/index.ts +++ b/src/index.ts @@ -63,7 +63,7 @@ function ensureTypeScriptInstance(loaderOptions: interfaces.LoaderOptions, loade const configParseResult = config.getConfigParseResult(compiler, configFile, configFilePath); if (configParseResult.errors.length) { - utils.pushArray( + utils.registerWebpackErrors( loader._module.errors, utils.formatErrors(configParseResult.errors, instance, { file: configFilePath })); @@ -79,7 +79,7 @@ function ensureTypeScriptInstance(loaderOptions: interfaces.LoaderOptions, loade const program = compiler.createProgram([], compilerOptions); const diagnostics = program.getOptionsDiagnostics(); - utils.pushArray( + utils.registerWebpackErrors( loader._module.errors, utils.formatErrors(diagnostics, instance, {file: configFilePath || 'tsconfig.json'})); @@ -183,7 +183,7 @@ function loader(contents: string) { ({ outputText, sourceMapText, diagnostics } = transpileResult); - utils.pushArray(this._module.errors, utils.formatErrors(diagnostics, instance, {module: this._module})); + utils.registerWebpackErrors(this._module.errors, utils.formatErrors(diagnostics, instance, {module: this._module})); } else { let langService = instance.languageService; diff --git a/src/interfaces.ts b/src/interfaces.ts index c777bc4ca..f68284d19 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -41,7 +41,7 @@ export interface WebpackModule { export interface WebpackNodeWatchFileSystem { watcher: { mtimes: number; // a guess - } + }; } export interface WebpackWatching { @@ -54,7 +54,8 @@ export interface Resolve { /** * The directory (absolute path) that contains your modules. * May also be an array of directories. - * This setting should be used to add individual directories to the search path. */ + * This setting should be used to add individual directories to the search path. + */ root?: string | string[]; /** * An array of directory names to be resolved to the current directory as well as its ancestors, and searched for modules. @@ -102,7 +103,7 @@ export interface TSInstances { } interface DependencyGraph { - [index: string]: string[] + [index: string]: string[]; } interface ReverseDependencyGraph { diff --git a/src/utils.ts b/src/utils.ts index c4c3c0663..26246eed7 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -5,8 +5,8 @@ import objectAssign = require('object-assign'); import constants = require('./constants'); import interfaces = require('./interfaces'); -export function pushArray(arr: T[], toPush: any) { - Array.prototype.splice.apply(arr, [0, 0].concat(toPush)); +export function registerWebpackErrors(existingErrors: interfaces.WebpackError[], errorsToPush: interfaces.WebpackError[]) { + Array.prototype.splice.apply(existingErrors, (<(number | interfaces.WebpackError)[]> [0, 0]).concat(errorsToPush)); } export function hasOwnProperty(obj: T, property: string) { From f36f4724a517643b007b2a471fa24aa06bdc942e Mon Sep 17 00:00:00 2001 From: John Reilly Date: Thu, 27 Oct 2016 21:24:47 +0100 Subject: [PATCH 10/23] split out instances --- src/index.ts | 109 +-------------------------------------------- src/instances.ts | 113 +++++++++++++++++++++++++++++++++++++++++++++++ src/tslint.json | 2 +- 3 files changed, 116 insertions(+), 108 deletions(-) create mode 100644 src/instances.ts diff --git a/src/index.ts b/src/index.ts index 96710e819..5b703db20 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,121 +1,16 @@ import typescript = require('typescript'); import path = require('path'); -import fs = require('fs'); import loaderUtils = require('loader-utils'); import objectAssign = require('object-assign'); import arrify = require('arrify'); require('colors'); -import afterCompile = require('./after-compile'); -import config = require('./config'); -import compilerSetup = require('./compilerSetup'); +import instances = require('./instances'); import interfaces = require('./interfaces'); import utils = require('./utils'); -import logger = require('./logger'); -import makeServicesHost = require('./servicesHost'); -import watchRun = require('./watch-run'); -let instances = {}; let webpackInstances: any = []; -/** - * The loader is executed once for each file seen by webpack. However, we need to keep - * a persistent instance of TypeScript that contains all of the files in the program - * along with definition files and options. This function either creates an instance - * or returns the existing one. Multiple instances are possible by using the - * `instance` property. - */ -function ensureTypeScriptInstance(loaderOptions: interfaces.LoaderOptions, loader: any): { instance?: interfaces.TSInstance, error?: interfaces.WebpackError } { - if (utils.hasOwnProperty(instances, loaderOptions.instance)) { - return { instance: instances[loaderOptions.instance] }; - } - - const log = logger.makeLogger(loaderOptions); - const { compiler, compilerCompatible, compilerDetailsLogMessage, errorMessage } = compilerSetup.getCompiler(loaderOptions, log); - - if (errorMessage) { - return { error: utils.makeError({ rawMessage: errorMessage }) }; - } - - const files: interfaces.TSFiles = {}; - const instance: interfaces.TSInstance = instances[loaderOptions.instance] = { - compiler, - compilerOptions: null, - loaderOptions, - files, - languageService: null, - version: 0, - dependencyGraph: {}, - reverseDependencyGraph: {}, - modifiedFiles: null, - }; - - const { - configFilePath, - configFile, - configFileError - } = config.getConfigFile(compiler, loader, loaderOptions, compilerCompatible, log, compilerDetailsLogMessage, instance); - - if (configFileError) { - return { error: configFileError }; - } - - const configParseResult = config.getConfigParseResult(compiler, configFile, configFilePath); - - if (configParseResult.errors.length) { - utils.registerWebpackErrors( - loader._module.errors, - utils.formatErrors(configParseResult.errors, instance, { file: configFilePath })); - - return { error: utils.makeError({ rawMessage: 'error while parsing tsconfig.json', file: configFilePath }) }; - } - - const compilerOptions = compilerSetup.getCompilerOptions(compilerCompatible, compiler, configParseResult); - instance.compilerOptions = compilerOptions; - - if (loaderOptions.transpileOnly) { - // 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(); - - utils.registerWebpackErrors( - loader._module.errors, - utils.formatErrors(diagnostics, instance, {file: configFilePath || 'tsconfig.json'})); - - return { instance: instances[loaderOptions.instance] = { compiler, compilerOptions, loaderOptions, files, dependencyGraph: {}, reverseDependencyGraph: {} }}; - } - - // Load initial files (core lib files, any files specified in tsconfig.json) - let filePath: string; - try { - const filesToLoad = configParseResult.fileNames; - filesToLoad.forEach(fp => { - filePath = path.normalize(fp); - files[filePath] = { - text: fs.readFileSync(filePath, 'utf-8'), - version: 0 - }; - }); - } catch (exc) { - return { error: utils.makeError({ - rawMessage: `A file specified in tsconfig.json could not be found: ${ filePath }` - }) }; - } - - // if allowJs is set then we should accept js(x) files - const scriptRegex = configFile.config.compilerOptions.allowJs - ? /\.tsx?$|\.jsx?$/i - : /\.tsx?$/i; - - const servicesHost = makeServicesHost(files, scriptRegex, log, loader, compilerOptions, instance, compiler); - - loader._compiler.plugin("after-compile", afterCompile(instance, compiler, servicesHost, configFilePath)); - loader._compiler.plugin("watch-run", watchRun(instance)); - - return { instance }; -} - function loader(contents: string) { this.cacheable && this.cacheable(); const callback = this.async(); @@ -144,7 +39,7 @@ function loader(contents: string) { } options.instance = webpackIndex + '_' + options.instance; - const { instance, error } = ensureTypeScriptInstance(options, this); + const { instance, error } = instances.ensureTypeScriptInstance(options, this); if (error) { callback(error); diff --git a/src/instances.ts b/src/instances.ts new file mode 100644 index 000000000..e18c3cfa3 --- /dev/null +++ b/src/instances.ts @@ -0,0 +1,113 @@ +import path = require('path'); +import fs = require('fs'); +require('colors'); + +import afterCompile = require('./after-compile'); +import config = require('./config'); +import compilerSetup = require('./compilerSetup'); +import interfaces = require('./interfaces'); +import utils = require('./utils'); +import logger = require('./logger'); +import makeServicesHost = require('./servicesHost'); +import watchRun = require('./watch-run'); + +const instances = {}; + +/** + * The loader is executed once for each file seen by webpack. However, we need to keep + * a persistent instance of TypeScript that contains all of the files in the program + * along with definition files and options. This function either creates an instance + * or returns the existing one. Multiple instances are possible by using the + * `instance` property. + */ +export function ensureTypeScriptInstance(loaderOptions: interfaces.LoaderOptions, loader: any): { instance?: interfaces.TSInstance, error?: interfaces.WebpackError } { + if (utils.hasOwnProperty(instances, loaderOptions.instance)) { + return { instance: instances[loaderOptions.instance] }; + } + + const log = logger.makeLogger(loaderOptions); + const { compiler, compilerCompatible, compilerDetailsLogMessage, errorMessage } = compilerSetup.getCompiler(loaderOptions, log); + + if (errorMessage) { + return { error: utils.makeError({ rawMessage: errorMessage }) }; + } + + const files: interfaces.TSFiles = {}; + const instance: interfaces.TSInstance = instances[loaderOptions.instance] = { + compiler, + compilerOptions: null, + loaderOptions, + files, + languageService: null, + version: 0, + dependencyGraph: {}, + reverseDependencyGraph: {}, + modifiedFiles: null, + }; + + const { + configFilePath, + configFile, + configFileError + } = config.getConfigFile(compiler, loader, loaderOptions, compilerCompatible, log, compilerDetailsLogMessage, instance); + + if (configFileError) { + return { error: configFileError }; + } + + const configParseResult = config.getConfigParseResult(compiler, configFile, configFilePath); + + if (configParseResult.errors.length) { + utils.registerWebpackErrors( + loader._module.errors, + utils.formatErrors(configParseResult.errors, instance, { file: configFilePath })); + + return { error: utils.makeError({ rawMessage: 'error while parsing tsconfig.json', file: configFilePath }) }; + } + + const compilerOptions = compilerSetup.getCompilerOptions(compilerCompatible, compiler, configParseResult); + instance.compilerOptions = compilerOptions; + + if (loaderOptions.transpileOnly) { + // 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(); + + utils.registerWebpackErrors( + loader._module.errors, + utils.formatErrors(diagnostics, instance, {file: configFilePath || 'tsconfig.json'})); + + return { instance: instances[loaderOptions.instance] = { compiler, compilerOptions, loaderOptions, files, dependencyGraph: {}, reverseDependencyGraph: {} }}; + } + + // Load initial files (core lib files, any files specified in tsconfig.json) + let filePath: string; + try { + const filesToLoad = configParseResult.fileNames; + filesToLoad.forEach(fp => { + filePath = path.normalize(fp); + files[filePath] = { + text: fs.readFileSync(filePath, 'utf-8'), + version: 0 + }; + }); + } catch (exc) { + return { error: utils.makeError({ + rawMessage: `A file specified in tsconfig.json could not be found: ${ filePath }` + }) }; + } + + // if allowJs is set then we should accept js(x) files + const scriptRegex = configFile.config.compilerOptions.allowJs + ? /\.tsx?$|\.jsx?$/i + : /\.tsx?$/i; + + const servicesHost = makeServicesHost(files, scriptRegex, log, loader, compilerOptions, instance, compiler); + + loader._compiler.plugin("after-compile", afterCompile(instance, compiler, servicesHost, configFilePath)); + loader._compiler.plugin("watch-run", watchRun(instance)); + + return { instance }; +} + diff --git a/src/tslint.json b/src/tslint.json index 58f8f15fd..251828e7c 100644 --- a/src/tslint.json +++ b/src/tslint.json @@ -9,7 +9,7 @@ "static-before-instance", "variables-before-functions" ], - "no-unused-variable": false, + "no-unused-variable": true, "no-var-requires": false, "quotemark": [ "single" From 97936387b303c0c312a471a54333d94a6aafaba6 Mon Sep 17 00:00:00 2001 From: John Reilly Date: Fri, 28 Oct 2016 07:12:58 +0100 Subject: [PATCH 11/23] start refactoring towards constructing instance last --- src/after-compile.ts | 9 ++++++--- src/config.ts | 5 ++--- src/index.ts | 2 +- src/instances.ts | 6 +++--- src/utils.ts | 9 +++++---- 5 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/after-compile.ts b/src/after-compile.ts index 02a4313f4..7bd625825 100644 --- a/src/after-compile.ts +++ b/src/after-compile.ts @@ -28,7 +28,10 @@ function makeAfterCompile( getCompilerOptionDiagnostics = false; utils.registerWebpackErrors( compilation.errors, - utils.formatErrors(languageService.getCompilerOptionsDiagnostics(), instance, { file: configFilePath || 'tsconfig.json' })); + utils.formatErrors(languageService.getCompilerOptionsDiagnostics(), + instance.loaderOptions, + compiler, + { file: configFilePath || 'tsconfig.json' })); } // build map of all modules based on normalized filename @@ -93,13 +96,13 @@ function makeAfterCompile( removeTSLoaderErrors(module.errors); // append errors - let formattedErrors = utils.formatErrors(errors, instance, { module }); + let formattedErrors = utils.formatErrors(errors, instance.loaderOptions, compiler, { module }); utils.registerWebpackErrors(module.errors, formattedErrors); utils.registerWebpackErrors(compilation.errors, formattedErrors); }); } else { // otherwise it's a more generic error - utils.registerWebpackErrors(compilation.errors, utils.formatErrors(errors, instance, { file: filePath })); + utils.registerWebpackErrors(compilation.errors, utils.formatErrors(errors, instance.loaderOptions, compiler, { file: filePath })); } }); diff --git a/src/config.ts b/src/config.ts index 0f1fcc5d6..75468d79a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -17,8 +17,7 @@ export function getConfigFile( loaderOptions: interfaces.LoaderOptions, compilerCompatible: boolean, log: logger.Logger, - compilerDetailsLogMessage: string, - instance: interfaces.TSInstance + compilerDetailsLogMessage: string ) { const configFilePath = findConfigFile(compiler, path.dirname(loader.resourcePath), loaderOptions.configFileName); let configFileError: interfaces.WebpackError; @@ -39,7 +38,7 @@ export function getConfigFile( ); if (configFile.error) { - configFileError = utils.formatErrors([configFile.error], instance, { file: configFilePath })[0]; + configFileError = utils.formatErrors([configFile.error], loaderOptions, compiler, { file: configFilePath })[0]; } } else { if (compilerCompatible) { log.logInfo(compilerDetailsLogMessage.green); } diff --git a/src/index.ts b/src/index.ts index 5b703db20..678136ded 100644 --- a/src/index.ts +++ b/src/index.ts @@ -78,7 +78,7 @@ function loader(contents: string) { ({ outputText, sourceMapText, diagnostics } = transpileResult); - utils.registerWebpackErrors(this._module.errors, utils.formatErrors(diagnostics, instance, {module: this._module})); + utils.registerWebpackErrors(this._module.errors, utils.formatErrors(diagnostics, instance.loaderOptions, instance.compiler, {module: this._module})); } else { let langService = instance.languageService; diff --git a/src/instances.ts b/src/instances.ts index e18c3cfa3..fa1cd9df2 100644 --- a/src/instances.ts +++ b/src/instances.ts @@ -49,7 +49,7 @@ export function ensureTypeScriptInstance(loaderOptions: interfaces.LoaderOptions configFilePath, configFile, configFileError - } = config.getConfigFile(compiler, loader, loaderOptions, compilerCompatible, log, compilerDetailsLogMessage, instance); + } = config.getConfigFile(compiler, loader, loaderOptions, compilerCompatible, log, compilerDetailsLogMessage); if (configFileError) { return { error: configFileError }; @@ -60,7 +60,7 @@ export function ensureTypeScriptInstance(loaderOptions: interfaces.LoaderOptions if (configParseResult.errors.length) { utils.registerWebpackErrors( loader._module.errors, - utils.formatErrors(configParseResult.errors, instance, { file: configFilePath })); + utils.formatErrors(configParseResult.errors, loaderOptions, compiler, { file: configFilePath })); return { error: utils.makeError({ rawMessage: 'error while parsing tsconfig.json', file: configFilePath }) }; } @@ -76,7 +76,7 @@ export function ensureTypeScriptInstance(loaderOptions: interfaces.LoaderOptions utils.registerWebpackErrors( loader._module.errors, - utils.formatErrors(diagnostics, instance, {file: configFilePath || 'tsconfig.json'})); + utils.formatErrors(diagnostics, loaderOptions, compiler, {file: configFilePath || 'tsconfig.json'})); return { instance: instances[loaderOptions.instance] = { compiler, compilerOptions, loaderOptions, files, dependencyGraph: {}, reverseDependencyGraph: {} }}; } diff --git a/src/utils.ts b/src/utils.ts index 26246eed7..85e1fc95e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -19,16 +19,17 @@ export function hasOwnProperty(obj: T, property: string) { */ export function formatErrors( diagnostics: typescript.Diagnostic[], - instance: interfaces.TSInstance, + loaderOptions: interfaces.LoaderOptions, + compiler: typeof typescript, merge?: any): interfaces.WebpackError[] { return diagnostics - .filter(diagnostic => instance.loaderOptions.ignoreDiagnostics.indexOf(diagnostic.code) === -1) + .filter(diagnostic => loaderOptions.ignoreDiagnostics.indexOf(diagnostic.code) === -1) .map(diagnostic => { - const errorCategory = instance.compiler.DiagnosticCategory[diagnostic.category].toLowerCase(); + const errorCategory = compiler.DiagnosticCategory[diagnostic.category].toLowerCase(); const errorCategoryAndCode = errorCategory + ' TS' + diagnostic.code + ': '; - const messageText = errorCategoryAndCode + instance.compiler.flattenDiagnosticMessageText(diagnostic.messageText, constants.EOL); + const messageText = errorCategoryAndCode + compiler.flattenDiagnosticMessageText(diagnostic.messageText, constants.EOL); if (diagnostic.file) { const lineChar = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); return makeError({ From 2bd6833b77f5fa065a4723d8ea96708f4c0dedef Mon Sep 17 00:00:00 2001 From: John Reilly Date: Fri, 28 Oct 2016 20:42:22 +0100 Subject: [PATCH 12/23] move instance creation until last possible moment --- src/after-compile.ts | 6 +----- src/instances.ts | 33 ++++++++++++++++----------------- src/interfaces.ts | 2 +- src/servicesHost.ts | 28 +++++++++++++++------------- src/tslint.json | 2 +- 5 files changed, 34 insertions(+), 37 deletions(-) diff --git a/src/after-compile.ts b/src/after-compile.ts index 7bd625825..fefab1ddc 100644 --- a/src/after-compile.ts +++ b/src/after-compile.ts @@ -1,15 +1,12 @@ -import typescript = require('typescript'); import interfaces = require('./interfaces'); import path = require('path'); import utils = require('./utils'); function makeAfterCompile( instance: interfaces.TSInstance, - compiler: typeof typescript, - servicesHost: typescript.LanguageServiceHost, configFilePath: string ) { - const languageService = instance.languageService = compiler.createLanguageService(servicesHost, compiler.createDocumentRegistry()); + const { compiler, languageService } = instance; let getCompilerOptionDiagnostics = true; let checkAllFilesForErrors = true; @@ -106,7 +103,6 @@ function makeAfterCompile( } }); - // gather all declaration files from TypeScript and output them to webpack Object.keys(filesToCheckForErrors) .filter(filePath => !!filePath.match(/\.ts(x?)$/)) diff --git a/src/instances.ts b/src/instances.ts index fa1cd9df2..1a16fe143 100644 --- a/src/instances.ts +++ b/src/instances.ts @@ -32,19 +32,6 @@ export function ensureTypeScriptInstance(loaderOptions: interfaces.LoaderOptions return { error: utils.makeError({ rawMessage: errorMessage }) }; } - const files: interfaces.TSFiles = {}; - const instance: interfaces.TSInstance = instances[loaderOptions.instance] = { - compiler, - compilerOptions: null, - loaderOptions, - files, - languageService: null, - version: 0, - dependencyGraph: {}, - reverseDependencyGraph: {}, - modifiedFiles: null, - }; - const { configFilePath, configFile, @@ -66,7 +53,7 @@ export function ensureTypeScriptInstance(loaderOptions: interfaces.LoaderOptions } const compilerOptions = compilerSetup.getCompilerOptions(compilerCompatible, compiler, configParseResult); - instance.compilerOptions = compilerOptions; + const files: interfaces.TSFiles = {}; if (loaderOptions.transpileOnly) { // quick return for transpiling @@ -103,11 +90,23 @@ export function ensureTypeScriptInstance(loaderOptions: interfaces.LoaderOptions ? /\.tsx?$|\.jsx?$/i : /\.tsx?$/i; - const servicesHost = makeServicesHost(files, scriptRegex, log, loader, compilerOptions, instance, compiler); + const instance: interfaces.TSInstance = instances[loaderOptions.instance] = { + compiler, + compilerOptions, + loaderOptions, + files, + languageService: null, + version: 0, + dependencyGraph: {}, + reverseDependencyGraph: {}, + modifiedFiles: null, + }; + + const servicesHost = makeServicesHost(scriptRegex, log, loader, instance); + instance.languageService = compiler.createLanguageService(servicesHost, compiler.createDocumentRegistry()); - loader._compiler.plugin("after-compile", afterCompile(instance, compiler, servicesHost, configFilePath)); + loader._compiler.plugin("after-compile", afterCompile(instance, configFilePath)); loader._compiler.plugin("watch-run", watchRun(instance)); return { instance }; } - diff --git a/src/interfaces.ts b/src/interfaces.ts index f68284d19..149edd5e2 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -109,7 +109,7 @@ interface DependencyGraph { interface ReverseDependencyGraph { [index: string]: { [index: string]: boolean - } + }; } export interface LoaderOptions { diff --git a/src/servicesHost.ts b/src/servicesHost.ts index 2741d847d..b58d21291 100644 --- a/src/servicesHost.ts +++ b/src/servicesHost.ts @@ -10,14 +10,13 @@ import utils = require('./utils'); * Create the TypeScript language service */ function makeServicesHost( - files: interfaces.TSFiles, scriptRegex: RegExp, log: logger.Logger, - loader: any, //TODO: not any - compilerOptions: typescript.CompilerOptions, - instance: interfaces.TSInstance, - compiler: typeof typescript + loader: any, // TODO: not any + instance: interfaces.TSInstance ) { + const { compiler, compilerOptions, files } = instance; + const newLine = compilerOptions.newLine === 0 /* CarriageReturnLineFeed */ ? constants.CarriageReturnLineFeed : compilerOptions.newLine === 1 /* LineFeed */ ? constants.LineFeed : @@ -57,12 +56,12 @@ function makeServicesHost( * getDirectories is also required for full import and type reference completions. * Without it defined, certain completions will not be provided */ - getDirectories: typescript.sys ? (typescript.sys).getDirectories : undefined, + getDirectories: typescript.sys ? ( typescript.sys).getDirectories : undefined, /** * For @types expansion, these two functions are needed. */ - directoryExists: typescript.sys ? (typescript.sys).directoryExists : undefined, + directoryExists: typescript.sys ? ( typescript.sys).directoryExists : undefined, getCurrentDirectory: () => process.cwd(), getCompilationSettings: () => compilerOptions, @@ -79,10 +78,12 @@ function makeServicesHost( try { resolvedFileName = resolver.resolveSync(path.normalize(path.dirname(containingFile)), moduleName); - if (!resolvedFileName.match(scriptRegex)) resolvedFileName = null; - else resolutionResult = { resolvedFileName }; - } - catch (e) { resolvedFileName = null; } + if (!resolvedFileName.match(scriptRegex)) { + resolvedFileName = null; + } else { + resolutionResult = { resolvedFileName }; + } + } catch (e) { resolvedFileName = null; } let tsResolution = compiler.resolveModuleName(moduleName, containingFile, compilerOptions, moduleResolutionHost); @@ -91,8 +92,9 @@ function makeServicesHost( if (resolvedFileName === tsResolution.resolvedModule.resolvedFileName) { resolutionResult.isExternalLibraryImport = tsResolution.resolvedModule.isExternalLibraryImport; } + } else { + resolutionResult = tsResolution.resolvedModule; } - else resolutionResult = tsResolution.resolvedModule; } resolvedModules.push(resolutionResult); @@ -114,4 +116,4 @@ function makeServicesHost( }; } -export = makeServicesHost; \ No newline at end of file +export = makeServicesHost; diff --git a/src/tslint.json b/src/tslint.json index 251828e7c..3b364fe66 100644 --- a/src/tslint.json +++ b/src/tslint.json @@ -9,7 +9,7 @@ "static-before-instance", "variables-before-functions" ], - "no-unused-variable": true, + "no-unused-variable": [true, "check-parameters"], "no-var-requires": false, "quotemark": [ "single" From c71fe163c1b43f1b1293ee36296de97ca311016a Mon Sep 17 00:00:00 2001 From: John Reilly Date: Sat, 29 Oct 2016 06:20:19 +0100 Subject: [PATCH 13/23] use const where possible --- src/after-compile.ts | 12 ++++++------ src/index.ts | 7 +++---- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/after-compile.ts b/src/after-compile.ts index fefab1ddc..7452a7675 100644 --- a/src/after-compile.ts +++ b/src/after-compile.ts @@ -34,7 +34,7 @@ function makeAfterCompile( // build map of all modules based on normalized filename // this is used for quick-lookup when trying to find modules // based on filepath - let modules: { [modulePath: string]: interfaces.WebpackModule[] } = {}; + const modules: { [modulePath: string]: interfaces.WebpackModule[] } = {}; compilation.modules.forEach(module => { if (module.resource) { let modulePath = path.normalize(module.resource); @@ -86,14 +86,14 @@ function makeAfterCompile( // if we have access to a webpack module, use that if (utils.hasOwnProperty(modules, filePath)) { - let associatedModules = modules[filePath]; + const associatedModules = modules[filePath]; associatedModules.forEach(module => { // remove any existing errors removeTSLoaderErrors(module.errors); // append errors - let formattedErrors = utils.formatErrors(errors, instance.loaderOptions, compiler, { module }); + const formattedErrors = utils.formatErrors(errors, instance.loaderOptions, compiler, { module }); utils.registerWebpackErrors(module.errors, formattedErrors); utils.registerWebpackErrors(compilation.errors, formattedErrors); }); @@ -107,10 +107,10 @@ function makeAfterCompile( Object.keys(filesToCheckForErrors) .filter(filePath => !!filePath.match(/\.ts(x?)$/)) .forEach(filePath => { - let output = languageService.getEmitOutput(filePath); - let declarationFile = output.outputFiles.filter(fp => !!fp.name.match(/\.d.ts$/)).pop(); + const output = languageService.getEmitOutput(filePath); + const declarationFile = output.outputFiles.filter(fp => !!fp.name.match(/\.d.ts$/)).pop(); if (declarationFile) { - let assetPath = path.relative(compilation.compiler.context, declarationFile.name); + const assetPath = path.relative(compilation.compiler.context, declarationFile.name); compilation.assets[assetPath] = { source: () => declarationFile.text, size: () => declarationFile.text.length, diff --git a/src/index.ts b/src/index.ts index 678136ded..dc3e9dc42 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,7 @@ import interfaces = require('./interfaces'); import utils = require('./utils'); let webpackInstances: any = []; +const definitionFileRegex = /\.d\.ts$/; function loader(contents: string) { this.cacheable && this.cacheable(); @@ -80,16 +81,14 @@ function loader(contents: string) { utils.registerWebpackErrors(this._module.errors, utils.formatErrors(diagnostics, instance.loaderOptions, instance.compiler, {module: this._module})); } else { - let langService = instance.languageService; - // Emit Javascript - const output = langService.getEmitOutput(filePath); + const output = instance.languageService.getEmitOutput(filePath); // Make this file dependent on *all* definition files in the program this.clearDependencies(); this.addDependency(filePath); - let allDefinitionFiles = Object.keys(instance.files).filter(fp => /\.d\.ts$/.test(fp)); + let allDefinitionFiles = Object.keys(instance.files).filter(fp => definitionFileRegex.test(fp)); allDefinitionFiles.forEach(this.addDependency.bind(this)); // Additionally make this file dependent on all imported files From 96d08a20bd9c6cef9c63bf946fa543fa4762962e Mon Sep 17 00:00:00 2001 From: John Reilly Date: Sat, 29 Oct 2016 06:37:41 +0100 Subject: [PATCH 14/23] split out update file in cache --- src/index.ts | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/src/index.ts b/src/index.ts index dc3e9dc42..3893c2197 100644 --- a/src/index.ts +++ b/src/index.ts @@ -47,23 +47,7 @@ function loader(contents: string) { return; } - // Update file contents - let file = instance.files[filePath]; - if (!file) { - file = instance.files[filePath] = { version: 0 }; - } - - if (file.text !== contents) { - file.version++; - file.text = contents; - instance.version++; - } - - // push this file to modified files hash. - if (!instance.modifiedFiles) { - instance.modifiedFiles = {}; - } - instance.modifiedFiles[filePath] = file; + const file = updateFileInCache(filePath, contents, instance); let outputText: string; let sourceMapText: string; @@ -129,4 +113,25 @@ function loader(contents: string) { callback(null, outputText, sourceMap); } +function updateFileInCache(filePath: string, contents: string, instance: interfaces.TSInstance) { + // Update file contents + let file = instance.files[filePath]; + if (!file) { + file = instance.files[filePath] = { version: 0 }; + } + + if (file.text !== contents) { + file.version++; + file.text = contents; + instance.version++; + } + + // push this file to modified files hash. + if (!instance.modifiedFiles) { + instance.modifiedFiles = {}; + } + instance.modifiedFiles[filePath] = file; + return file; +} + export = loader; From c4df665a1db53998af8334de01649c6e9b16845d Mon Sep 17 00:00:00 2001 From: John Reilly Date: Sat, 29 Oct 2016 07:39:53 +0100 Subject: [PATCH 15/23] further modularised index --- src/index.ts | 168 ++++++++++++++++++++++++++-------------------- src/interfaces.ts | 6 ++ 2 files changed, 100 insertions(+), 74 deletions(-) diff --git a/src/index.ts b/src/index.ts index 3893c2197..6ad162acc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,3 @@ -import typescript = require('typescript'); import path = require('path'); import loaderUtils = require('loader-utils'); import objectAssign = require('object-assign'); @@ -17,28 +16,7 @@ function loader(contents: string) { const callback = this.async(); const filePath = path.normalize(this.resourcePath); - const queryOptions = loaderUtils.parseQuery(this.query); - const configFileOptions = this.options.ts || {}; - - const options = objectAssign({}, { - silent: false, - logLevel: 'INFO', - logInfoToStdOut: false, - instance: 'default', - compiler: 'typescript', - configFileName: 'tsconfig.json', - transpileOnly: false, - compilerOptions: {}, - }, configFileOptions, queryOptions); - options.ignoreDiagnostics = arrify(options.ignoreDiagnostics).map(Number); - options.logLevel = options.logLevel.toUpperCase(); - - // differentiate the TypeScript instance based on the webpack instance - let webpackIndex = webpackInstances.indexOf(this._compiler); - if (webpackIndex === -1) { - webpackIndex = webpackInstances.push(this._compiler) - 1; - } - options.instance = webpackIndex + '_' + options.instance; + const options = makeOptions(this); const { instance, error } = instances.ensureTypeScriptInstance(options, this); @@ -49,68 +27,49 @@ function loader(contents: string) { const file = updateFileInCache(filePath, contents, instance); - let outputText: string; - let sourceMapText: string; - let diagnostics: typescript.Diagnostic[] = []; - - if (options.transpileOnly) { - const fileName = path.basename(filePath); - const transpileResult = instance.compiler.transpileModule(contents, { - compilerOptions: instance.compilerOptions, - reportDiagnostics: true, - fileName, - }); - - ({ outputText, sourceMapText, diagnostics } = transpileResult); - - utils.registerWebpackErrors(this._module.errors, utils.formatErrors(diagnostics, instance.loaderOptions, instance.compiler, {module: this._module})); - } else { - // Emit Javascript - const output = instance.languageService.getEmitOutput(filePath); - - // Make this file dependent on *all* definition files in the program - this.clearDependencies(); - this.addDependency(filePath); - - let allDefinitionFiles = Object.keys(instance.files).filter(fp => definitionFileRegex.test(fp)); - allDefinitionFiles.forEach(this.addDependency.bind(this)); - - // Additionally make this file dependent on all imported files - let additionalDependencies = instance.dependencyGraph[filePath]; - if (additionalDependencies) { - additionalDependencies.forEach(this.addDependency.bind(this)); - } - - this._module.meta.tsLoaderDefinitionFileVersions = allDefinitionFiles - .concat(additionalDependencies) - .map(fp => fp + '@' + (instance.files[fp] || {version: '?'}).version); - - const outputFile = output.outputFiles.filter(f => !!f.name.match(/\.js(x?)$/)).pop(); - if (outputFile) { outputText = outputFile.text; } - - const sourceMapFile = output.outputFiles.filter(f => !!f.name.match(/\.js(x?)\.map$/)).pop(); - if (sourceMapFile) { sourceMapText = sourceMapFile.text; } - } + let { outputText, sourceMapText } = options.transpileOnly + ? getTranspilationEmit(filePath, contents, instance, this) + : getEmit(filePath, instance, this); if (outputText === null || outputText === undefined) { throw new Error(`Typescript emitted no output for ${filePath}`); } - let sourceMap: { sources: any[], file: string; sourcesContent: string[] }; - if (sourceMapText) { - sourceMap = JSON.parse(sourceMapText); - sourceMap.sources = [loaderUtils.getRemainingRequest(this)]; - sourceMap.file = filePath; - sourceMap.sourcesContent = [contents]; - outputText = outputText.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, ''); - } + const { sourceMap, output } = makeSourceMap(sourceMapText, outputText, filePath, contents, this); // Make sure webpack is aware that even though the emitted JavaScript may be the same as // a previously cached version the TypeScript may be different and therefore should be // treated as new this._module.meta.tsLoaderFileVersion = file.version; - callback(null, outputText, sourceMap); + callback(null, output, sourceMap); +} + +function makeOptions(loader: any) { + const queryOptions = loaderUtils.parseQuery(loader.query); + const configFileOptions = loader.options.ts || {}; + + const options = objectAssign({}, { + silent: false, + logLevel: 'INFO', + logInfoToStdOut: false, + instance: 'default', + compiler: 'typescript', + configFileName: 'tsconfig.json', + transpileOnly: false, + compilerOptions: {}, + }, configFileOptions, queryOptions); + options.ignoreDiagnostics = arrify(options.ignoreDiagnostics).map(Number); + options.logLevel = options.logLevel.toUpperCase(); + + // differentiate the TypeScript instance based on the webpack instance + let webpackIndex = webpackInstances.indexOf(loader._compiler); + if (webpackIndex === -1) { + webpackIndex = webpackInstances.push(loader._compiler) - 1; + } + options.instance = webpackIndex + '_' + options.instance; + + return options; } function updateFileInCache(filePath: string, contents: string, instance: interfaces.TSInstance) { @@ -134,4 +93,65 @@ function updateFileInCache(filePath: string, contents: string, instance: interfa return file; } +function getEmit(filePath: string, instance: interfaces.TSInstance, loader: any) { + // Emit Javascript + const output = instance.languageService.getEmitOutput(filePath); + + // Make this file dependent on *all* definition files in the program + loader.clearDependencies(); + loader.addDependency(filePath); + + const allDefinitionFiles = Object.keys(instance.files).filter(fp => definitionFileRegex.test(fp)); + allDefinitionFiles.forEach(loader.addDependency.bind(loader)); + + // Additionally make this file dependent on all imported files + let additionalDependencies = instance.dependencyGraph[filePath]; + if (additionalDependencies) { + additionalDependencies.forEach(loader.addDependency.bind(loader)); + } + + loader._module.meta.tsLoaderDefinitionFileVersions = allDefinitionFiles + .concat(additionalDependencies) + .map(fp => fp + '@' + (instance.files[fp] || {version: '?'}).version); + + const outputFile = output.outputFiles.filter(f => !!f.name.match(/\.js(x?)$/)).pop(); + const outputText = (outputFile) ? outputFile.text : undefined; + + const sourceMapFile = output.outputFiles.filter(f => !!f.name.match(/\.js(x?)\.map$/)).pop(); + const sourceMapText = (sourceMapFile) ? sourceMapFile.text : undefined; + + return { outputText, sourceMapText }; +} + +function getTranspilationEmit(filePath: string, contents: string, instance: interfaces.TSInstance, loader: any) { + const fileName = path.basename(filePath); + const transpileResult = instance.compiler.transpileModule(contents, { + compilerOptions: instance.compilerOptions, + reportDiagnostics: true, + fileName, + }); + + const { outputText, sourceMapText, diagnostics } = transpileResult; + + utils.registerWebpackErrors(loader._module.errors, utils.formatErrors(diagnostics, instance.loaderOptions, instance.compiler, {module: loader._module})); + + return { outputText, sourceMapText }; +} + +function makeSourceMap(sourceMapText: string, outputText: string, filePath: string, contents: string, loader: any) { + if (!sourceMapText) { + return { output: outputText, sourceMap: undefined as interfaces.SourceMap }; + } + + const sourceMap = JSON.parse(sourceMapText); + sourceMap.sources = [loaderUtils.getRemainingRequest(loader)]; + sourceMap.file = filePath; + sourceMap.sourcesContent = [contents]; + + return { + output: outputText.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, ''), + sourceMap + }; +} + export = loader; diff --git a/src/interfaces.ts b/src/interfaces.ts index 149edd5e2..c31e9a279 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -1,5 +1,11 @@ import typescript = require('typescript'); +export interface SourceMap { + sources: any[]; + file: string; + sourcesContent: string[]; +} + export interface WebpackError { module?: any; file?: string; From 2047cc8fafe7cfc032a3ae133930ec67d6cf1494 Mon Sep 17 00:00:00 2001 From: John Reilly Date: Sat, 29 Oct 2016 15:09:21 +0100 Subject: [PATCH 16/23] up version to 1.0 beta 1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f446c3328..6ed63cd75 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ts-loader", - "version": "0.9.5", + "version": "1.0.0-beta.1", "description": "TypeScript loader for webpack", "main": "index.js", "scripts": { From 51b769842f1e66b35a36d7ce28a867748462e820 Mon Sep 17 00:00:00 2001 From: John Reilly Date: Sat, 29 Oct 2016 19:26:11 +0100 Subject: [PATCH 17/23] typed Webpack --- src/config.ts | 2 +- src/index.ts | 25 ++++++++++--- src/instances.ts | 5 ++- src/interfaces.ts | 27 ++++++++++++++ src/logger.ts | 4 +-- src/resolver.ts | 88 ++++++++++++++++++++++----------------------- src/servicesHost.ts | 2 +- src/watch-run.ts | 1 - 8 files changed, 99 insertions(+), 55 deletions(-) diff --git a/src/config.ts b/src/config.ts index 75468d79a..90a6e37d7 100644 --- a/src/config.ts +++ b/src/config.ts @@ -13,7 +13,7 @@ interface ConfigFile { export function getConfigFile( compiler: typeof typescript, - loader: any, + loader: interfaces.Webpack, loaderOptions: interfaces.LoaderOptions, compilerCompatible: boolean, log: logger.Logger, diff --git a/src/index.ts b/src/index.ts index 6ad162acc..a43f8c03f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,7 +11,7 @@ import utils = require('./utils'); let webpackInstances: any = []; const definitionFileRegex = /\.d\.ts$/; -function loader(contents: string) { +function loader(this: interfaces.Webpack, contents: string) { this.cacheable && this.cacheable(); const callback = this.async(); const filePath = path.normalize(this.resourcePath); @@ -45,7 +45,7 @@ function loader(contents: string) { callback(null, output, sourceMap); } -function makeOptions(loader: any) { +function makeOptions(loader: interfaces.Webpack) { const queryOptions = loaderUtils.parseQuery(loader.query); const configFileOptions = loader.options.ts || {}; @@ -93,7 +93,11 @@ function updateFileInCache(filePath: string, contents: string, instance: interfa return file; } -function getEmit(filePath: string, instance: interfaces.TSInstance, loader: any) { +function getEmit( + filePath: string, + instance: interfaces.TSInstance, + loader: interfaces.Webpack +) { // Emit Javascript const output = instance.languageService.getEmitOutput(filePath); @@ -123,7 +127,12 @@ function getEmit(filePath: string, instance: interfaces.TSInstance, loader: any) return { outputText, sourceMapText }; } -function getTranspilationEmit(filePath: string, contents: string, instance: interfaces.TSInstance, loader: any) { +function getTranspilationEmit( + filePath: string, + contents: string, + instance: interfaces.TSInstance, + loader: interfaces.Webpack +) { const fileName = path.basename(filePath); const transpileResult = instance.compiler.transpileModule(contents, { compilerOptions: instance.compilerOptions, @@ -138,7 +147,13 @@ function getTranspilationEmit(filePath: string, contents: string, instance: inte return { outputText, sourceMapText }; } -function makeSourceMap(sourceMapText: string, outputText: string, filePath: string, contents: string, loader: any) { +function makeSourceMap( + sourceMapText: string, + outputText: string, + filePath: string, + contents: string, + loader: interfaces.Webpack +) { if (!sourceMapText) { return { output: outputText, sourceMap: undefined as interfaces.SourceMap }; } diff --git a/src/instances.ts b/src/instances.ts index 1a16fe143..b1e0b799d 100644 --- a/src/instances.ts +++ b/src/instances.ts @@ -20,7 +20,10 @@ const instances = {}; * or returns the existing one. Multiple instances are possible by using the * `instance` property. */ -export function ensureTypeScriptInstance(loaderOptions: interfaces.LoaderOptions, loader: any): { instance?: interfaces.TSInstance, error?: interfaces.WebpackError } { +export function ensureTypeScriptInstance( + loaderOptions: interfaces.LoaderOptions, + loader: interfaces.Webpack +): { instance?: interfaces.TSInstance, error?: interfaces.WebpackError } { if (utils.hasOwnProperty(instances, loaderOptions.instance)) { return { instance: instances[loaderOptions.instance] }; } diff --git a/src/interfaces.ts b/src/interfaces.ts index c31e9a279..e38cdf1f4 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -6,6 +6,33 @@ export interface SourceMap { sourcesContent: string[]; } +export interface Webpack { + _compiler: Compiler; + _module: { + meta: { + tsLoaderFileVersion: number; + tsLoaderDefinitionFileVersions: string[]; + }, + errors: WebpackError[]; + }; + cacheable: () => void; + query: string; + async: () => (err: Error | WebpackError, source?: string, map?: string) => void; + resourcePath: string; + resolve: () => void; // unused yet... + addDependency: (dep: string) => void; + clearDependencies: () => void; + emitFile: (fileName: string, text: string) => void; // unused + options: { + ts: {}, + resolve: Resolve; + }; +} + +interface Compiler { + plugin: (name: string, callback: Function) => void; +} + export interface WebpackError { module?: any; file?: string; diff --git a/src/logger.ts b/src/logger.ts index b5a1d1f31..1ddb91c3e 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -11,7 +11,7 @@ enum LogLevel { } interface InternalLoggerFunc { - (whereToLog: any, messages: string[]): void + (whereToLog: any, messages: string[]): void; } const doNothingLogger = (...messages: string[]) => {}; @@ -46,7 +46,7 @@ function makeLogWarning(loaderOptions: interfaces.LoaderOptions, logger: Interna } interface LoggerFunc { - (...messages: string[]): void + (...messages: string[]): void; } export interface Logger { diff --git a/src/resolver.ts b/src/resolver.ts index 656055296..230ddbeb0 100644 --- a/src/resolver.ts +++ b/src/resolver.ts @@ -3,56 +3,56 @@ // that webpack does. import interfaces = require('./interfaces'); -var Resolver = require("enhanced-resolve/lib/Resolver"); -var SyncNodeJsInputFileSystem = require("enhanced-resolve/lib/SyncNodeJsInputFileSystem"); -var CachedInputFileSystem = require("enhanced-resolve/lib/CachedInputFileSystem"); -var UnsafeCachePlugin = require("enhanced-resolve/lib/UnsafeCachePlugin"); -var ModulesInDirectoriesPlugin = require("enhanced-resolve/lib/ModulesInDirectoriesPlugin"); -var ModulesInRootPlugin = require("enhanced-resolve/lib/ModulesInRootPlugin"); -var ModuleAsFilePlugin = require("enhanced-resolve/lib/ModuleAsFilePlugin"); -var ModuleAsDirectoryPlugin = require("enhanced-resolve/lib/ModuleAsDirectoryPlugin"); -var ModuleAliasPlugin = require("enhanced-resolve/lib/ModuleAliasPlugin"); -var DirectoryDefaultFilePlugin = require("enhanced-resolve/lib/DirectoryDefaultFilePlugin"); -var DirectoryDescriptionFilePlugin = require("enhanced-resolve/lib/DirectoryDescriptionFilePlugin"); -var DirectoryDescriptionFileFieldAliasPlugin = require("enhanced-resolve/lib/DirectoryDescriptionFileFieldAliasPlugin"); -var FileAppendPlugin = require("enhanced-resolve/lib/FileAppendPlugin"); -var ResultSymlinkPlugin = require("enhanced-resolve/lib/ResultSymlinkPlugin"); +const Resolver = require("enhanced-resolve/lib/Resolver"); +const SyncNodeJsInputFileSystem = require("enhanced-resolve/lib/SyncNodeJsInputFileSystem"); +const CachedInputFileSystem = require("enhanced-resolve/lib/CachedInputFileSystem"); +const UnsafeCachePlugin = require("enhanced-resolve/lib/UnsafeCachePlugin"); +const ModulesInDirectoriesPlugin = require("enhanced-resolve/lib/ModulesInDirectoriesPlugin"); +const ModulesInRootPlugin = require("enhanced-resolve/lib/ModulesInRootPlugin"); +const ModuleAsFilePlugin = require("enhanced-resolve/lib/ModuleAsFilePlugin"); +const ModuleAsDirectoryPlugin = require("enhanced-resolve/lib/ModuleAsDirectoryPlugin"); +const ModuleAliasPlugin = require("enhanced-resolve/lib/ModuleAliasPlugin"); +const DirectoryDefaultFilePlugin = require("enhanced-resolve/lib/DirectoryDefaultFilePlugin"); +const DirectoryDescriptionFilePlugin = require("enhanced-resolve/lib/DirectoryDescriptionFilePlugin"); +const DirectoryDescriptionFileFieldAliasPlugin = require("enhanced-resolve/lib/DirectoryDescriptionFileFieldAliasPlugin"); +const FileAppendPlugin = require("enhanced-resolve/lib/FileAppendPlugin"); +const ResultSymlinkPlugin = require("enhanced-resolve/lib/ResultSymlinkPlugin"); function makeRootPlugin(name: string, root: string | string[]) { - if(typeof root === "string") - return new ModulesInRootPlugin(name, root); - else if(Array.isArray(root)) { - return function() { - root.forEach(function(root) { - this.apply(new ModulesInRootPlugin(name, root)); - }, this); - }; - } - return function() {}; + if (typeof root === "string") { + return new ModulesInRootPlugin(name, root); + } else if (Array.isArray(root)) { + return function() { + root.forEach(function(root) { + this.apply(new ModulesInRootPlugin(name, root)); + }, this); + }; + } + return function() {}; } function makeResolver(options: { resolve: interfaces.Resolve }) { - let fileSystem = new CachedInputFileSystem(new SyncNodeJsInputFileSystem(), 60000); + let fileSystem = new CachedInputFileSystem(new SyncNodeJsInputFileSystem(), 60000); let resolver = new Resolver(fileSystem); - - // apply the same plugins that webpack does, see webpack/lib/WebpackOptionsApply.js - resolver.apply( - new UnsafeCachePlugin(options.resolve.unsafeCache), - options.resolve.packageAlias ? new DirectoryDescriptionFileFieldAliasPlugin("package.json", options.resolve.packageAlias) : function() {}, - new ModuleAliasPlugin(options.resolve.alias), - makeRootPlugin("module", options.resolve.root), - new ModulesInDirectoriesPlugin("module", options.resolve.modulesDirectories), - makeRootPlugin("module", options.resolve.fallback), - new ModuleAsFilePlugin("module"), - new ModuleAsDirectoryPlugin("module"), - new DirectoryDescriptionFilePlugin("package.json", options.resolve.packageMains), - new DirectoryDefaultFilePlugin(["index"]), - new FileAppendPlugin(options.resolve.extensions), - new ResultSymlinkPlugin() - ); - - return resolver; + + // apply the same plugins that webpack does, see webpack/lib/WebpackOptionsApply.js + resolver.apply( + new UnsafeCachePlugin(options.resolve.unsafeCache), + options.resolve.packageAlias ? new DirectoryDescriptionFileFieldAliasPlugin("package.json", options.resolve.packageAlias) : function() {}, + new ModuleAliasPlugin(options.resolve.alias), + makeRootPlugin("module", options.resolve.root), + new ModulesInDirectoriesPlugin("module", options.resolve.modulesDirectories), + makeRootPlugin("module", options.resolve.fallback), + new ModuleAsFilePlugin("module"), + new ModuleAsDirectoryPlugin("module"), + new DirectoryDescriptionFilePlugin("package.json", options.resolve.packageMains), + new DirectoryDefaultFilePlugin(["index"]), + new FileAppendPlugin(options.resolve.extensions), + new ResultSymlinkPlugin() + ); + + return resolver; } -export = makeResolver; \ No newline at end of file +export = makeResolver; diff --git a/src/servicesHost.ts b/src/servicesHost.ts index b58d21291..8ca49a448 100644 --- a/src/servicesHost.ts +++ b/src/servicesHost.ts @@ -12,7 +12,7 @@ import utils = require('./utils'); function makeServicesHost( scriptRegex: RegExp, log: logger.Logger, - loader: any, // TODO: not any + loader: interfaces.Webpack, instance: interfaces.TSInstance ) { const { compiler, compilerOptions, files } = instance; diff --git a/src/watch-run.ts b/src/watch-run.ts index 556e57010..8113b685c 100644 --- a/src/watch-run.ts +++ b/src/watch-run.ts @@ -8,7 +8,6 @@ import interfaces = require('./interfaces'); function makeWatchRun( instance: interfaces.TSInstance ) { - return (watching: interfaces.WebpackWatching, cb: () => void) => { const mtimes = watching.compiler.watchFileSystem.watcher.mtimes; if (null === instance.modifiedFiles) { From 2cc78dd9c1551dfff62e7f411ad48857819b016d Mon Sep 17 00:00:00 2001 From: John Reilly Date: Sun, 30 Oct 2016 06:45:10 +0000 Subject: [PATCH 18/23] add a publishing guide and split out testing guides --- CONTRIBUTING.md | 149 +++++--------------------------- src/interfaces.ts | 2 +- test/comparison-tests/README.md | 73 ++++++++++++++++ test/execution-tests/README.md | 61 +++++++++++++ 4 files changed, 156 insertions(+), 129 deletions(-) create mode 100644 test/comparison-tests/README.md create mode 100644 test/execution-tests/README.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8027c7f49..62a1336b2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,7 +19,7 @@ npm run build ## Changing Most of the information you need to contribute code changes can [be found here](https://guides.github.com/activities/contributing-to-open-source/). -In short: fork, branch, make your changes, and submit a pull request. +In short: fork, make your changes, and submit a pull request. ## Testing @@ -32,138 +32,31 @@ To run execution tests alone use `npm run execution-tests`. Not all bugs/features necessarily fit into either framework and that's OK. However, most do and therefore you should make every effort to create at least one test which demonstrates the issue or exercises the feature. Use your judgement to decide whether you think a comparison test or an execution test is most appropriate. -### Comparison Test Pack - -This test pack comprises a number of mini-typescript projects which, as part of the test run, are each run through webpack. -The outputs (both compiled JavaScript and webpack compilation output) are compared against a set of expected -outputs. These are particularly useful for testing failure cases; that is testing scenarios where you expect compilation -to fail and ensuring the failure is what you expect. For example, ensuring the presence of error messages from the TypeScript -compiler in the output etc. - -The comparison test pack can be found under `/test/comparison-tests`. The test harness uses certain conventions. All tests have their own directory under `/test/comparison-tests`, eg `/test/comparison-tests/someFeature`. Each test should have a `webpack.config.js` file which follows this general convention: - -```javascript -module.exports = { - entry: './app.ts', - output: { - filename: 'bundle.js' - }, - resolve: { - extensions: ['', '.ts', 'tsx', '.js'] - }, - module: { - loaders: [ - { test: /\.tsx?$/, loader: 'ts-loader' } - ] - } -} - -// for test harness purposes only, you would not need this in a normal project -module.exports.resolveLoader = { alias: { 'ts-loader': require('path').join(__dirname, "../../index.js") } } -``` - -You can run all the tests in the Comparison Test Pack with `npm run comparison-tests`. You can also go into an individual test directory and manually build a project using `webpack` or `webpack --watch`. This can be useful both when developing the test and also when fixing an issue or adding a feature. - -Each test should have an `expectedOutput` directory which contains any webpack filesystem output (typically `bundle.js` and possibly `bundle.js.map`) and any console output. stdout should go in `output.txt` and stderr should go in `err.txt`. - -To run all the tests use: - -`npm run comparison-tests`. - -If you would like to run just a single test then: - -`npm run comparison-tests -- --single-test nameOfTest` - -#### Regenerating test data - -As a convenience it is possible to regenerate the expected output from the actual output. This is useful when creating new tests and also when making a change that affects multiple existing tests. To run use: - -`npm run comparison-tests -- --save-output`. - -Note that all tests will automatically pass when using this feature. You should double check the generated files to make sure -the output is indeed correct. - -If you would like to regenerate a single test then combine `--save-output` with -`--single-test` like so: - -`npm run comparison-tests -- --save-output --single-test nameOfTest` - -#### Watch Specific Tests - -The test harness additionally supports tests which exercise watch mode, since that is such an integral part of webpack. Watch mode tests are just the as standard comparison tests. However, after the initial compilation and comparison, a series of "patches" are applied and tested. - -The patches live in folders following the naming convention of `/patchN` starting with 0. After the initial compilation and comparison, the patches are iterated through and the files in place are replaced with any modified files in the `/patchN` directory. After each patch application the compilation / comparison is performed once more. - -For example: - -Initial state: -- test/someFeature/app.ts -- test/someFeature/expectedOutput/bundle.js -- test/someFeature/expectedOutput/output.txt - -patch0 is applied: -- test/someFeature/patch0/app.ts - *modified file* -- test/someFeature/expectedOutput/patch0/bundle.js - *bundle after applying patch* -- test/someFeature/expectedOutput/patch0/output.txt - *output after applying patch* - -### Execution Test Pack - -This test pack is made up of a number of mini-typescript projects which include a test suite. As part of the test run, each project is compiled and the test suite run using Karma. So this test pack is different from the comparison test pack in that it **executes the compiled code**. This test pack is useful for testing expected behaviour. (It's also reassuring to see your -code being executed.) - -These tests are executed more widely that the comparison tests; we aim to run these against each version of TypeScript defined in our CI build matrices. (Take a look at [`appveyor.yml`](appveyor.yml) and [`.travis.yml`](.travis.yml) for details.) - -#### Structure - -The execution test pack can be found under `/test/execution-tests`. Like the comparison test pack, the execution test pack uses certain conventions. All tests have their own directory under `/test/execution-tests`, eg `/test/execution-tests/someFeature`. Each test is expected to have a `karma.conf.js` file and a `webpack.config.js` file. - -If a test requires a minimum version of TypeScript then the test directory should be prefixed with the minimum TypeScript version. For example, the `2.0.3_es2016` test requires a minimum TypeScript version of 2.0.3; if the installed version is lower than the test needs then the test will be skipped. - -**IMPORTANT** - -In order that the local version of ts-loader is resolved for tests a `webpack.config.js` file will need to include this line: - -``` -// for test harness purposes only, you would not need this in a normal project -module.exports.resolveLoader = { alias: { 'ts-loader': path.join(__dirname, "../../../index.js") } } -// note that there are 3 ../ here as compared with only 2 for the comparison tests -``` - -And likewise the `karma.conf.js` will need to reuse this like so: - -``` - webpack: { - devtool: 'inline-source-map', - debug: true, - module: { - loaders: webpackConfig.module.loaders - }, - resolve: webpackConfig.resolve, - - // for test harness purposes only, you would not need this in a normal project - resolveLoader: webpackConfig.resolveLoader - }, -``` - -Without this, the test won't be able to resolve ts-loader and webpack won't find your TypeScript tests. - -#### What sort of tests can be included? - -It's pretty much your choice what goes in testwise. At present there are only Jasmine tests in place; it should be possible to put any test in place that Karma is compatible with. The test pack also expects a `typings.json` file and calls `typings install` in each. **Be warned, type definitions are not installed until the test framework has been run.** So if you're wanting to refactor a test you'll need to `typings install` if the requisite typings have not yet been installed. It's possible / probably that this may changed in the future; -particularly to cater for situations where types should be acquired via npm etc. +To read about the comparison test pack take a look [here](test/comparison-tests/README.md) +To read about the execution test pack take a look [here](test/execution-tests/README.md) -#### Running / debugging the tests +## Publishing -To run all the tests use: +So the time has come to publish the latest version of ts-loader to npm. Exciting! -`npm run execution-tests`. +Before you can actually publish make sure the following statements are true: -If you would like to run just a single test then: +- Tests should be green +- The version number in [package.json](package.json) has been incremented. +- The [changelog](CHANGELOG.md) has been updated with details of the changes in this release. Where possible include the details of the issues affected and the PRs raised. -`npm run execution-tests -- --single-test nameOfTest` +OK - you're actually ready. We're going to publish. Here we need tread carefully. Follow these steps: -It's pretty handy to be able to debug tests; for that reason you can run a single test in watch mode like this: +- clone ts-loader from the main repo with this command: `https://github.com/TypeStrong/ts-loader.git` +- [Login to npm](https://docs.npmjs.com/cli/adduser) if you need to: `npm login` +- install ts-loaders packages with `npm install` +- build ts-loader with `npm run build` +- run the tests to ensure all is still good: `npm test` -`npm run execution-tests -- --single-test nameOfTest --watch` +If all the test passed then we're going to ship: +- tag the release in git. You can see existing tags with the command `git tag`. If the version in your `package.json` is `"1.0.1"` then you would tag the release like so: `git tag v1.0.1`. For more on type of tags we're using read [here](https://git-scm.com/book/en/v2/Git-Basics-Tagging#Lightweight-Tags). +- Push the tag so the new version will show up in the [releases](https://github.com/TypeStrong/ts-loader/releases): `git push origin --tags` +- On the releases page, click the "Draft a new release button" and, on the presented page, select the version you've just released, name it and copy in the new markdown that you added to the [changelog](CHANGELOG.md). +- Now the big moment: `npm publish` -Then you can fire up http://localhost:9876/ and the world's your oyster. +You've released! Pat yourself on the back. \ No newline at end of file diff --git a/src/interfaces.ts b/src/interfaces.ts index e38cdf1f4..1680118c9 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -123,11 +123,11 @@ export interface TSInstance { compilerOptions: typescript.CompilerOptions; loaderOptions: LoaderOptions; files: TSFiles; + modifiedFiles?: TSFiles; languageService?: typescript.LanguageService; version?: number; dependencyGraph: DependencyGraph; reverseDependencyGraph: ReverseDependencyGraph; - modifiedFiles?: TSFiles; filesWithErrors?: TSFiles; } diff --git a/test/comparison-tests/README.md b/test/comparison-tests/README.md new file mode 100644 index 000000000..1b9fddcc2 --- /dev/null +++ b/test/comparison-tests/README.md @@ -0,0 +1,73 @@ +# Comparison Test Pack + +This test pack comprises a number of mini-typescript projects which, as part of the test run, are each run through webpack. +The outputs (both compiled JavaScript and webpack compilation output) are compared against a set of expected +outputs. These are particularly useful for testing failure cases; that is testing scenarios where you expect compilation +to fail and ensuring the failure is what you expect. For example, ensuring the presence of error messages from the TypeScript +compiler in the output etc. + +The comparison test pack can be found under `/test/comparison-tests`. The test harness uses certain conventions. All tests have their own directory under `/test/comparison-tests`, eg `/test/comparison-tests/someFeature`. Each test should have a `webpack.config.js` file which follows this general convention: + +```javascript +module.exports = { + entry: './app.ts', + output: { + filename: 'bundle.js' + }, + resolve: { + extensions: ['', '.ts', 'tsx', '.js'] + }, + module: { + loaders: [ + { test: /\.tsx?$/, loader: 'ts-loader' } + ] + } +} + +// for test harness purposes only, you would not need this in a normal project +module.exports.resolveLoader = { alias: { 'ts-loader': require('path').join(__dirname, "../../index.js") } } +``` + +You can run all the tests in the Comparison Test Pack with `npm run comparison-tests`. You can also go into an individual test directory and manually build a project using `webpack` or `webpack --watch`. This can be useful both when developing the test and also when fixing an issue or adding a feature. + +Each test should have an `expectedOutput` directory which contains any webpack filesystem output (typically `bundle.js` and possibly `bundle.js.map`) and any console output. stdout should go in `output.txt` and stderr should go in `err.txt`. + +To run all the tests use: + +`npm run comparison-tests`. + +If you would like to run just a single test then: + +`npm run comparison-tests -- --single-test nameOfTest` + +## Regenerating test data + +As a convenience it is possible to regenerate the expected output from the actual output. This is useful when creating new tests and also when making a change that affects multiple existing tests. To run use: + +`npm run comparison-tests -- --save-output`. + +Note that all tests will automatically pass when using this feature. You should double check the generated files to make sure +the output is indeed correct. + +If you would like to regenerate a single test then combine `--save-output` with +`--single-test` like so: + +`npm run comparison-tests -- --save-output --single-test nameOfTest` + +## Watch Specific Tests + +The test harness additionally supports tests which exercise watch mode, since that is such an integral part of webpack. Watch mode tests are just the as standard comparison tests. However, after the initial compilation and comparison, a series of "patches" are applied and tested. + +The patches live in folders following the naming convention of `/patchN` starting with 0. After the initial compilation and comparison, the patches are iterated through and the files in place are replaced with any modified files in the `/patchN` directory. After each patch application the compilation / comparison is performed once more. + +For example: + +Initial state: +- test/someFeature/app.ts +- test/someFeature/expectedOutput/bundle.js +- test/someFeature/expectedOutput/output.txt + +patch0 is applied: +- test/someFeature/patch0/app.ts - *modified file* +- test/someFeature/expectedOutput/patch0/bundle.js - *bundle after applying patch* +- test/someFeature/expectedOutput/patch0/output.txt - *output after applying patch* diff --git a/test/execution-tests/README.md b/test/execution-tests/README.md new file mode 100644 index 000000000..1b0399033 --- /dev/null +++ b/test/execution-tests/README.md @@ -0,0 +1,61 @@ +# Execution Test Pack + +This test pack is made up of a number of mini-typescript projects which include a test suite. As part of the test run, each project is compiled and the test suite run using Karma. So this test pack is different from the comparison test pack in that it **executes the compiled code**. This test pack is useful for testing expected behaviour. (It's also reassuring to see your +code being executed.) + +These tests are executed more widely that the comparison tests; we aim to run these against each version of TypeScript defined in our CI build matrices. (Take a look at [`appveyor.yml`](appveyor.yml) and [`.travis.yml`](.travis.yml) for details.) + +## Structure + +The execution test pack can be found under `/test/execution-tests`. Like the comparison test pack, the execution test pack uses certain conventions. All tests have their own directory under `/test/execution-tests`, eg `/test/execution-tests/someFeature`. Each test is expected to have a `karma.conf.js` file and a `webpack.config.js` file. + +If a test requires a minimum version of TypeScript then the test directory should be prefixed with the minimum TypeScript version. For example, the `2.0.3_es2016` test requires a minimum TypeScript version of 2.0.3; if the installed version is lower than the test needs then the test will be skipped. + +**IMPORTANT** + +In order that the local version of ts-loader is resolved for tests a `webpack.config.js` file will need to include this line: + +``` +// for test harness purposes only, you would not need this in a normal project +module.exports.resolveLoader = { alias: { 'ts-loader': path.join(__dirname, "../../../index.js") } } +// note that there are 3 ../ here as compared with only 2 for the comparison tests +``` + +And likewise the `karma.conf.js` will need to reuse this like so: + +``` + webpack: { + devtool: 'inline-source-map', + debug: true, + module: { + loaders: webpackConfig.module.loaders + }, + resolve: webpackConfig.resolve, + + // for test harness purposes only, you would not need this in a normal project + resolveLoader: webpackConfig.resolveLoader + }, +``` + +Without this, the test won't be able to resolve ts-loader and webpack won't find your TypeScript tests. + +## What sort of tests can be included? + +It's pretty much your choice what goes in testwise. At present there are only Jasmine tests in place; it should be possible to put any test in place that Karma is compatible with. The test pack also expects a `typings.json` file and calls `typings install` in each. **Be warned, type definitions are not installed until the test framework has been run.** So if you're wanting to refactor a test you'll need to `typings install` if the requisite typings have not yet been installed. It's possible / probably that this may changed in the future; +particularly to cater for situations where types should be acquired via npm etc. + +## Running / debugging the tests + +To run all the tests use: + +`npm run execution-tests`. + +If you would like to run just a single test then: + +`npm run execution-tests -- --single-test nameOfTest` + +It's pretty handy to be able to debug tests; for that reason you can run a single test in watch mode like this: + +`npm run execution-tests -- --single-test nameOfTest --watch` + +Then you can fire up http://localhost:9876/ and the world's your oyster. From 036cd69363323f8c477941ae02582d73fce38b0b Mon Sep 17 00:00:00 2001 From: John Reilly Date: Sun, 30 Oct 2016 06:50:02 +0000 Subject: [PATCH 19/23] warning about casing --- test/comparison-tests/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/comparison-tests/README.md b/test/comparison-tests/README.md index 1b9fddcc2..f533c1604 100644 --- a/test/comparison-tests/README.md +++ b/test/comparison-tests/README.md @@ -54,6 +54,8 @@ If you would like to regenerate a single test then combine `--save-output` with `npm run comparison-tests -- --save-output --single-test nameOfTest` +**When doing this, do make sure you get the casing of the name of the test right. If you get it wrong you'll spend a long time wondering why tests are failing...** + ## Watch Specific Tests The test harness additionally supports tests which exercise watch mode, since that is such an integral part of webpack. Watch mode tests are just the as standard comparison tests. However, after the initial compilation and comparison, a series of "patches" are applied and tested. From 6965d8afe1272d7013198e1ed4465b4f399c9562 Mon Sep 17 00:00:00 2001 From: John Reilly Date: Sun, 30 Oct 2016 09:39:38 +0000 Subject: [PATCH 20/23] update changelog / npmignore and upgrade guide --- .npmignore | 1 + CHANGELOG.md | 17 +++++++++-------- UPGRADE.md | 7 +++++++ 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/.npmignore b/.npmignore index 73a9d504a..9931d45d5 100644 --- a/.npmignore +++ b/.npmignore @@ -1,4 +1,5 @@ *.ts +.vscode .test test src diff --git a/CHANGELOG.md b/CHANGELOG.md index b25bc589a..08fd9428e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ # Changelog -## v0.9.x - NOT RELEASED YET +## v1.0.0 +- [General refactor of ts-loader; some performance improvements](https://github.com/TypeStrong/ts-loader/pull/343) [#335] - thanks @johnnyreilly - [Make the loader resilient to watched declaration files being removed.](https://github.com/TypeStrong/ts-loader/pull/281) - thanks @opichals ## v0.9.5 @@ -15,22 +16,22 @@ ## v0.9.3 -- [Added support for allowJs](https://github.com/TypeStrong/ts-loader/pull/320) (#316) +- [Added support for allowJs](https://github.com/TypeStrong/ts-loader/pull/320) (#316) - thanks @dschnare ## v0.9.2 -- [Added support for @types](https://github.com/TypeStrong/ts-loader/pull/318) (#247) +- [Added support for @types](https://github.com/TypeStrong/ts-loader/pull/318) (#247) -thanks @basarat for the ideas ## v0.9.1 -- [Normalize dependency graph paths - Fix broken dependencies on Windows ](https://github.com/TypeStrong/ts-loader/pull/286) -- [Fixed the declaration issue](https://github.com/TypeStrong/ts-loader/pull/307) (#214 part deux) +- [Normalize dependency graph paths - Fix broken dependencies on Windows ](https://github.com/TypeStrong/ts-loader/pull/286) - thanks @pzavolinsky +- [Fixed the declaration issue](https://github.com/TypeStrong/ts-loader/pull/307) (#214 part deux) - thanks @dizel3d ## v0.9.0 -- [Made ts-loader compatible with node v6](https://github.com/TypeStrong/ts-loader/commit/a4f835345e495f45b40365f025afce72d1817996) -- [Fixed the declaration issue](https://github.com/TypeStrong/ts-loader/commit/3bb0fec73a2fab47953b51d256f0f5378f236ad1) (#214) -- [Declarations update independent of compiler.watchFileSystem](https://github.com/TypeStrong/ts-loader/pull/167/commits/ae824b2676b226bdd0c860a787754a4ae28e339c) (#155) +- [Made ts-loader compatible with node v6](https://github.com/TypeStrong/ts-loader/commit/a4f835345e495f45b40365f025afce72d1817996) - thanks @Blechhirn +- [Fixed the declaration issue](https://github.com/TypeStrong/ts-loader/commit/3bb0fec73a2fab47953b51d256f0f5378f236ad1) (#214) - thanks @17cupsofcoffee +- [Declarations update independent of compiler.watchFileSystem](https://github.com/TypeStrong/ts-loader/pull/167/commits/ae824b2676b226bdd0c860a787754a4ae28e339c) (#155) - thanks @opichals Now built using TypeScript v2.0 diff --git a/UPGRADE.md b/UPGRADE.md index 2ab470647..5483e21e3 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,5 +1,12 @@ # Upgrade Guide +## v0.9.x to 1.0.x + +We no longer support Node 0.12 officially since it is being end-of-lifed. +That said, ts-loader will probably still work with it at present. +(Though you shouldn't depend on it and ought to upgrade your version of node). +Otherwise there are no breaking changes known of; if you find any then let us know! + ## v0.8.x to 0.9.x No breaking changes known of; if there are then let us know! From 29258189ee2aa38e59050b84c2b6aa6072c93af7 Mon Sep 17 00:00:00 2001 From: John Reilly Date: Mon, 31 Oct 2016 05:49:46 +0000 Subject: [PATCH 21/23] made a few lets consts --- src/after-compile.ts | 6 +++--- src/servicesHost.ts | 2 +- src/tsconfig.json | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/after-compile.ts b/src/after-compile.ts index 7452a7675..cd472c4b8 100644 --- a/src/after-compile.ts +++ b/src/after-compile.ts @@ -37,9 +37,9 @@ function makeAfterCompile( const modules: { [modulePath: string]: interfaces.WebpackModule[] } = {}; compilation.modules.forEach(module => { if (module.resource) { - let modulePath = path.normalize(module.resource); + const modulePath = path.normalize(module.resource); if (utils.hasOwnProperty(modules, modulePath)) { - let existingModules = modules[modulePath]; + const existingModules = modules[modulePath]; if (existingModules.indexOf(module) === -1) { existingModules.push(module); } @@ -76,7 +76,7 @@ function makeAfterCompile( Object.keys(filesToCheckForErrors) .filter(filePath => !!filePath.match(/(\.d)?\.ts(x?)$/)) .forEach(filePath => { - let errors = languageService.getSyntacticDiagnostics(filePath).concat(languageService.getSemanticDiagnostics(filePath)); + const errors = languageService.getSyntacticDiagnostics(filePath).concat(languageService.getSemanticDiagnostics(filePath)); if (errors.length > 0) { if (null === filesWithErrors) { filesWithErrors = {}; diff --git a/src/servicesHost.ts b/src/servicesHost.ts index 8ca49a448..f2f9a8db9 100644 --- a/src/servicesHost.ts +++ b/src/servicesHost.ts @@ -85,7 +85,7 @@ function makeServicesHost( } } catch (e) { resolvedFileName = null; } - let tsResolution = compiler.resolveModuleName(moduleName, containingFile, compilerOptions, moduleResolutionHost); + const tsResolution = compiler.resolveModuleName(moduleName, containingFile, compilerOptions, moduleResolutionHost); if (tsResolution.resolvedModule) { if (resolvedFileName) { diff --git a/src/tsconfig.json b/src/tsconfig.json index 7fbbcb5e7..6c8a662b7 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -6,6 +6,7 @@ "noUnusedLocals": true, "noUnusedParameters": false, "suppressImplicitAnyIndexErrors": true, + "strictNullChecks": false, "module": "commonjs", "moduleResolution": "node", "outDir": "../dist" From 6aa608131e4b66ba0d3e83f0294e999a98b2c41c Mon Sep 17 00:00:00 2001 From: John Reilly Date: Mon, 31 Oct 2016 21:56:07 +0000 Subject: [PATCH 22/23] improve interfaces optimise formatErrors Fix test data for errors test --- src/interfaces.ts | 12 +++++------- src/utils.ts | 12 +++++++----- .../errors/expectedOutput-2.0/output.transpiled.txt | 2 +- .../errors/expectedOutput-2.0/output.txt | 2 +- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/interfaces.ts b/src/interfaces.ts index 1680118c9..b725a8cd7 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -8,13 +8,7 @@ export interface SourceMap { export interface Webpack { _compiler: Compiler; - _module: { - meta: { - tsLoaderFileVersion: number; - tsLoaderDefinitionFileVersions: string[]; - }, - errors: WebpackError[]; - }; + _module: WebpackModule; cacheable: () => void; query: string; async: () => (err: Error | WebpackError, source?: string, map?: string) => void; @@ -69,6 +63,10 @@ export interface WebpackCompiler { export interface WebpackModule { resource: string; errors: WebpackError[]; + meta: { + tsLoaderFileVersion: number; + tsLoaderDefinitionFileVersions: string[]; + }; } export interface WebpackNodeWatchFileSystem { diff --git a/src/utils.ts b/src/utils.ts index 85e1fc95e..3861b64b2 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -21,7 +21,8 @@ export function formatErrors( diagnostics: typescript.Diagnostic[], loaderOptions: interfaces.LoaderOptions, compiler: typeof typescript, - merge?: any): interfaces.WebpackError[] { + merge?: { file?: string; module?: interfaces.WebpackModule } +): interfaces.WebpackError[] { return diagnostics .filter(diagnostic => loaderOptions.ignoreDiagnostics.indexOf(diagnostic.code) === -1) @@ -30,18 +31,19 @@ export function formatErrors( const errorCategoryAndCode = errorCategory + ' TS' + diagnostic.code + ': '; const messageText = errorCategoryAndCode + compiler.flattenDiagnosticMessageText(diagnostic.messageText, constants.EOL); + let error: interfaces.WebpackError; if (diagnostic.file) { const lineChar = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); - return makeError({ + error = makeError({ message: `${'('.white}${(lineChar.line + 1).toString().cyan},${(lineChar.character + 1).toString().cyan}): ${messageText.red}`, rawMessage: messageText, location: { line: lineChar.line + 1, character: lineChar.character + 1 } }); } else { - return makeError({ rawMessage: messageText }); + error = makeError({ rawMessage: messageText }); } - }) - .map(error => objectAssign(error, merge)); + return objectAssign(error, merge); + }); } export function readFile(fileName: string) { diff --git a/test/comparison-tests/errors/expectedOutput-2.0/output.transpiled.txt b/test/comparison-tests/errors/expectedOutput-2.0/output.transpiled.txt index 71a597a9f..0657a1130 100644 --- a/test/comparison-tests/errors/expectedOutput-2.0/output.transpiled.txt +++ b/test/comparison-tests/errors/expectedOutput-2.0/output.transpiled.txt @@ -26,7 +26,7 @@ SyntaxError: Unexpected token (2:9) at nextLoader (node_modules\webpack-core\lib\NormalModuleMixin.js:275:25) at node_modules\webpack-core\lib\NormalModuleMixin.js:292:15 at context.callback (node_modules\webpack-core\lib\NormalModuleMixin.js:148:14) - at Object.loader (index.js:597:5) + at Object.loader (dist\index.js:33:5) at WEBPACK_CORE_LOADER_EXECUTION (node_modules\webpack-core\lib\NormalModuleMixin.js:155:71) at runSyncOrAsync (node_modules\webpack-core\lib\NormalModuleMixin.js:155:93) at nextLoader (node_modules\webpack-core\lib\NormalModuleMixin.js:290:3) diff --git a/test/comparison-tests/errors/expectedOutput-2.0/output.txt b/test/comparison-tests/errors/expectedOutput-2.0/output.txt index b1eee14ad..d18c4df15 100644 --- a/test/comparison-tests/errors/expectedOutput-2.0/output.txt +++ b/test/comparison-tests/errors/expectedOutput-2.0/output.txt @@ -26,7 +26,7 @@ SyntaxError: Unexpected token (1:9) at nextLoader (node_modules\webpack-core\lib\NormalModuleMixin.js:275:25) at node_modules\webpack-core\lib\NormalModuleMixin.js:292:15 at context.callback (node_modules\webpack-core\lib\NormalModuleMixin.js:148:14) - at Object.loader (index.js:597:5) + at Object.loader (dist\index.js:33:5) at WEBPACK_CORE_LOADER_EXECUTION (node_modules\webpack-core\lib\NormalModuleMixin.js:155:71) at runSyncOrAsync (node_modules\webpack-core\lib\NormalModuleMixin.js:155:93) at nextLoader (node_modules\webpack-core\lib\NormalModuleMixin.js:290:3) From 7926b570d1a6e7ad14384d21030f829da9522c8e Mon Sep 17 00:00:00 2001 From: John Reilly Date: Tue, 1 Nov 2016 05:51:23 +0000 Subject: [PATCH 23/23] fix links --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7c42dbe55..20ea1ae71 100644 --- a/README.md +++ b/README.md @@ -200,7 +200,7 @@ require('!style!css!./style.css'); The same basic process is required for code splitting. In this case, you `import` modules you need but you don't directly use them. Instead you require them at [split points](http://webpack.github.io/docs/code-splitting.html#defining-a-split-point). -See [this example](test/codeSplitting) for more details. +See [this example](test/comparison-tests/codeSplitting) and [this example](test/comparison-tests/es6codeSplitting) for more details. ## License