diff --git a/packages/stryker-webpack/.gitignore b/packages/stryker-webpack/.gitignore index 20dd5ff0bf..1e4732ddc3 100644 --- a/packages/stryker-webpack/.gitignore +++ b/packages/stryker-webpack/.gitignore @@ -2,4 +2,10 @@ /node_modules **/*.js **/*.js.map -**/*.d.ts \ No newline at end of file +**/*.d.ts + +!testResources/**/*.js +!stryker.conf.js +/package-lock.json +/reports +/.nyc_output \ No newline at end of file diff --git a/packages/stryker-webpack/.idea/vcs.xml b/packages/stryker-webpack/.idea/vcs.xml deleted file mode 100644 index 35eb1ddfbb..0000000000 --- a/packages/stryker-webpack/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/packages/stryker-webpack/.travis.yml b/packages/stryker-webpack/.travis.yml index 72ed477b34..c8996bd8ab 100644 --- a/packages/stryker-webpack/.travis.yml +++ b/packages/stryker-webpack/.travis.yml @@ -6,4 +6,12 @@ node_js: - '4' install: npm install before_install: -- if [[ `npm -v` = 2* ]]; then npm i -g npm@3; fi \ No newline at end of file +- if [[ `npm -v` = 2* ]]; then npm i -g npm@3; fi +notifications: + slack: + secure: PaygAi3u8LIEIpsMbzr6BKgU8qW4abz9D+I5fBaxtrCycBYcj6qWSz5SY6XMAsf6cHj0U0CLl7e7tw7sR8FJlha61ej9u/p+gnHlPPQDsAfOYNifuTyCoj7s7HjF1bboKGKB66P/Np/QPyFvTHwVdA6l2JPoJO5a6eiqaUUvhWoZKGvs4HGBzGfd3g/6RZ7ugUH1C6DoUnPv1eoFoA22QH8pj30ugq/E0xZnD64nx0vBrFzfbpH9r9wDOphe9lnLygxHTzXY/smJfXqNrpdRyWnztTcI0XwTEe4lZR1l9y5UkziVvqrVoSblUp68JvB9yrfdZzWkLyXAoYaiOPIsszHnT9ovUWFLYqsbBkjqJOYPZX0koCeP2a07i5UMXbjwKcwdg+HKW3ZMq0oRX5n1dMOVBlCqlaZjW9Q0TJQiseLvrOmtpM2X3mKb+2ibyuoObpnUEBNdluQ1xiqBFsrFNpmowPUidt4TDmT4R2ggOM11O5iW5kv4gaE6N4q6hu1+tlQR6eu5LM5d8CNrJ3k362STqw11bOQbXtondVMV2kOo7+VJT8JHDK4V8aX5FRk8yhrC80FxD7Et7eJOSXOnz8HXQo064+0vjJjdzzEjfJKeqwuhLTjZvQnSjBDRa6u59h4X50RqN/MUmzSd18HqKuX2EhVMl+08u5YhRg+mi0U= + on_success: change + on_failure: change + email: + on_success: never + on_failure: change diff --git a/packages/stryker-webpack/.vscode/settings.json b/packages/stryker-webpack/.vscode/settings.json new file mode 100644 index 0000000000..4580195e13 --- /dev/null +++ b/packages/stryker-webpack/.vscode/settings.json @@ -0,0 +1,16 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib", + "files": { + "exclude": { + ".git": "", + ".tscache": "", + "**/*.js": { + "when": "$(basename).ts" + }, + "**/*.d.ts": true, + "**/*.map": { + "when": "$(basename)" + } + } + } + } \ No newline at end of file diff --git a/packages/stryker-webpack/.vscode/tasks.json b/packages/stryker-webpack/.vscode/tasks.json new file mode 100644 index 0000000000..61112faee9 --- /dev/null +++ b/packages/stryker-webpack/.vscode/tasks.json @@ -0,0 +1,25 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "build", + "problemMatcher": [] + }, + { + "type": "npm", + "script": "test", + "problemMatcher": [] + }, + { + "type": "typescript", + "tsconfig": "tsconfig.json", + "option": "watch", + "problemMatcher": [ + "$tsc-watch" + ] + } + ] +} \ No newline at end of file diff --git a/packages/stryker-webpack/package-lock.json b/packages/stryker-webpack/package-lock.json deleted file mode 100644 index 5bda234798..0000000000 --- a/packages/stryker-webpack/package-lock.json +++ /dev/null @@ -1,266 +0,0 @@ -{ - "name": "stryker-webpack", - "version": "0.0.1", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@types/chai": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.0.4.tgz", - "integrity": "sha512-cvU0HomQ7/aGDQJZsbtJXqBQ7w4J4TqLB0Z/h8mKrpRjfeZEvTbygkfJEb7fWdmwpIeDeFmIVwAEqS0OYuUv3Q==", - "dev": true - }, - "@types/mocha": { - "version": "2.2.43", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-2.2.43.tgz", - "integrity": "sha512-xNlAmH+lRJdUMXClMTI9Y0pRqIojdxfm7DHsIxoB2iTzu3fnPmSMEN8SsSx0cdwV36d02PWCWaDUoZPDSln+xw==", - "dev": true - }, - "assertion-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.2.tgz", - "integrity": "sha1-E8pRXYYgbaC6xm6DTdOX2HWBCUw=", - "dev": true - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "brace-expansion": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", - "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", - "dev": true, - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "browser-stdout": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", - "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", - "dev": true - }, - "chai": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz", - "integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=", - "dev": true, - "requires": { - "assertion-error": "1.0.2", - "check-error": "1.0.2", - "deep-eql": "3.0.1", - "get-func-name": "2.0.0", - "pathval": "1.1.0", - "type-detect": "4.0.3" - } - }, - "check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", - "dev": true - }, - "commander": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", - "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", - "dev": true, - "requires": { - "type-detect": "4.0.3" - } - }, - "diff": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.3.1.tgz", - "integrity": "sha512-MKPHZDMB0o6yHyDryUOScqZibp914ksXwAMYMTHj6KO8UeKsRYNJD3oNCKjTqZon+V488P7N/HzXF8t7ZR95ww==", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", - "dev": true - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "dev": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "growl": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz", - "integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==", - "dev": true - }, - "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", - "dev": true - }, - "he": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", - "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "1.1.8" - } - }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "mocha": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-4.0.1.tgz", - "integrity": "sha512-evDmhkoA+cBNiQQQdSKZa2b9+W2mpLoj50367lhy+Klnx9OV8XlCIhigUnn1gaTFLQCa0kdNhEGDr0hCXOQFDw==", - "dev": true, - "requires": { - "browser-stdout": "1.3.0", - "commander": "2.11.0", - "debug": "3.1.0", - "diff": "3.3.1", - "escape-string-regexp": "1.0.5", - "glob": "7.1.2", - "growl": "1.10.3", - "he": "1.1.1", - "mkdirp": "0.5.1", - "supports-color": "4.4.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1.0.2" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "pathval": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", - "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", - "dev": true - }, - "supports-color": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz", - "integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==", - "dev": true, - "requires": { - "has-flag": "2.0.0" - } - }, - "type-detect": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.3.tgz", - "integrity": "sha1-Dj8mcLRAmbC0bChNE2p+9Jx0wuo=", - "dev": true - }, - "typescript": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.5.3.tgz", - "integrity": "sha512-ptLSQs2S4QuS6/OD1eAKG+S5G8QQtrU5RT32JULdZQtM1L3WTi34Wsu48Yndzi8xsObRAB9RPt/KhA9wlpEF6w==", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - } - } -} diff --git a/packages/stryker-webpack/package.json b/packages/stryker-webpack/package.json index 0ca3d0f13e..31d11078cd 100644 --- a/packages/stryker-webpack/package.json +++ b/packages/stryker-webpack/package.json @@ -6,8 +6,10 @@ "main": "src/index.js", "scripts": { "build": "tsc", - "pretest": "tsc", - "test": "mocha --recursive test/**/*.spec.js" + "premocha": "npm run build", + "test": "npm run mocha && npm run stryker", + "mocha": "nyc --reporter=html --report-dir=reports/coverage --check-coverage --lines 85 --functions 90 --branches 65 mocha \"test/unit/**/*.js\" \"test/integration/**/*.js\"", + "stryker": "stryker run" }, "repository": { "type": "git", @@ -35,12 +37,31 @@ "homepage": "https://github.com/Archcry/stryker-webpack#readme", "devDependencies": { "@types/chai": "^4.0.4", + "@types/memory-fs": "^0.3.0", "@types/mocha": "^2.2.43", + "@types/mz": "0.0.32", + "@types/sinon": "^2.3.6", + "@types/webpack": "^3.0.13", "chai": "^4.1.2", - "mocha": "^4.0.1", - "typescript": "^2.2.2" + "mocha": "^3.5.3", + "mz": "^2.7.0", + "nyc": "^11.2.1", + "sinon": "^4.0.1", + "stryker": "^0.13.0", + "stryker-cli": "^0.1.3", + "stryker-html-reporter": "^0.11.0", + "stryker-mocha-framework": "^0.6.1", + "stryker-mocha-runner": "^0.9.1", + "stryker-typescript": "^0.3.0", + "typescript": "^2.5.0" }, "peerDependencies": { + "webpack": "^3.7.1", + "stryker-api": "^0.11.0" + }, + "dependencies": { + "memory-fs": "^0.4.1", + "stryker-api": "^0.11.0", "webpack": "^3.7.1" } } diff --git a/packages/stryker-webpack/src/.gitkeep b/packages/stryker-webpack/src/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/stryker-webpack/src/WebpackTranspiler.ts b/packages/stryker-webpack/src/WebpackTranspiler.ts new file mode 100644 index 0000000000..7f9233e1f0 --- /dev/null +++ b/packages/stryker-webpack/src/WebpackTranspiler.ts @@ -0,0 +1,71 @@ +import {TranspilerOptions, Transpiler, TranspileResult, FileLocation} from "stryker-api/transpile"; +import {File, TextFile} from "stryker-api/core"; +import {Config} from "stryker-api/config" +import WebpackCompiler from "./compiler/WebpackCompiler"; +import { FileSystem } from "./helpers/FsWrapper"; +import * as path from "path"; +import {Configuration} from "webpack"; + +// TODO: Fix types for memory-fs +import MemoryFileSystem = require("memory-fs"); + +class WebpackTranspiler implements Transpiler { + private _config: Config; + private _compiler: WebpackCompiler; + + public constructor(options: TranspilerOptions) { + this._config = options.config; + const fs = new MemoryFileSystem() as FileSystem; + + // Temporarily clone the config object so that it is no longer readonly + // TODO: Create a ConfigEditor plugin to do this in the future. + const webpackConfig = Object.assign({}, this._config.webpackConfig); + + this._compiler = new WebpackCompiler(this.createWebpackConfig(webpackConfig), fs); + } + + private createWebpackConfig(config: Configuration): Configuration { + return config || { + entry: [path.resolve("index.js")], + output: { + path: "/out", + filename: "bundle.js", + } + } + } + + public async transpile(files: Array): Promise { + try { + await this._compiler.replace(files as Array); + + const compileResult = await this._compiler.emit(); + + return this.createSuccessResult(compileResult); + } catch (err) { + return this.createErrorResult(`${err.name}: ${err.message}`); + } + } + + private createErrorResult(error: string): TranspileResult { + return { + error: error, + outputFiles: [] + }; + + } + + private createSuccessResult(outPutFiles: File[]): TranspileResult { + return { + error: null, + outputFiles: outPutFiles + }; + } + + public getMappedLocation(sourceFileLocation: FileLocation): FileLocation { + // Waiting for a decision on how this is going to be implemented in the future + // Return a "Method nog implemented" error for now. + throw new Error("Method not implemented."); + } +} + +export default WebpackTranspiler; \ No newline at end of file diff --git a/packages/stryker-webpack/src/compiler/Webpack.ts b/packages/stryker-webpack/src/compiler/Webpack.ts new file mode 100644 index 0000000000..4b69d94126 --- /dev/null +++ b/packages/stryker-webpack/src/compiler/Webpack.ts @@ -0,0 +1,2 @@ +import * as webpack from "webpack"; +export default webpack; \ No newline at end of file diff --git a/packages/stryker-webpack/src/compiler/WebpackCompiler.ts b/packages/stryker-webpack/src/compiler/WebpackCompiler.ts new file mode 100644 index 0000000000..5298444595 --- /dev/null +++ b/packages/stryker-webpack/src/compiler/WebpackCompiler.ts @@ -0,0 +1,78 @@ +import {FileKind, TextFile} from "stryker-api/core"; +import FsWrapper, {FileSystem} from "../helpers/FsWrapper"; +import {Compiler, Configuration} from "webpack"; +import webpack from "./Webpack"; +import * as path from "path"; + +export default class WebpackCompiler { + private _compiler: Compiler; + private _fsWrapper: FsWrapper; + private _outPath: string; + private _bundleFileName: string; + + public constructor(webpackConfig: Configuration, fs: FileSystem) { + this._fsWrapper = new FsWrapper(fs); + this._compiler = this.createCompiler(webpackConfig, fs); + this._outPath = "/out"; + this._bundleFileName = "bundle.js"; + } + + private createCompiler(webpackConfig: Configuration, fileSystem: FileSystem): Compiler { + // Force cache to on in the webpack configuration to make compilation go faster + webpackConfig.cache = true; + + // Declare as any here to avoid errors when setting filesystem + const compiler: any = webpack(webpackConfig); + + // Setting filesystem to provided fs so compilation can be done in memory + compiler.inputFileSystem = fileSystem; + compiler.outputFileSystem = fileSystem; + compiler.resolvers.normal.fileSystem = fileSystem; + compiler.resolvers.context.fileSystem = fileSystem; + + return compiler as Compiler; + } + + public async replace(files: Array): Promise { + for(let file of files) { + await this.writeToFs(file); + } + } + + private async writeToFs(file: TextFile): Promise { + // Create the directory + await this._fsWrapper.mkdirp(path.dirname(file.name)); + + // Write the file to the filesystem + await this._fsWrapper.writeFile(file.name, file.content); + } + + public async emit(): Promise> { + await this.compile(); + + const compileResult: string = await this._fsWrapper.readFile(path.join(this._outPath, this._bundleFileName)); + + return new Array({ + content: compileResult, + name: this._bundleFileName, + mutated: true, // TODO: change this to the correct value + kind: FileKind.Text, + transpiled: true, + included: true + }); + } + + private compile(): Promise { + return new Promise((resolve, reject) => { + this._compiler.run((err, stats) => { + if(err) { + reject(err); + } else if(stats.hasErrors()) { + reject(Error(stats.toString("errors-only"))); + } else { + resolve(stats); + } + }); + }); + } +} \ No newline at end of file diff --git a/packages/stryker-webpack/src/helpers/FsWrapper.ts b/packages/stryker-webpack/src/helpers/FsWrapper.ts new file mode 100644 index 0000000000..172adcddb7 --- /dev/null +++ b/packages/stryker-webpack/src/helpers/FsWrapper.ts @@ -0,0 +1,73 @@ +import * as path from "path"; +import {Stats} from "fs"; + +class FsWrapper { + static readonly NO_SUCH_DIRECTORY_ENTRY = "ENOENT"; + static readonly FILE_ALREADY_EXISTS = "EEXIST"; + + private _fs: FileSystem; + + public constructor(fs: FileSystem) { + this._fs = fs; + } + + public readFile(path: string): Promise { + return new Promise((resolve, reject) => { + this._fs.readFile(path, "utf8", (err: Error, result: string) => err ? reject(err) : resolve(result)); + }) + } + + public writeFile(path: string, content: string): Promise { + return new Promise((resolve, reject) => { + this._fs.writeFile(path, content, (err: Error) => err ? reject(err) : resolve()); + }); + } + + public async mkdirp(directoryPath: string): Promise { + try { + await this.mkdir(directoryPath); + } catch (error) { + if(error.code === FsWrapper.NO_SUCH_DIRECTORY_ENTRY) { + // Recursively move down the tree until we find a dir that exists + await this.mkdirp(path.dirname(directoryPath)); + + // Bubble back up and create every directory + await this.mkdirp(directoryPath); + } else if(error.code === FsWrapper.FILE_ALREADY_EXISTS) { + const stats = await this.stat(directoryPath); + + // Throw an error when the required directory turns out to be a file + if (!stats.isDirectory()) { + throw error; + } + } else { + // Throw all other errors + throw error; + } + } + } + + private mkdir(path: string): Promise { + return new Promise((resolve, reject) => { + this._fs.mkdir(path, {}, (err: Error) => err ? reject(err) : resolve()); + }); + } + + private stat(path: string): Promise { + return new Promise((resolve, reject) => { + this._fs.stat(path, (err: Error, result: any) => err ? reject(err) : resolve(result)); + }); + } +} + +export interface FileSystem { + mkdir(path: string, optArgs: {}, callback: (err: Error, result: any) => void): void + + stat(path: string, callback: (err: Error, result: Stats) => void): void + + writeFile(path: string, content: string, callback: (err: Error) => void): void + + readFile(path: string, options: string, callback: (err: Error, content: string) => void): void +} + +export default FsWrapper; \ No newline at end of file diff --git a/packages/stryker-webpack/src/index.ts b/packages/stryker-webpack/src/index.ts index e013bdb5f8..a4c8cf2afc 100644 --- a/packages/stryker-webpack/src/index.ts +++ b/packages/stryker-webpack/src/index.ts @@ -1,7 +1,4 @@ -class Main { - public hello() { - return "Hello World!"; - } -} +import { TranspilerFactory } from 'stryker-api/transpile'; +import WebpackTranspiler from "./WebpackTranspiler"; -export default Main; \ No newline at end of file +TranspilerFactory.instance().register('webpack', WebpackTranspiler); \ No newline at end of file diff --git a/packages/stryker-webpack/stryker.conf.js b/packages/stryker-webpack/stryker.conf.js new file mode 100644 index 0000000000..dc703dc626 --- /dev/null +++ b/packages/stryker-webpack/stryker.conf.js @@ -0,0 +1,19 @@ +module.exports = function(config) { + config.set({ + files: [ + '!src/**/*.ts', + '!**/*.d.ts', + '!test/integration/**/*.ts', + { pattern: 'src/**/*.ts', included: false, mutated: true }, + { pattern: 'testResources/**/*.js', included: false, mutated: false, transpiled: false } + ], + testRunner: "mocha", + testFramework: "mocha", + mutator: "typescript", + transpilers: ["typescript"], + reporter: ["clear-text", "progress", "html"], + tsconfigFile: "tsconfig.json", + coverageAnalysis: "off", + logLevel: "info" + }); +}; diff --git a/packages/stryker-webpack/test/helpers/executeJs.ts b/packages/stryker-webpack/test/helpers/executeJs.ts new file mode 100644 index 0000000000..778bc55add --- /dev/null +++ b/packages/stryker-webpack/test/helpers/executeJs.ts @@ -0,0 +1,15 @@ +import {exec} from "child_process"; + +const execute = (code: string): Promise<{stdout: any, stderr: any}> => { + return new Promise((resolve, reject) => { + exec(`node -e '${code}'`, (err, stdout, stderr) => { + if(err) { + reject(err) + } else { + resolve({stdout, stderr}); + } + }); + }); +} + +export default execute; \ No newline at end of file diff --git a/packages/stryker-webpack/test/helpers/mockInterfaces.ts b/packages/stryker-webpack/test/helpers/mockInterfaces.ts new file mode 100644 index 0000000000..12298e9bd6 --- /dev/null +++ b/packages/stryker-webpack/test/helpers/mockInterfaces.ts @@ -0,0 +1,8 @@ +import {Stats} from "webpack"; + +export interface WebpackCompilerMock { + run: (callback: (err: Error, stats: Stats) => void) => void; + inputFileSystem: { fileSystem: any }; + outputFileSystem: { fileSystem: any }; + resolvers: { normal: { fileSystem: any }, context: { fileSystem: any } }; +} \ No newline at end of file diff --git a/packages/stryker-webpack/test/helpers/producers.ts b/packages/stryker-webpack/test/helpers/producers.ts new file mode 100644 index 0000000000..501dd0ceef --- /dev/null +++ b/packages/stryker-webpack/test/helpers/producers.ts @@ -0,0 +1,47 @@ +import {Configuration, Stats} from "webpack"; +import {FileSystem} from "../../src/helpers/FsWrapper" +import {FileKind, TextFile} from "stryker-api/core"; +import {WebpackCompilerMock} from "./mockInterfaces"; + +export function createFakeFileSystem(): FileSystem { + return { + readFile: () => {}, + writeFile: () => {}, + mkdir: () => {}, + stat: () => {} + }; +} + +export function createFakeWebpackConfig(): Configuration { + return { + entry: ["index.js"], + output: { + path: "/out", + filename: "bundle.js", + } + }; +} + +export function createTextFile(name: string): TextFile { + return { + name: name, + content: 'c = a^2 + b^2', + mutated: true, + included: true, + transpiled: true, + kind: FileKind.Text + }; +} + +export function createWebpackMock(): WebpackCompilerMock { + return { + run: (callback: (err: Error, stats: Stats) => void) => {}, + inputFileSystem: { fileSystem: { } }, + outputFileSystem: { fileSystem: { } }, + resolvers: { + normal: { fileSystem: {} }, + context: { fileSystem: {} } + } + } +} + diff --git a/packages/stryker-webpack/test/index.spec.ts b/packages/stryker-webpack/test/index.spec.ts deleted file mode 100644 index b45eb51b21..0000000000 --- a/packages/stryker-webpack/test/index.spec.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { expect } from "chai"; -import Main from "../src/index"; - -describe("index.js", () => { - let main: Main; - - beforeEach(() => { - main = new Main(); - }); - - it("should return \"Hello World\" when the hello function is called", () => { - expect(main.hello()).to.equal("Hello World!"); - }); -}); diff --git a/packages/stryker-webpack/test/integration/ownDogFoodSpec.ts b/packages/stryker-webpack/test/integration/ownDogFoodSpec.ts new file mode 100644 index 0000000000..8c13cb5a61 --- /dev/null +++ b/packages/stryker-webpack/test/integration/ownDogFoodSpec.ts @@ -0,0 +1,102 @@ +import * as fs from "mz/fs"; +import {TextFile, FileKind} from "stryker-api/core"; +import {Config} from "stryker-api/config"; +import {TranspileResult, TranspilerOptions} from "stryker-api/transpile"; +import * as path from "path"; +import {expect} from "chai"; +import WebpackTranspiler from "../../src/WebpackTranspiler"; +import Transpiler from "stryker-api/src/transpile/Transpiler"; +import execute from "../helpers/executeJs"; + +describe("WebpackTranspiler", () => { + const sampleProjectLocation: string = path.resolve(__dirname, "../../testResources/sampleProject"); + + let files: Array = []; + let webPackTranspiler: Transpiler; + + beforeEach(async () => { + webPackTranspiler = new WebpackTranspiler(createStrykerConfig()); + + files = await fetchTextFiles(sampleProjectLocation); + }); + + it("should have an array with files", () => { + expect(files).to.be.an("array").that.is.not.empty; + }); + + it("should return a bundle file when the transpiler is called", async () => { + try { + const result: TranspileResult = await webPackTranspiler.transpile(files); + const outFile: TextFile = result.outputFiles[0] as TextFile; + + outFile.content = outFile.content.replace(/'/g, '"') + + const {stdout} = await execute(outFile.content); + expect(stdout).to.equal('[ 2, 1, 1, 4, 4, 1, 6, 9, 1, 8, 16, 1 ]\n'); + } catch(err) { + throw new Error(err); + } + }); + + it("should return an error when the entry file cannot be resolved", async () => { + files = files.filter((file) => { + return file.name !== path.join(sampleProjectLocation, "index.js"); + }); + + const result: TranspileResult = await webPackTranspiler.transpile(files); + + expect(result.error).to.not.be.null; + expect(result.outputFiles).to.be.an("array").that.is.empty; + }); + + async function fetchTextFiles(dir: string, textFileArray?: Array): Promise> { + const results = await fs.readdir(dir); + textFileArray = textFileArray || []; + + for(let key in results) { + if(results.hasOwnProperty(key)) { + const result = path.join(dir, results[key]); + const stats: fs.Stats = await fs.stat(result); + + if(stats.isDirectory()) { + await fetchTextFiles(result, textFileArray); + } else { + textFileArray.push(await createTextFile(result)); + } + } + } + + return textFileArray; + } + + async function createTextFile(fileName: string): Promise { + return { + name: fileName, + content: await fs.readFile(fileName, "utf8"), + included: true, + mutated: true, + transpiled: true, + kind: FileKind.Text + } + } + + function createStrykerConfig(): TranspilerOptions { + const config: Config = new Config; + + config.set(Object.assign(config, { + webpackConfig : { + entry: [path.resolve(path.resolve(sampleProjectLocation, "index.js"))], + output: { + path: "/out", + filename: "bundle.js", + } + } + })); + + return { + config: config, + keepSourceMaps: false + } + } +}); + diff --git a/packages/stryker-webpack/test/unit/FsWrapperSpec.ts b/packages/stryker-webpack/test/unit/FsWrapperSpec.ts new file mode 100644 index 0000000000..586c5d362f --- /dev/null +++ b/packages/stryker-webpack/test/unit/FsWrapperSpec.ts @@ -0,0 +1,202 @@ +import FsWrapper from "../../src/helpers/FsWrapper"; +import * as sinon from "sinon"; +import { assert, expect } from "chai"; +import * as fs from "fs"; +import * as path from "path"; + +describe("FsWrapper", () => { + let fsWrapper: FsWrapper; + let fsStubs: FsStubs; + let sandbox: sinon.SinonSandbox; + + const fakeErr: Error = new Error("err"); + const exampleFileContent = "Hello World!"; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + fsStubs = createFsStubs(); + fsWrapper = new FsWrapper(fs); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe("readFile", () => { + it("should return a promise which resolves with \"" + exampleFileContent + "\"", async () => { + try { + const content: string = await fsWrapper.readFile("/path/to/file"); + + expect(content).to.equal(exampleFileContent); + } catch (err) { } + }); + + it("should call readFile on the given fs with path and \"utf8\"", async () => { + try { + const path: string = "/path/to/file"; + + await fsWrapper.readFile(path); + + assert(fsStubs.readFile.calledWith(path, "utf8")); + } catch (err) { } + }); + + it("should reject with an error when the readFile function on the given fs returns an error", async () => { + fsStubs.readFile.callsArgWith(2, fakeErr); + + try { + await fsWrapper.readFile("asdf"); + + assert(false, "Function should throw an error!"); + } catch (err) { + console.log(err); + + expect(err).to.equal(fakeErr); + } + }); + }); + + describe("writeFile", () => { + it("should call writeFile on the given fs with path and content", async () => { + const examplePath: string = "/path/to/file"; + + try { + await fsWrapper.writeFile(examplePath, exampleFileContent); + + assert(fsStubs.writeFile.calledWith(examplePath, exampleFileContent)); + } catch (err) { } + }); + + it("should reject with an error when writeFile on the given fs returns with an error", async () => { + fsStubs.writeFile.callsArgWith(2, fakeErr); + + try { + await fsWrapper.writeFile("asdf", "asdf"); + + assert(false, "Function should throw an error!"); + } catch (err) { + expect(err).to.equal(fakeErr); + } + }); + }); + + describe("mkdirp", () => { + const examplePath: string = "path/to/directory/and/file"; + let pathElementCounter: number; + + beforeEach(() => { + pathElementCounter = 0; + fsStubs.mkdir.callsFake((requestedPath: string, options: {}, callback: Function) => { + const elements: Array = requestedPath.split(path.sep); + + if (elements.length > pathElementCounter + 1) { + callback(new FileSystemError({ code: "ENOENT" })); + } else if (elements.length === pathElementCounter) { + callback(new FileSystemError({ code: "EEXIST" })); + } else { + pathElementCounter++; + + callback(); + } + }); + }); + + it("should call the mkdir function on the given fs with the given path", async () => { + await fsWrapper.mkdirp(examplePath); + + assert(fsStubs.mkdir.calledWith(examplePath)); + }); + + it("should bubble down and back up again", async () => { + const callCount: number = (examplePath.split(path.sep).length * 2) - 1; + + await fsWrapper.mkdirp(examplePath); + + assert(fsStubs.mkdir.callCount === callCount); + }); + + it("should reject with an error when a file exists in the given path", async () => { + pathElementCounter = examplePath.split(path.sep).length; + + try { + await fsWrapper.mkdirp(examplePath); + + assert(false, "Function should throw an error!"); + } catch (err) { + expect(err.code).to.equal("EEXIST"); + } + }); + + it("should not reject with an error when the directory already exists in the given path", async () => { + const examplePathArray: Array = examplePath.split(path.sep); + + // Pop the last element, in this case "file" so isDirectory in the stat function will return true + examplePathArray.pop(); + + pathElementCounter = examplePathArray.length; + + try { + await fsWrapper.mkdirp(examplePathArray.join(path.sep)); + + assert(true); + } catch (err) { + assert(false, "Function should not throw an error!"); + } + }); + + it("should throw an error when an unknown error is thrown by mkdir", async () => { + fsStubs.mkdir.throws(new FileSystemError({code: "UNKNOWN"})); + + try { + await fsWrapper.mkdirp("path/to/file"); + + assert(false, "Function should throw an error!"); + } catch(err) { + assert(fsStubs.stat.notCalled); + expect(err.code).to.equal("UNKNOWN"); + } + }); + + it("should throw an error when stat rejects with an error", async () => { + fsStubs.mkdir.throws(new FileSystemError({code: "EEXIST"})); + + fsStubs.stat.callsArgWith(1, fakeErr, null); + + try { + await fsWrapper.mkdirp("path/to/file"); + + assert(false, "Function should throw an error!"); + } catch(err) { + expect(err).to.equal(fakeErr); + } + }); + }); + + function createFsStubs(): FsStubs { + return { + readFile: sandbox.stub(fs, "readFile").callsArgWith(2, null, exampleFileContent), + writeFile: sandbox.stub(fs, "writeFile").callsArgWith(2, null), + stat: sandbox.stub(fs, "stat").callsFake((subject: string, callback: Function) => callback(null, { + isDirectory: () => path.basename(subject) !== "file" + })), + mkdir: sandbox.stub(fs, "mkdir") + }; + } +}); + +class FileSystemError extends Error { + public code: string; + + public constructor(err: { code: string }) { + super("err"); + + this.code = err.code; + } +} + +interface FsStubs { + readFile: sinon.SinonStub; + writeFile: sinon.SinonStub; + stat: sinon.SinonStub; + mkdir: sinon.SinonStub; +} \ No newline at end of file diff --git a/packages/stryker-webpack/test/unit/WebpackCompilerSpec.ts b/packages/stryker-webpack/test/unit/WebpackCompilerSpec.ts new file mode 100644 index 0000000000..2af9ba33b1 --- /dev/null +++ b/packages/stryker-webpack/test/unit/WebpackCompilerSpec.ts @@ -0,0 +1,142 @@ +import {assert, expect} from "chai"; +import * as sinon from "sinon"; +import {createFakeFileSystem, createFakeWebpackConfig, createTextFile, createWebpackMock} from "../helpers/producers"; +import {WebpackCompilerMock} from "../helpers/mockInterfaces"; +import FsWrapper, * as fsWrapper from "../../src/helpers/FsWrapper"; +import WebpackCompiler from "../../src/compiler/WebpackCompiler"; +import {TextFile, FileKind} from "stryker-api/core"; +import * as path from "path"; +import * as webpack from "../../src/compiler/Webpack"; +import {Configuration} from "webpack"; + +describe("WebpackCompiler", () => { + let webpackCompiler: WebpackCompiler; + let sandbox: sinon.SinonSandbox; + let fsWrapperStubs: FsWrapperStubs; + let webpackCompilerMock: WebpackCompilerMock; + + let fakeTextFileArray: Array = createFakeTextFileArray(); + let fakeWebpackConfig: Configuration = createFakeWebpackConfig(); + + beforeEach(() => { + sandbox = sinon.createSandbox(); + + fsWrapperStubs = sinon.createStubInstance(FsWrapper); + webpackCompilerMock = createWebpackMock(); + + sandbox.stub(webpack, "default").returns(webpackCompilerMock); + sandbox.stub(fsWrapper, "default").returns(fsWrapperStubs); + + webpackCompiler = new WebpackCompiler(fakeWebpackConfig, createFakeFileSystem()); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe("construct", () => { + it("should have a compiler with the cache flag set to true", () => { + expect(fakeWebpackConfig.cache).to.equal(true); + }); + }); + + describe("replace", () => { + it("should call the mkdirp function on the fsWrapper with the basedir of the given file", async () => { + await webpackCompiler.replace(fakeTextFileArray); + + fakeTextFileArray.forEach((textFile, index) => { + assert(fsWrapperStubs.mkdirp.getCall(index).calledWith(path.dirname(textFile.name))); + }); + }); + + it("should call the writeFile function on the fsWrapper with the given file", async () => { + await webpackCompiler.replace(fakeTextFileArray); + + fakeTextFileArray.forEach((textFile, index) => { + assert(fsWrapperStubs.writeFile.getCall(index).calledWith(textFile.name)); + }); + }); + }); + + describe("emit", () => { + let webpackRunStub: sinon.SinonStub; + + beforeEach(() => { + webpackRunStub = sandbox.stub(webpackCompilerMock, "run").callsArgWith(0, null, { hasErrors: () => false }); + }); + + it("should call the run function on the webpack compiler", async () => { + await webpackCompiler.emit(); + + assert(webpackRunStub.calledOnce); + }); + + it("should call the readFile function on the fsWrapper with the bundle path", async () => { + await webpackCompiler.emit(); + + assert(fsWrapperStubs.readFile.calledWith("/out/bundle.js")); + }); + + it("should return a new TextFile array with the bundle in it", async () => { + const content: string = "Hello World!"; + fsWrapperStubs.readFile.resolves(content); + + const files: Array = await webpackCompiler.emit(); + + expect(files).to.deep.equal([{ + name: "bundle.js", + content: content, + mutated: true, + included: true, + transpiled: true, + kind: FileKind.Text + }]); + }); + + it("should return an error when the webpack compiler fails to compile", async () => { + const fakeError: string = "fakeError"; + webpackRunStub.callsArgWith(0, new Error(fakeError)); + + try { + await webpackCompiler.emit(); + + assert(false, "Function should throw an error!"); + } catch(err) { + expect(err.name).to.equal("Error"); + expect(err.message).to.equal(fakeError); + } + }); + + it("should return a string representation of the error when the compiler has errors", async () => { + const fakeError: string = "fakeError"; + webpackRunStub.callsArgWith(0, null, { + hasErrors: () => true, + toString: () => fakeError + }); + + try { + await webpackCompiler.emit(); + + assert(false, "Function should throw an error!"); + } catch(err) { + expect(err.name).to.equal("Error"); + expect(err.message).to.equal(fakeError); + } + }); + }); + + function createFakeTextFileArray(): Array { + return [ + createTextFile("path/to/awesome/directory/file1"), + createTextFile("path/to/awesome/directory/file2"), + createTextFile("path/to/awesome/directory/file3"), + createTextFile("path/to/awesome/directory/file4") + ]; + } +}); + +interface FsWrapperStubs { + readFile: sinon.SinonStub; + writeFile: sinon.SinonStub; + mkdirp: sinon.SinonStub; +} \ No newline at end of file diff --git a/packages/stryker-webpack/test/unit/WebpackTranspilerSpec.ts b/packages/stryker-webpack/test/unit/WebpackTranspilerSpec.ts new file mode 100644 index 0000000000..e354ca866b --- /dev/null +++ b/packages/stryker-webpack/test/unit/WebpackTranspilerSpec.ts @@ -0,0 +1,91 @@ +import {expect, assert} from "chai"; +import {Config} from "stryker-api/config"; +import {TranspileResult} from "stryker-api/transpile"; +import {Position, TextFile} from "stryker-api/core"; +import {createTextFile} from "../helpers/producers"; +import * as sinon from "sinon"; +import WebpackTranspiler from "../../src/WebpackTranspiler"; +import WebpackCompiler, * as webpackCompiler from "../../src/compiler/WebpackCompiler"; + +describe("WebpackTranspiler", () => { + const sandbox: sinon.SinonSandbox = sinon.createSandbox(); + let webpackTranspiler: WebpackTranspiler; + let webpackCompilerService: WebpackCompilerStubs; + + let fakeFileArray: Array; + + beforeEach(() => { + webpackCompilerService = sinon.createStubInstance(WebpackCompiler); + + sandbox.stub(webpackCompiler, 'default').returns(webpackCompilerService); + + webpackTranspiler = new WebpackTranspiler({config: new Config, keepSourceMaps: false}); + + fakeFileArray = [ + createTextFile("test"), + createTextFile("works") + ]; + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe("transpile", () => { + it("should call the \"replace\" function on the \"webpackCompiler\"", async () => { + await webpackTranspiler.transpile(fakeFileArray); + + assert(webpackCompilerService.replace.calledWith(fakeFileArray)); + }); + + it("should call the \"emit\" function on the \"webpackCompiler\"", async () => { + await webpackTranspiler.transpile([]); + + assert(webpackCompilerService.emit.calledOnce); + }); + + it("should send a successResponse when finished", async () => { + const fakeEmitFiles: Array = [createTextFile("bundle.js")]; + + webpackCompilerService.emit.resolves(fakeEmitFiles); + + const result: TranspileResult = await webpackTranspiler.transpile(fakeFileArray); + + expect(result.outputFiles).to.deep.equal(fakeEmitFiles); + expect(result.error).to.be.null; + }); + + it("should send a errorResponse when the webpackCompiler throws an error", async () => { + const fakeError: string = "fakeError"; + + webpackCompilerService.emit.throwsException(Error(fakeError)); + + const result: TranspileResult = await webpackTranspiler.transpile(fakeFileArray); + + expect(result.outputFiles).to.be.an("array").that.is.empty; + expect(result.error).to.equal(`Error: ${fakeError}`); + }); + }); + + describe("getMappedLocation", () => { + it("should throw an error informing the user the function is not implemented", () => { + const position: Position = { + line: 0, + column: 0 + }; + + const fileLocation: { fileName: string, start: Position, end: Position } = { + fileName: "test", + start: position, + end: position + } + + expect(webpackTranspiler.getMappedLocation.bind(this, fileLocation)).to.throw(Error, "Method not implemented."); + }); + }) +}); + +interface WebpackCompilerStubs { + replace: sinon.SinonStub; + emit: sinon.SinonStub; +} \ No newline at end of file diff --git a/packages/stryker-webpack/testResources/sampleProject/index.js b/packages/stryker-webpack/testResources/sampleProject/index.js new file mode 100644 index 0000000000..9064816636 --- /dev/null +++ b/packages/stryker-webpack/testResources/sampleProject/index.js @@ -0,0 +1,13 @@ +const sum = require("./lib/sum"); +const square = require("./lib/square"); +const divide = require("./lib/divide"); + +const answerArray = []; + +[1,2,3,4].map(function(number) { + answerArray.push(sum(number, number)); + answerArray.push(square(number, number)); + answerArray.push(divide(number, number)); +}); + +console.log(answerArray); \ No newline at end of file diff --git a/packages/stryker-webpack/testResources/sampleProject/lib/divide.js b/packages/stryker-webpack/testResources/sampleProject/lib/divide.js new file mode 100644 index 0000000000..476a1bc63c --- /dev/null +++ b/packages/stryker-webpack/testResources/sampleProject/lib/divide.js @@ -0,0 +1,3 @@ +module.exports = function(number) { + return number / number; +}; \ No newline at end of file diff --git a/packages/stryker-webpack/testResources/sampleProject/lib/square.js b/packages/stryker-webpack/testResources/sampleProject/lib/square.js new file mode 100644 index 0000000000..19760a9764 --- /dev/null +++ b/packages/stryker-webpack/testResources/sampleProject/lib/square.js @@ -0,0 +1,3 @@ +module.exports = function(number) { + return number * number; +}; \ No newline at end of file diff --git a/packages/stryker-webpack/testResources/sampleProject/lib/sum.js b/packages/stryker-webpack/testResources/sampleProject/lib/sum.js new file mode 100644 index 0000000000..12428ac561 --- /dev/null +++ b/packages/stryker-webpack/testResources/sampleProject/lib/sum.js @@ -0,0 +1,3 @@ +module.exports = function(number) { + return number + number; +}; \ No newline at end of file