diff --git a/.gitignore b/.gitignore index a213b918c..65b17e037 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,15 @@ /*.js +!index.js +/src/*.js /*.d.ts /*.log *.js.map bundle.js npm-debug.log -.test/ +/.test/ +/.vscode/ /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..9931d45d5 100644 --- a/.npmignore +++ b/.npmignore @@ -1,4 +1,5 @@ *.ts +.vscode +.test test -.* -typings \ No newline at end of file +src 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/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/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/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 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! 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..6ed63cd75 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,13 @@ { "name": "ts-loader", - "version": "0.9.5", + "version": "1.0.0-beta.1", "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/resolver.ts b/resolver.ts deleted file mode 100644 index 1f66ec317..000000000 --- a/resolver.ts +++ /dev/null @@ -1,59 +0,0 @@ -// 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. - -/// - -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"); - -function makeRootPlugin(name, root) { - 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) { - 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; -} - -export = makeResolver; \ No newline at end of file diff --git a/src/after-compile.ts b/src/after-compile.ts new file mode 100644 index 000000000..cd472c4b8 --- /dev/null +++ b/src/after-compile.ts @@ -0,0 +1,162 @@ +import interfaces = require('./interfaces'); +import path = require('path'); +import utils = require('./utils'); + +function makeAfterCompile( + instance: interfaces.TSInstance, + configFilePath: string +) { + const { compiler, languageService } = instance; + + 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.registerWebpackErrors( + compilation.errors, + utils.formatErrors(languageService.getCompilerOptionsDiagnostics(), + instance.loaderOptions, + compiler, + { 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 + const modules: { [modulePath: string]: interfaces.WebpackModule[] } = {}; + compilation.modules.forEach(module => { + if (module.resource) { + const modulePath = path.normalize(module.resource); + if (utils.hasOwnProperty(modules, modulePath)) { + const 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 => { + const 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)) { + const associatedModules = modules[filePath]; + + associatedModules.forEach(module => { + // remove any existing errors + removeTSLoaderErrors(module.errors); + + // append errors + const 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.loaderOptions, compiler, { file: filePath })); + } + }); + + // gather all declaration files from TypeScript and output them to webpack + Object.keys(filesToCheckForErrors) + .filter(filePath => !!filePath.match(/\.ts(x?)$/)) + .forEach(filePath => { + const output = languageService.getEmitOutput(filePath); + const declarationFile = output.outputFiles.filter(fp => !!fp.name.match(/\.d.ts$/)).pop(); + if (declarationFile) { + const 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; + let 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; diff --git a/src/compilerSetup.ts b/src/compilerSetup.ts new file mode 100644 index 000000000..a61197eb8 --- /dev/null +++ b/src/compilerSetup.ts @@ -0,0 +1,62 @@ +import typescript = require('typescript'); +import objectAssign = require('object-assign'); +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 }; +} + +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/config.ts b/src/config.ts new file mode 100644 index 000000000..90a6e37d7 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,113 @@ +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; +} + +export function getConfigFile( + compiler: typeof typescript, + loader: interfaces.Webpack, + loaderOptions: interfaces.LoaderOptions, + compilerCompatible: boolean, + log: logger.Logger, + compilerDetailsLogMessage: string +) { + 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], loaderOptions, compiler, { 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 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; +} 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..a43f8c03f --- /dev/null +++ b/src/index.ts @@ -0,0 +1,172 @@ +import path = require('path'); +import loaderUtils = require('loader-utils'); +import objectAssign = require('object-assign'); +import arrify = require('arrify'); +require('colors'); + +import instances = require('./instances'); +import interfaces = require('./interfaces'); +import utils = require('./utils'); + +let webpackInstances: any = []; +const definitionFileRegex = /\.d\.ts$/; + +function loader(this: interfaces.Webpack, contents: string) { + this.cacheable && this.cacheable(); + const callback = this.async(); + const filePath = path.normalize(this.resourcePath); + + const options = makeOptions(this); + + const { instance, error } = instances.ensureTypeScriptInstance(options, this); + + if (error) { + callback(error); + return; + } + + const file = updateFileInCache(filePath, contents, instance); + + 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}`); + } + + 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, output, sourceMap); +} + +function makeOptions(loader: interfaces.Webpack) { + 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) { + // 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; +} + +function getEmit( + filePath: string, + instance: interfaces.TSInstance, + loader: interfaces.Webpack +) { + // 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: interfaces.Webpack +) { + 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: interfaces.Webpack +) { + 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/instances.ts b/src/instances.ts new file mode 100644 index 000000000..b1e0b799d --- /dev/null +++ b/src/instances.ts @@ -0,0 +1,115 @@ +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: interfaces.Webpack +): { 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 { + configFilePath, + configFile, + configFileError + } = config.getConfigFile(compiler, loader, loaderOptions, compilerCompatible, log, compilerDetailsLogMessage); + + 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, loaderOptions, compiler, { file: configFilePath })); + + return { error: utils.makeError({ rawMessage: 'error while parsing tsconfig.json', file: configFilePath }) }; + } + + const compilerOptions = compilerSetup.getCompilerOptions(compilerCompatible, compiler, configParseResult); + const files: interfaces.TSFiles = {}; + + 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, loaderOptions, compiler, {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 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, configFilePath)); + loader._compiler.plugin("watch-run", watchRun(instance)); + + return { instance }; +} diff --git a/src/interfaces.ts b/src/interfaces.ts new file mode 100644 index 000000000..b725a8cd7 --- /dev/null +++ b/src/interfaces.ts @@ -0,0 +1,188 @@ +import typescript = require('typescript'); + +export interface SourceMap { + sources: any[]; + file: string; + sourcesContent: string[]; +} + +export interface Webpack { + _compiler: Compiler; + _module: WebpackModule; + 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; + 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[]; + meta: { + tsLoaderFileVersion: number; + tsLoaderDefinitionFileVersions: string[]; + }; +} + +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; + modifiedFiles?: TSFiles; + languageService?: typescript.LanguageService; + version?: number; + dependencyGraph: DependencyGraph; + reverseDependencyGraph: ReverseDependencyGraph; + 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..1ddb91c3e --- /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/src/resolver.ts b/src/resolver.ts new file mode 100644 index 000000000..230ddbeb0 --- /dev/null +++ b/src/resolver.ts @@ -0,0 +1,58 @@ +// 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'); + +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() {}; +} + +function makeResolver(options: { resolve: interfaces.Resolve }) { + 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; +} + +export = makeResolver; diff --git a/src/servicesHost.ts b/src/servicesHost.ts new file mode 100644 index 000000000..f2f9a8db9 --- /dev/null +++ b/src/servicesHost.ts @@ -0,0 +1,119 @@ +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( + scriptRegex: RegExp, + log: logger.Logger, + loader: interfaces.Webpack, + instance: interfaces.TSInstance +) { + const { compiler, compilerOptions, files } = instance; + + 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 undefined; } + + 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; } + + const 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; diff --git a/src/tsconfig.json b/src/tsconfig.json new file mode 100644 index 000000000..6c8a662b7 --- /dev/null +++ b/src/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": false, + "noUnusedLocals": true, + "noUnusedParameters": false, + "suppressImplicitAnyIndexErrors": true, + "strictNullChecks": false, + "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..3b364fe66 --- /dev/null +++ b/src/tslint.json @@ -0,0 +1,28 @@ +{ + "extends": "tslint:latest", + "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", + "variables-before-functions" + ], + "no-unused-variable": [true, "check-parameters"], + "no-var-requires": false, + "quotemark": [ + "single" + ], + "trailing-comma": [false], + "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..3861b64b2 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,73 @@ +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 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) { + 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[], + loaderOptions: interfaces.LoaderOptions, + compiler: typeof typescript, + merge?: { file?: string; module?: interfaces.WebpackModule } +): interfaces.WebpackError[] { + + return diagnostics + .filter(diagnostic => loaderOptions.ignoreDiagnostics.indexOf(diagnostic.code) === -1) + .map(diagnostic => { + const errorCategory = compiler.DiagnosticCategory[diagnostic.category].toLowerCase(); + 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); + 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 { + error = makeError({ rawMessage: messageText }); + } + return objectAssign(error, merge); + }); +} + +export function readFile(fileName: string) { + fileName = path.normalize(fileName); + try { + return fs.readFileSync(fileName, { encoding: 'utf8' }); + } catch (e) { + return undefined; + } +} + +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 }); +} diff --git a/src/watch-run.ts b/src/watch-run.ts new file mode 100644 index 000000000..8113b685c --- /dev/null +++ b/src/watch-run.ts @@ -0,0 +1,33 @@ +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; diff --git a/test/comparison-tests/README.md b/test/comparison-tests/README.md new file mode 100644 index 000000000..f533c1604 --- /dev/null +++ b/test/comparison-tests/README.md @@ -0,0 +1,75 @@ +# 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` + +**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. + +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/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/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) 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/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. 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" - ] -}