diff --git a/package.json b/package.json
index 433b3f6d569d..df72999e725b 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 05d47cae2b84..63cd94c150ef 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'));
+}