mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-25 16:57:51 +08:00
#6160 broke `ng test` sourcemaps in Chrome, this PR fixes it. Before: ``` kamik@T460p MINGW64 D:/sandbox/master-project (master) $ ng test --sr 10% building modules 1/1 modules 0 active13 07 2017 16:38:43.942:INFO [karma]: Karma v1.7.0 server started at http://0.0.0.0:9876/ 13 07 2017 16:38:43.945:INFO [launcher]: Launching browser Chrome with unlimited concurrency 13 07 2017 16:38:43.953:INFO [launcher]: Starting browser Chrome 13 07 2017 16:38:54.099:INFO [Chrome 59.0. 3071 (Windows 10 0.0.0)]: Connected on socket Xdep72ofT_Q7MWBHAAAA with id 76010950 Chrome 59.0.3071 (Windows 10 0.0.0) AppComponent should have as title 'app' FAILED Expected 'app' to equal 'forced bug'. at Object.<anonymous> (http://localhost:9876/_karma_webpack_/main.bundle.js:89:27) at ZoneDelegate.webpackJsonp.../../../../../../../sandbox/master-project/node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke (http://localhost:9 876/_karma_webpack_/polyfills.bundle.js:2704:26) at AsyncTestZoneSpec.webpackJsonp.../../../../../../../sandbox/master-project/node_modules/zone.js/dist/async-test.js.AsyncTestZoneSpec.onInvoke ( http://localhost:9876/_karma_webpack_/vendor.bundle.js:55972:39) at ProxyZoneSpec.webpackJsonp.../../../../../../../sandbox/master-project/node_modules/zone.js/dist/proxy.js.ProxyZoneSpec.onInvoke (http://localh ost:9876/_karma_webpack_/vendor.bundle.js:56735:39) Chrome 59.0.3071 (Windows 10 0.0.0): Executed 2 of 3 (1 FAILED) (0 secs / 0.162 secs) Chrome 59.0.3071 (Windows 10 0.0.0) AppComponent should have as title 'app' FAILED Expected 'app' to equal 'forced bug'. at Object.<anonymous> (http://localhost:9876/_karma_webpack_/main.bundle.js:89:27) at ZoneDelegate.webpackJsonp.../../../../../../../sandbox/master-project/node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke (http://localhost:9 876/_karma_webpack_/polyfills.bundle.js:2704:26) at AsyncTestZoneSpec.webpackJsonp.../../../../../../../sandbox/master-project/node_modules/zone.js/dist/async-test.js.AsyncTestZoneSpec.onInvoke ( http://localhost:9876/_karma_webpack_/vendor.bundle.js:55972:39) at ProxyZoneSpec.webpackJsonp.../../../../../../../sandbox/master-project/node_modules/zone.js/dist/proxy.js.ProxyZoneSpec.onInvoke (http://localh Chrome 59.0.3071 (Windows 10 0.0.0): Executed 3 of 3 (1 FAILED) (0.225 secs / 0.204 secs) ``` After: ``` kamik@T460p MINGW64 D:/sandbox/master-project (master) $ ng test --sr 10% building modules 1/1 modules 0 active13 07 2017 16:37:59.478:INFO [karma]: Karma v1.7.0 server started at http://0.0.0.0:9876/ 13 07 2017 16:37:59.481:INFO [launcher]: Launching browser Chrome with unlimited concurrency 13 07 2017 16:37:59.487:INFO [launcher]: Starting browser Chrome 13 07 2017 16:38:08.559:INFO [Chrome 59.0. 3071 (Windows 10 0.0.0)]: Connected on socket ixHN4NzVFTEW1iYiAAAA with id 92819713 Chrome 59.0.3071 (Windows 10 0.0.0) AppComponent should have as title 'app' FAILED Expected 'app' to equal 'forced bug'. at Object.<anonymous> D:/sandbox/master-project/src/app/app.component.spec.ts:23:23) at ZoneDelegate.webpackJsonp.../../../../../../../sandbox/master-project/node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke D:/sandbox/master-p roject/node_modules/zone.js/dist/zone.js:391:1) at AsyncTestZoneSpec.webpackJsonp.../../../../../../../sandbox/master-project/node_modules/zone.js/dist/async-test.js.AsyncTestZoneSpec.onInvoke D :/sandbox/master-project/node_modules/zone.js/dist/async-test.js:49:1) at ProxyZoneSpec.webpackJsonp.../../../../../../../sandbox/master-project/node_modules/zone.js/dist/proxy.js.ProxyZoneSpec.onInvoke D:/sandbox/mas ter-project/node_modules/zone.js/dist/proxy.js:76:1) Chrome 59.0.3071 (Windows 10 0.0.0): Executed 2 of 3 (1 FAILED) (0 secs / 0.367 secs) Chrome 59.0.3071 (Windows 10 0.0.0) AppComponent should have as title 'app' FAILED Expected 'app' to equal 'forced bug'. at Object.<anonymous> D:/sandbox/master-project/src/app/app.component.spec.ts:23:23) at ZoneDelegate.webpackJsonp.../../../../../../../sandbox/master-project/node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke D:/sandbox/master-p roject/node_modules/zone.js/dist/zone.js:391:1) at AsyncTestZoneSpec.webpackJsonp.../../../../../../../sandbox/master-project/node_modules/zone.js/dist/async-test.js.AsyncTestZoneSpec.onInvoke D :/sandbox/master-project/node_modules/zone.js/dist/async-test.js:49:1) at ProxyZoneSpec.webpackJsonp.../../../../../../../sandbox/master-project/node_modules/zone.js/dist/proxy.js.ProxyZoneSpec.onInvoke D:/sandbox/mas Chrome 59.0.3071 (Windows 10 0.0.0): Executed 3 of 3 (1 FAILED) (0.421 secs / 0.4 secs) ``` Fix #6583
276 lines
9.3 KiB
TypeScript
276 lines
9.3 KiB
TypeScript
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 { WebpackTestConfig, WebpackTestOptions } from '../models/webpack-test-config';
|
|
import { KarmaWebpackThrowError } from './karma-webpack-throw-error';
|
|
|
|
/**
|
|
* Enumerate needed (but not require/imported) dependencies from this file
|
|
* to let the dependency validator know they are used.
|
|
*
|
|
* require('karma-source-map-support')
|
|
*/
|
|
|
|
|
|
const getAppFromConfig = require('../utilities/app-utils').getAppFromConfig;
|
|
|
|
let blocked: any[] = [];
|
|
let isBlocked = false;
|
|
|
|
function isDirectory(path: string) {
|
|
try {
|
|
return fs.statSync(path).isDirectory();
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Add files to the Karma files array.
|
|
function addKarmaFiles(files: any[], newFiles: any[], prepend = false) {
|
|
const defaults = {
|
|
included: true,
|
|
served: true,
|
|
watched: true
|
|
};
|
|
|
|
const processedFiles = newFiles
|
|
// Remove globs that do not match any files, otherwise Karma will show a warning for these.
|
|
.filter(file => glob.sync(file.pattern, { nodir: true }).length != 0)
|
|
// Fill in pattern properties with defaults.
|
|
.map(file => ({ ...defaults, ...file }));
|
|
|
|
// It's important to not replace the array, because
|
|
// karma already has a reference to the existing array.
|
|
if (prepend) {
|
|
files.unshift(...processedFiles);
|
|
} else {
|
|
files.push(...processedFiles);
|
|
}
|
|
}
|
|
|
|
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({
|
|
environment: 'dev',
|
|
codeCoverage: false,
|
|
sourcemaps: true,
|
|
progress: true,
|
|
}, config.angularCli);
|
|
|
|
if (testConfig.sourcemaps) {
|
|
// Add a reporter that fixes sourcemap urls.
|
|
config.reporters.unshift('@angular/cli');
|
|
|
|
// Code taken from https://github.com/tschaub/karma-source-map-support.
|
|
// We can't use it directly because we need to add it conditionally in this file, and karma
|
|
// frameworks cannot be added dynamically.
|
|
const smsPath = path.dirname(require.resolve('source-map-support'));
|
|
const ksmsPath = path.dirname(require.resolve('karma-source-map-support'));
|
|
|
|
addKarmaFiles(config.files, [
|
|
{ pattern: path.join(smsPath, 'browser-source-map-support.js'), watched: false },
|
|
{ pattern: path.join(ksmsPath, 'client.js'), watched: false }
|
|
], true);
|
|
}
|
|
|
|
// Add assets. This logic is mimics the one present in GlobCopyWebpackPlugin.
|
|
if (appConfig.assets) {
|
|
config.proxies = config.proxies || {};
|
|
appConfig.assets.forEach((pattern: Pattern) => {
|
|
// Convert all string patterns to Pattern type.
|
|
pattern = typeof pattern === 'string' ? { glob: pattern } : pattern;
|
|
// Add defaults.
|
|
// Input is always resolved relative to the appRoot.
|
|
pattern.input = path.resolve(appRoot, pattern.input || '');
|
|
pattern.output = pattern.output || '';
|
|
pattern.glob = pattern.glob || '';
|
|
|
|
// Build karma file pattern.
|
|
const assetPath = path.join(pattern.input, pattern.glob);
|
|
const filePattern = isDirectory(assetPath) ? assetPath + '/**' : assetPath;
|
|
addKarmaFiles(config.files, [{ pattern: filePattern, included: false }]);
|
|
|
|
// The `files` entry serves the file from `/base/{asset.input}/{asset.glob}`.
|
|
// We need to add a URL rewrite that exposes the asset as `/{asset.output}/{asset.glob}`.
|
|
let relativePath: string, proxyPath: string;
|
|
if (fs.existsSync(assetPath)) {
|
|
relativePath = path.relative(config.basePath, assetPath);
|
|
proxyPath = path.join(pattern.output, pattern.glob);
|
|
} else {
|
|
// For globs (paths that don't exist), proxy pattern.output to pattern.input.
|
|
relativePath = path.relative(config.basePath, pattern.input);
|
|
proxyPath = path.join(pattern.output);
|
|
|
|
}
|
|
// Proxy paths must have only forward slashes.
|
|
proxyPath = proxyPath.replace(/\\/g, '/');
|
|
config.proxies['/' + proxyPath] = '/base/' + relativePath;
|
|
});
|
|
}
|
|
|
|
// Add webpack config.
|
|
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,
|
|
version: false,
|
|
hash: false,
|
|
timings: false,
|
|
chunks: false,
|
|
chunkModules: false
|
|
}
|
|
};
|
|
|
|
// If Karma is being ran in single run mode, throw errors.
|
|
if (config.singleRun) {
|
|
webpackConfig.plugins.push(new KarmaWebpackThrowError());
|
|
}
|
|
|
|
// Use existing config if any.
|
|
config.webpack = Object.assign(webpackConfig, config.webpack);
|
|
config.webpackMiddleware = Object.assign(webpackMiddlewareConfig, config.webpackMiddleware);
|
|
|
|
// 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');
|
|
}
|
|
|
|
// 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', '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();
|
|
}
|
|
};
|
|
}
|
|
|
|
// Strip the server address and webpack scheme (webpack://) from error log.
|
|
const initSourcemapReporter: any = function (baseReporterDecorator: any) {
|
|
baseReporterDecorator(this);
|
|
const urlRegexp = /\(http:\/\/localhost:\d+\/_karma_webpack_\/webpack:\//gi;
|
|
|
|
this.onSpecComplete = function (_browser: any, result: any) {
|
|
if (!result.success && result.log.length > 0) {
|
|
result.log.forEach((log: string, idx: number) => {
|
|
result.log[idx] = log.replace(urlRegexp, '');
|
|
});
|
|
}
|
|
};
|
|
};
|
|
|
|
initSourcemapReporter.$inject = ['baseReporterDecorator'];
|
|
|
|
module.exports = Object.assign({
|
|
'framework:@angular/cli': ['factory', init],
|
|
'preprocessor:@angular/cli': ['factory', preprocessor],
|
|
'reporter:@angular/cli': ['type', initSourcemapReporter],
|
|
'middleware:angularCliBlocker': ['factory', requestBlocker]
|
|
});
|