diff --git a/package.json b/package.json index bb49a5aab7e5..7b1ff0bd0a47 100644 --- a/package.json +++ b/package.json @@ -63,8 +63,6 @@ "isbinaryfile": "^3.0.0", "istanbul-instrumenter-loader": "^2.0.0", "json-loader": "^0.5.4", - "karma-sourcemap-loader": "^0.3.7", - "karma-webpack": "^2.0.0", "less": "^2.7.2", "less-loader": "^4.0.2", "loader-utils": "^1.0.2", @@ -96,6 +94,7 @@ "url-loader": "^0.5.7", "walk-sync": "^0.3.1", "webpack": "~2.4.0", + "webpack-dev-middleware": "^1.10.2", "webpack-dev-server": "~2.4.2", "webpack-merge": "^2.4.0", "zone.js": "^0.8.4" diff --git a/packages/@angular/cli/blueprints/ng/files/karma.conf.js b/packages/@angular/cli/blueprints/ng/files/karma.conf.js index 328acb70f395..4d9ab9d94828 100644 --- a/packages/@angular/cli/blueprints/ng/files/karma.conf.js +++ b/packages/@angular/cli/blueprints/ng/files/karma.conf.js @@ -15,15 +15,6 @@ module.exports = function (config) { client:{ clearContext: false // leave Jasmine Spec Runner output visible in browser }, - files: [ - { pattern: './<%= sourceDir %>/test.ts', watched: false } - ], - preprocessors: { - './<%= sourceDir %>/test.ts': ['@angular/cli'] - }, - mime: { - 'text/x-typescript': ['ts','tsx'] - }, coverageIstanbulReporter: { reports: [ 'html', 'lcovonly' ], fixWebpackSourcePaths: true @@ -31,9 +22,7 @@ module.exports = function (config) { angularCli: { environment: 'dev' }, - reporters: config.angularCli && config.angularCli.codeCoverage - ? ['progress', 'coverage-istanbul'] - : ['progress', 'kjhtml'], + reporters: ['progress', 'kjhtml'], port: 9876, colors: true, logLevel: config.LOG_INFO, diff --git a/packages/@angular/cli/models/webpack-configs/test.ts b/packages/@angular/cli/models/webpack-configs/test.ts index add32bd23cb0..29053597520e 100644 --- a/packages/@angular/cli/models/webpack-configs/test.ts +++ b/packages/@angular/cli/models/webpack-configs/test.ts @@ -4,7 +4,7 @@ import * as webpack from 'webpack'; import { CliConfig } from '../config'; import { WebpackTestOptions } from '../webpack-test-config'; -import { KarmaWebpackEmitlessError } from '../../plugins/karma-webpack-emitless-error'; + /** * Enumerate loaders and their dependencies from this file to let the dependency validator @@ -20,7 +20,9 @@ export function getTestConfig(testConfig: WebpackTestOptions) { const configPath = CliConfig.configFilePath(); const projectRoot = path.dirname(configPath); const appConfig = CliConfig.fromProject().config.apps[0]; + const nodeModules = path.resolve(projectRoot, 'node_modules'); const extraRules: any[] = []; + const extraPlugins: any[] = []; if (testConfig.codeCoverage && CliConfig.fromProject()) { const codeCoverageExclude = CliConfig.fromProject().get('test.codeCoverage.exclude'); @@ -38,7 +40,6 @@ export function getTestConfig(testConfig: WebpackTestOptions) { }); } - extraRules.push({ test: /\.(js|ts)$/, loader: 'istanbul-instrumenter-loader', enforce: 'post', @@ -49,17 +50,21 @@ export function getTestConfig(testConfig: WebpackTestOptions) { return { devtool: testConfig.sourcemaps ? 'inline-source-map' : 'eval', entry: { - test: path.resolve(projectRoot, appConfig.root, appConfig.test) + main: path.resolve(projectRoot, appConfig.root, appConfig.test) }, module: { rules: [].concat(extraRules) }, plugins: [ - new webpack.SourceMapDevToolPlugin({ - filename: null, // if no value is provided the sourcemap is inlined - test: /\.(ts|js)($|\?)/i // process .js and .ts files only + new webpack.optimize.CommonsChunkPlugin({ + minChunks: Infinity, + name: 'inline' }), - new KarmaWebpackEmitlessError() - ] + new webpack.optimize.CommonsChunkPlugin({ + name: 'vendor', + chunks: ['main'], + minChunks: (module: any) => module.resource && module.resource.startsWith(nodeModules) + }) + ].concat(extraPlugins) }; } diff --git a/packages/@angular/cli/models/webpack-test-config.ts b/packages/@angular/cli/models/webpack-test-config.ts index a5b9049f62db..b9de83223662 100644 --- a/packages/@angular/cli/models/webpack-test-config.ts +++ b/packages/@angular/cli/models/webpack-test-config.ts @@ -1,4 +1,3 @@ -import * as webpack from 'webpack'; const webpackMerge = require('webpack-merge'); import { BuildOptions } from './build-options'; @@ -28,11 +27,6 @@ export class WebpackTestConfig extends NgCliWebpackConfig { ]; this.config = webpackMerge(webpackConfigs); - delete this.config.entry; - - // Remove any instance of CommonsChunkPlugin, not needed with karma-webpack. - this.config.plugins = this.config.plugins.filter((plugin: any) => - !(plugin instanceof webpack.optimize.CommonsChunkPlugin)); return this.config; } diff --git a/packages/@angular/cli/package.json b/packages/@angular/cli/package.json index a97078b5a00d..5365ad0a57b6 100644 --- a/packages/@angular/cli/package.json +++ b/packages/@angular/cli/package.json @@ -50,8 +50,6 @@ "inquirer": "^3.0.0", "isbinaryfile": "^3.0.0", "json-loader": "^0.5.4", - "karma-sourcemap-loader": "^0.3.7", - "karma-webpack": "^2.0.0", "less": "^2.7.2", "less-loader": "^4.0.2", "lodash": "^4.11.1", @@ -81,6 +79,7 @@ "url-loader": "^0.5.7", "walk-sync": "^0.3.1", "webpack": "~2.4.0", + "webpack-dev-middleware": "^1.10.2", "webpack-dev-server": "~2.4.2", "webpack-merge": "^2.4.0", "zone.js": "^0.8.4" diff --git a/packages/@angular/cli/plugins/karma-context.html b/packages/@angular/cli/plugins/karma-context.html new file mode 100644 index 000000000000..1c8f49c653f5 --- /dev/null +++ b/packages/@angular/cli/plugins/karma-context.html @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + %SCRIPTS% + + + + + + + diff --git a/packages/@angular/cli/plugins/karma-debug.html b/packages/@angular/cli/plugins/karma-debug.html new file mode 100644 index 000000000000..649d59817e04 --- /dev/null +++ b/packages/@angular/cli/plugins/karma-debug.html @@ -0,0 +1,43 @@ + + + + + +%X_UA_COMPATIBLE% + Karma DEBUG RUNNER + + + + + + + + + + + + + + %SCRIPTS% + + + + + + + diff --git a/packages/@angular/cli/plugins/karma-webpack-emitless-error.ts b/packages/@angular/cli/plugins/karma-webpack-emitless-error.ts deleted file mode 100644 index d028d22fc7f9..000000000000 --- a/packages/@angular/cli/plugins/karma-webpack-emitless-error.ts +++ /dev/null @@ -1,20 +0,0 @@ -// Don't emit anything when there are compilation errors. This is useful for preventing Karma -// from re-running tests when there is a compilation error. -// Workaround for https://github.com/webpack-contrib/karma-webpack/issues/49 - -export class KarmaWebpackEmitlessError { - constructor() { } - - apply(compiler: any): void { - compiler.plugin('done', (stats: any) => { - if (stats.compilation.errors.length > 0) { - stats.stats = [{ - toJson: function () { - return this; - }, - assets: [] - }]; - } - }); - } -} diff --git a/packages/@angular/cli/plugins/karma.ts b/packages/@angular/cli/plugins/karma.ts index 42303db5a9d8..8025af5f23f8 100644 --- a/packages/@angular/cli/plugins/karma.ts +++ b/packages/@angular/cli/plugins/karma.ts @@ -1,14 +1,18 @@ import * as path from 'path'; import * as fs from 'fs'; import * as glob from 'glob'; +import * as webpack from 'webpack'; +const webpackDevMiddleware = require('webpack-dev-middleware'); import { Pattern } from './glob-copy-webpack-plugin'; -import { extraEntryParser } from '../models/webpack-configs/utils'; import { WebpackTestConfig, WebpackTestOptions } from '../models/webpack-test-config'; import { KarmaWebpackThrowError } from './karma-webpack-throw-error'; const getAppFromConfig = require('../utilities/app-utils').getAppFromConfig; +let blocked: any[] = []; +let isBlocked = false; + function isDirectory(path: string) { try { return fs.statSync(path).isDirectory(); @@ -40,7 +44,7 @@ function addKarmaFiles(files: any[], newFiles: any[], prepend = false) { } } -const init: any = (config: any) => { +const init: any = (config: any, emitter: any, customFileHandlers: any) => { const appConfig = getAppFromConfig(config.angularCli.app); const appRoot = path.join(config.basePath, appConfig.root); const testConfig: WebpackTestOptions = Object.assign({ @@ -89,6 +93,8 @@ const init: any = (config: any) => { const webpackConfig = new WebpackTestConfig(testConfig, appConfig).buildConfig(); const webpackMiddlewareConfig = { noInfo: true, // Hide webpack output because its noisy. + watchOptions: { poll: testConfig.poll }, + publicPath: '/_karma_webpack_/', stats: { // Also prevent chunk and module display output, cleaner look. Only emit errors. assets: false, colors: true, @@ -97,9 +103,6 @@ const init: any = (config: any) => { timings: false, chunks: false, chunkModules: false - }, - watchOptions: { - poll: testConfig.poll } }; @@ -108,40 +111,125 @@ const init: any = (config: any) => { webpackConfig.plugins.push(new KarmaWebpackThrowError()); } + // Use existing config if any. config.webpack = Object.assign(webpackConfig, config.webpack); config.webpackMiddleware = Object.assign(webpackMiddlewareConfig, config.webpackMiddleware); - // Replace the @angular/cli preprocessor with webpack+sourcemap. - Object.keys(config.preprocessors) - .filter((file) => config.preprocessors[file].indexOf('@angular/cli') !== -1) - .map((file) => config.preprocessors[file]) - .map((arr) => arr.splice(arr.indexOf('@angular/cli'), 1, 'webpack', 'sourcemap')); - - // Add global scripts. This logic mimics the one in webpack-configs/common. - if (appConfig.scripts && appConfig.scripts.length > 0) { - const globalScriptPatterns = extraEntryParser(appConfig.scripts, appRoot, 'scripts') - // Neither renamed nor lazy scripts are currently supported - .filter(script => !(script.output || script.lazy)) - .map(script => ({ pattern: path.resolve(appRoot, script.input) })); - addKarmaFiles(config.files, globalScriptPatterns, true); + // Remove the @angular/cli test file if present, for backwards compatibility. + const testFilePath = path.join(appRoot, appConfig.test); + config.files.forEach((file: any, index: number) => { + if (path.normalize(file.pattern) === testFilePath) { + config.files.splice(index, 1); + } + }); + + // When using code-coverage, auto-add coverage-istanbul. + config.reporters = config.reporters || []; + if (testConfig.codeCoverage && config.reporters.indexOf('coverage-istanbul') === -1) { + config.reporters.push('coverage-istanbul'); } - // Add polyfills file before everything else - if (appConfig.polyfills) { - const polyfillsFile = path.resolve(appRoot, appConfig.polyfills); - config.preprocessors[polyfillsFile] = ['webpack', 'sourcemap']; - addKarmaFiles(config.files, [{ pattern: polyfillsFile }], true); + // Our custom context and debug files list the webpack bundles directly instead of using + // the karma files array. + config.customContextFile = `${__dirname}/karma-context.html`; + config.customDebugFile = `${__dirname}/karma-debug.html`; + + // Add the request blocker. + config.beforeMiddleware = config.beforeMiddleware || []; + config.beforeMiddleware.push('angularCliBlocker'); + + // Delete global styles entry, we don't want to load them. + delete webpackConfig.entry.styles; + + // The webpack tier owns the watch behavior so we want to force it in the config. + webpackConfig.watch = true; + // Files need to be served from a custom path for Karma. + webpackConfig.output.path = '/_karma_webpack_/'; + webpackConfig.output.publicPath = '/_karma_webpack_/'; + + let compiler: any; + try { + compiler = webpack(webpackConfig); + } catch (e) { + console.error(e.stack || e); + if (e.details) { + console.error(e.details); + } + throw e; } + + ['invalid', 'watch-run', 'run'].forEach(function (name) { + compiler.plugin(name, function (_: any, callback: () => void) { + isBlocked = true; + + if (typeof callback === 'function') { + callback(); + } + }); + }); + + compiler.plugin('done', (stats: any) => { + // Don't refresh karma when there are webpack errors. + if (stats.compilation.errors.length === 0) { + emitter.refreshFiles(); + isBlocked = false; + blocked.forEach((cb) => cb()); + blocked = []; + } + }); + + const middleware = new webpackDevMiddleware(compiler, webpackMiddlewareConfig); + + // Forward requests to webpack server. + customFileHandlers.push({ + urlRegex: /^\/_karma_webpack_\/.*/, + handler: function handler(req: any, res: any) { + middleware(req, res, function () { + // Ensure script and style bundles are served. + // They are mentioned in the custom karma context page and we don't want them to 404. + const alwaysServe = [ + '/_karma_webpack_/inline.bundle.js', + '/_karma_webpack_/polyfills.bundle.js', + '/_karma_webpack_/scripts.bundle.js', + '/_karma_webpack_/vendor.bundle.js', + ]; + if (alwaysServe.indexOf(req.url) != -1) { + res.statusCode = 200; + res.end(); + } else { + res.statusCode = 404; + res.end('Not found'); + } + }); + } + }); + + emitter.on('exit', (done: any) => { + middleware.close(); + done(); + }); }; -init.$inject = ['config']; +init.$inject = ['config', 'emitter', 'customFileHandlers']; // Dummy preprocessor, just to keep karma from showing a warning. const preprocessor: any = () => (content: any, _file: string, done: any) => done(null, content); preprocessor.$inject = []; +// Block requests until the Webpack compilation is done. +function requestBlocker() { + return function (_request: any, _response: any, next: () => void) { + if (isBlocked) { + blocked.push(next); + } else { + next(); + } + }; +} + // Also export karma-webpack and karma-sourcemap-loader. module.exports = Object.assign({ 'framework:@angular/cli': ['factory', init], - 'preprocessor:@angular/cli': ['factory', preprocessor] -}, require('karma-webpack'), require('karma-sourcemap-loader')); + 'preprocessor:@angular/cli': ['factory', preprocessor], + 'middleware:angularCliBlocker': ['factory', requestBlocker] +}); diff --git a/tests/e2e/tests/test/test-backwards-compat.ts b/tests/e2e/tests/test/test-backwards-compat.ts new file mode 100644 index 000000000000..3cd2ecc0cf15 --- /dev/null +++ b/tests/e2e/tests/test/test-backwards-compat.ts @@ -0,0 +1,21 @@ +import { ng } from '../../utils/process'; +import { replaceInFile } from '../../utils/fs'; + +export default function () { + // Old configs (with the cli preprocessor listed) should still supported. + return Promise.resolve() + .then(() => replaceInFile('karma.conf.js', + 'coverageIstanbulReporter: {', ` + files: [ + { pattern: './src/test.ts', watched: false } + ], + preprocessors: { + './src/test.ts': ['@angular/cli'] + }, + mime: { + 'text/x-typescript': ['ts','tsx'] + }, + coverageIstanbulReporter: { + `)) + .then(() => ng('test', '--single-run')); +}