mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-19 04:26:01 +08:00
This PR uses a new Karma plugin to enable vendor bundles in unit tests, increasing rebuild performance. On a medium size project rebuilds times were 15x smaller (16.5s to 0.9s). Fix #5423
236 lines
7.8 KiB
TypeScript
236 lines
7.8 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';
|
|
|
|
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);
|
|
|
|
// 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();
|
|
}
|
|
};
|
|
}
|
|
|
|
// Also export karma-webpack and karma-sourcemap-loader.
|
|
module.exports = Object.assign({
|
|
'framework:@angular/cli': ['factory', init],
|
|
'preprocessor:@angular/cli': ['factory', preprocessor],
|
|
'middleware:angularCliBlocker': ['factory', requestBlocker]
|
|
});
|