chore(nx): format new files

This commit is contained in:
Brandon Roberts 2019-11-05 09:44:20 -06:00 committed by Victor Savkin
parent c2ed286d5a
commit a24c31dd20
56 changed files with 2176 additions and 1543 deletions

View File

@ -81,16 +81,14 @@ export function getBaseWebpackPartial(
} }
if (options.extractLicenses) { if (options.extractLicenses) {
extraPlugins.push( extraPlugins.push((new LicenseWebpackPlugin({
new LicenseWebpackPlugin({ stats: {
stats: { warnings: false,
warnings: false, errors: false
errors: false, },
}, perChunkOutput: false,
perChunkOutput: false, outputFilename: `3rdpartylicenses.txt`
outputFilename: `3rdpartylicenses.txt`, }) as unknown) as webpack.Plugin);
}) as unknown as webpack.Plugin
);
} }
// process asset entries // process asset entries

View File

@ -71,12 +71,12 @@
"license-webpack-plugin": "^1.4.0", "license-webpack-plugin": "^1.4.0",
"loader-utils": "1.2.3", "loader-utils": "1.2.3",
"mini-css-extract-plugin": "0.8.0", "mini-css-extract-plugin": "0.8.0",
"minimatch": "3.0.4", "minimatch": "3.0.4",
"parse5": "4.0.0", "parse5": "4.0.0",
"open": "6.4.0", "open": "6.4.0",
"postcss": "7.0.17", "postcss": "7.0.17",
"postcss-import": "12.0.1", "postcss-import": "12.0.1",
"postcss-loader": "3.0.0", "postcss-loader": "3.0.0",
"raw-loader": "3.1.0", "raw-loader": "3.1.0",
"rxjs": "6.4.0", "rxjs": "6.4.0",
"regenerator-runtime": "0.13.3", "regenerator-runtime": "0.13.3",
@ -93,15 +93,15 @@
"sass-loader": "7.2.0", "sass-loader": "7.2.0",
"semver": "6.3.0", "semver": "6.3.0",
"source-map": "0.7.3", "source-map": "0.7.3",
"source-map-loader": "0.2.4", "source-map-loader": "0.2.4",
"source-map-support": "0.5.12", "source-map-support": "0.5.12",
"speed-measure-webpack-plugin": "1.3.1", "speed-measure-webpack-plugin": "1.3.1",
"style-loader": "1.0.0", "style-loader": "1.0.0",
"stylus": "0.54.5", "stylus": "0.54.5",
"stylus-loader": "3.0.2", "stylus-loader": "3.0.2",
"tree-kill": "1.2.1", "tree-kill": "1.2.1",
"terser": "4.3.8", "terser": "4.3.8",
"terser-webpack-plugin": "1.4.1", "terser-webpack-plugin": "1.4.1",
"ts-loader": "5.4.5", "ts-loader": "5.4.5",
"tsconfig-paths-webpack-plugin": "3.2.0", "tsconfig-paths-webpack-plugin": "3.2.0",
"webpack": "4.39.2", "webpack": "4.39.2",
@ -109,7 +109,7 @@
"webpack-merge": "4.2.1", "webpack-merge": "4.2.1",
"webpack-sources": "1.4.3", "webpack-sources": "1.4.3",
"webpack-subresource-integrity": "1.1.0-rc.6", "webpack-subresource-integrity": "1.1.0-rc.6",
"worker-plugin": "3.2.0", "worker-plugin": "3.2.0",
"webpack-dev-server": "3.1.14", "webpack-dev-server": "3.1.14",
"webpack-node-externals": "1.7.2" "webpack-node-externals": "1.7.2"
} }

View File

@ -16,7 +16,7 @@ import {
Budget, Budget,
ExtraEntryPoint, ExtraEntryPoint,
OptimizationClass, OptimizationClass,
SourceMapClass, SourceMapClass
} from '../../browser/schema'; } from '../../browser/schema';
import { NormalizedFileReplacement } from '../../utils/normalize-file-replacements'; import { NormalizedFileReplacement } from '../../utils/normalize-file-replacements';

View File

@ -101,4 +101,4 @@ import 'core-js/modules/web.dom-collections.iterator';
import 'core-js/modules/es.promise'; import 'core-js/modules/es.promise';
import 'core-js/modules/es.json.to-string-tag'; import 'core-js/modules/es.json.to-string-tag';
import 'regenerator-runtime/runtime'; import 'regenerator-runtime/runtime';

View File

@ -1,19 +1,23 @@
(function () { (function() {
var check = document.createElement('script'); var check = document.createElement('script');
if (!('noModule' in check) && 'onbeforeload' in check) { if (!('noModule' in check) && 'onbeforeload' in check) {
var support = false; var support = false;
document.addEventListener('beforeload', function (e) { document.addEventListener(
if (e.target === check) { 'beforeload',
support = true; function(e) {
} else if (!e.target.hasAttribute('nomodule') || !support) { if (e.target === check) {
return; support = true;
} } else if (!e.target.hasAttribute('nomodule') || !support) {
e.preventDefault(); return;
}, true); }
e.preventDefault();
},
true
);
check.type = 'module'; check.type = 'module';
check.src = '.'; check.src = '.';
document.head.appendChild(check); document.head.appendChild(check);
check.remove(); check.remove();
} }
}()); })();

View File

@ -8,70 +8,92 @@
import { LicenseWebpackPlugin } from 'license-webpack-plugin'; import { LicenseWebpackPlugin } from 'license-webpack-plugin';
import * as webpack from 'webpack'; import * as webpack from 'webpack';
import { WebpackConfigOptions } from '../build-options'; import { WebpackConfigOptions } from '../build-options';
import { getSourceMapDevTool, isPolyfillsEntry, normalizeExtraEntryPoints } from './utils'; import {
getSourceMapDevTool,
isPolyfillsEntry,
normalizeExtraEntryPoints
} from './utils';
const SubresourceIntegrityPlugin = require('webpack-subresource-integrity'); const SubresourceIntegrityPlugin = require('webpack-subresource-integrity');
export function getBrowserConfig(
export function getBrowserConfig(wco: WebpackConfigOptions): webpack.Configuration { wco: WebpackConfigOptions
): webpack.Configuration {
const { buildOptions } = wco; const { buildOptions } = wco;
const extraPlugins = []; const extraPlugins = [];
let isEval = false; let isEval = false;
const { styles: stylesOptimization, scripts: scriptsOptimization } = buildOptions.optimization; const {
styles: stylesOptimization,
scripts: scriptsOptimization
} = buildOptions.optimization;
const { const {
styles: stylesSourceMap, styles: stylesSourceMap,
scripts: scriptsSourceMap, scripts: scriptsSourceMap,
hidden: hiddenSourceMap, hidden: hiddenSourceMap
} = buildOptions.sourceMap; } = buildOptions.sourceMap;
// See https://webpack.js.org/configuration/devtool/ for sourcemap types. // See https://webpack.js.org/configuration/devtool/ for sourcemap types.
if ((stylesSourceMap || scriptsSourceMap) && if (
(stylesSourceMap || scriptsSourceMap) &&
buildOptions.evalSourceMap && buildOptions.evalSourceMap &&
!stylesOptimization && !stylesOptimization &&
!scriptsOptimization) { !scriptsOptimization
) {
// Produce eval sourcemaps for development with serve, which are faster. // Produce eval sourcemaps for development with serve, which are faster.
isEval = true; isEval = true;
} }
if (buildOptions.subresourceIntegrity) { if (buildOptions.subresourceIntegrity) {
extraPlugins.push(new SubresourceIntegrityPlugin({ extraPlugins.push(
hashFuncNames: ['sha384'], new SubresourceIntegrityPlugin({
})); hashFuncNames: ['sha384']
})
);
} }
if (buildOptions.extractLicenses) { if (buildOptions.extractLicenses) {
extraPlugins.push(new LicenseWebpackPlugin({ extraPlugins.push(
stats: { new LicenseWebpackPlugin({
warnings: false, stats: {
errors: false, warnings: false,
}, errors: false
perChunkOutput: false, },
outputFilename: `3rdpartylicenses.txt`, perChunkOutput: false,
})); outputFilename: `3rdpartylicenses.txt`
})
);
} }
if (!isEval && (scriptsSourceMap || stylesSourceMap)) { if (!isEval && (scriptsSourceMap || stylesSourceMap)) {
extraPlugins.push(getSourceMapDevTool( extraPlugins.push(
!!scriptsSourceMap, getSourceMapDevTool(
!!stylesSourceMap, !!scriptsSourceMap,
hiddenSourceMap, !!stylesSourceMap,
)); hiddenSourceMap
)
);
} }
const globalStylesBundleNames = normalizeExtraEntryPoints(buildOptions.styles, 'styles') const globalStylesBundleNames = normalizeExtraEntryPoints(
.map(style => style.bundleName); buildOptions.styles,
'styles'
).map(style => style.bundleName);
return { return {
devtool: isEval ? 'eval' : false, devtool: isEval ? 'eval' : false,
resolve: { resolve: {
mainFields: [ mainFields: [
...(wco.supportES2015 ? ['es2015'] : []), ...(wco.supportES2015 ? ['es2015'] : []),
'browser', 'module', 'main', 'browser',
], 'module',
'main'
]
}, },
output: { output: {
crossOriginLoading: buildOptions.subresourceIntegrity ? 'anonymous' : false, crossOriginLoading: buildOptions.subresourceIntegrity
? 'anonymous'
: false
}, },
optimization: { optimization: {
runtimeChunk: 'single', runtimeChunk: 'single',
@ -81,32 +103,42 @@ export function getBrowserConfig(wco: WebpackConfigOptions): webpack.Configurati
default: !!buildOptions.commonChunk && { default: !!buildOptions.commonChunk && {
chunks: 'async', chunks: 'async',
minChunks: 2, minChunks: 2,
priority: 10, priority: 10
}, },
common: !!buildOptions.commonChunk && { common: !!buildOptions.commonChunk && {
name: 'common', name: 'common',
chunks: 'async', chunks: 'async',
minChunks: 2, minChunks: 2,
enforce: true, enforce: true,
priority: 5, priority: 5
}, },
vendors: false, vendors: false,
vendor: !!buildOptions.vendorChunk && { vendor: !!buildOptions.vendorChunk && {
name: 'vendor', name: 'vendor',
chunks: 'initial', chunks: 'initial',
enforce: true, enforce: true,
test: (module: { nameForCondition?: Function }, chunks: Array<{ name: string }>) => { test: (
const moduleName = module.nameForCondition ? module.nameForCondition() : ''; module: { nameForCondition?: Function },
chunks: Array<{ name: string }>
) => {
const moduleName = module.nameForCondition
? module.nameForCondition()
: '';
return /[\\/]node_modules[\\/]/.test(moduleName) return (
&& !chunks.some(({ name }) => isPolyfillsEntry(name) /[\\/]node_modules[\\/]/.test(moduleName) &&
|| globalStylesBundleNames.includes(name)); !chunks.some(
}, ({ name }) =>
}, isPolyfillsEntry(name) ||
}, globalStylesBundleNames.includes(name)
}, )
);
}
}
}
}
}, },
plugins: extraPlugins, plugins: extraPlugins,
node: false, node: false
}; };
} }

View File

@ -7,7 +7,7 @@
*/ */
import { import {
BuildOptimizerWebpackPlugin, BuildOptimizerWebpackPlugin,
buildOptimizerLoaderPath, buildOptimizerLoaderPath
} from '@angular-devkit/build-optimizer'; } from '@angular-devkit/build-optimizer';
import { tags } from '@angular-devkit/core'; import { tags } from '@angular-devkit/core';
import * as CopyWebpackPlugin from 'copy-webpack-plugin'; import * as CopyWebpackPlugin from 'copy-webpack-plugin';
@ -19,7 +19,7 @@ import {
ContextReplacementPlugin, ContextReplacementPlugin,
HashedModuleIdsPlugin, HashedModuleIdsPlugin,
compilation, compilation,
debug, debug
} from 'webpack'; } from 'webpack';
import { RawSource } from 'webpack-sources'; import { RawSource } from 'webpack-sources';
import { AssetPatternClass, ExtraEntryPoint } from '../../../browser/schema'; import { AssetPatternClass, ExtraEntryPoint } from '../../../browser/schema';
@ -31,7 +31,11 @@ import { NamedLazyChunksPlugin } from '../../plugins/named-chunks-plugin';
import { ScriptsWebpackPlugin } from '../../plugins/scripts-webpack-plugin'; import { ScriptsWebpackPlugin } from '../../plugins/scripts-webpack-plugin';
import { findAllNodeModules, findUp } from '../../utilities/find-up'; import { findAllNodeModules, findUp } from '../../utilities/find-up';
import { WebpackConfigOptions } from '../build-options'; import { WebpackConfigOptions } from '../build-options';
import { getEsVersionForFileName, getOutputHashFormat, normalizeExtraEntryPoints } from './utils'; import {
getEsVersionForFileName,
getOutputHashFormat,
normalizeExtraEntryPoints
} from './utils';
const ProgressPlugin = require('webpack/lib/ProgressPlugin'); const ProgressPlugin = require('webpack/lib/ProgressPlugin');
const CircularDependencyPlugin = require('circular-dependency-plugin'); const CircularDependencyPlugin = require('circular-dependency-plugin');
@ -43,11 +47,14 @@ const g: any = typeof global !== 'undefined' ? global : {};
// tslint:disable-next-line:no-big-function // tslint:disable-next-line:no-big-function
export function getCommonConfig(wco: WebpackConfigOptions): Configuration { export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
const { root, projectRoot, buildOptions, tsConfig } = wco; const { root, projectRoot, buildOptions, tsConfig } = wco;
const { styles: stylesOptimization, scripts: scriptsOptimization } = buildOptions.optimization; const {
styles: stylesOptimization,
scripts: scriptsOptimization
} = buildOptions.optimization;
const { const {
styles: stylesSourceMap, styles: stylesSourceMap,
scripts: scriptsSourceMap, scripts: scriptsSourceMap,
vendor: vendorSourceMap, vendor: vendorSourceMap
} = buildOptions.sourceMap; } = buildOptions.sourceMap;
const nodeModules = findUp('node_modules', projectRoot); const nodeModules = findUp('node_modules', projectRoot);
@ -60,8 +67,10 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
const entryPoints: { [key: string]: string[] } = {}; const entryPoints: { [key: string]: string[] } = {};
const targetInFileName = getEsVersionForFileName( const targetInFileName = getEsVersionForFileName(
fullDifferential ? buildOptions.scriptTargetOverride : tsConfig.options.target, fullDifferential
buildOptions.esVersionInFileName, ? buildOptions.scriptTargetOverride
: tsConfig.options.target,
buildOptions.esVersionInFileName
); );
if (buildOptions.main) { if (buildOptions.main) {
@ -72,15 +81,19 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
if (wco.buildOptions.platform !== 'server') { if (wco.buildOptions.platform !== 'server') {
const buildBrowserFeatures = new BuildBrowserFeatures( const buildBrowserFeatures = new BuildBrowserFeatures(
projectRoot, projectRoot,
tsConfig.options.target || ScriptTarget.ES5, tsConfig.options.target || ScriptTarget.ES5
); );
differentialLoadingNeeded = buildBrowserFeatures.isDifferentialLoadingNeeded(); differentialLoadingNeeded = buildBrowserFeatures.isDifferentialLoadingNeeded();
if ((buildOptions.scriptTargetOverride || tsConfig.options.target) === ScriptTarget.ES5) { if (
(buildOptions.scriptTargetOverride || tsConfig.options.target) ===
ScriptTarget.ES5
) {
if ( if (
buildOptions.es5BrowserSupport || buildOptions.es5BrowserSupport ||
(buildOptions.es5BrowserSupport === undefined && buildBrowserFeatures.isEs5SupportNeeded()) (buildOptions.es5BrowserSupport === undefined &&
buildBrowserFeatures.isEs5SupportNeeded())
) { ) {
// The nomodule polyfill needs to be inject prior to any script and be // The nomodule polyfill needs to be inject prior to any script and be
// outside of webpack compilation because otherwise webpack will cause the // outside of webpack compilation because otherwise webpack will cause the
@ -88,7 +101,7 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
if (buildBrowserFeatures.isNoModulePolyfillNeeded()) { if (buildBrowserFeatures.isNoModulePolyfillNeeded()) {
const noModuleScript: ExtraEntryPoint = { const noModuleScript: ExtraEntryPoint = {
bundleName: 'polyfills-nomodule-es5', bundleName: 'polyfills-nomodule-es5',
input: path.join(__dirname, '..', 'safari-nomodule.js'), input: path.join(__dirname, '..', 'safari-nomodule.js')
}; };
buildOptions.scripts = buildOptions.scripts buildOptions.scripts = buildOptions.scripts
? [...buildOptions.scripts, noModuleScript] ? [...buildOptions.scripts, noModuleScript]
@ -98,9 +111,13 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
// For full build differential loading we don't need to generate a seperate polyfill file // For full build differential loading we don't need to generate a seperate polyfill file
// because they will be loaded exclusivly based on module and nomodule // because they will be loaded exclusivly based on module and nomodule
const polyfillsChunkName = const polyfillsChunkName =
fullDifferential && differentialLoadingNeeded ? 'polyfills' : 'polyfills-es5'; fullDifferential && differentialLoadingNeeded
? 'polyfills'
: 'polyfills-es5';
entryPoints[polyfillsChunkName] = [path.join(__dirname, '..', 'es5-polyfills.js')]; entryPoints[polyfillsChunkName] = [
path.join(__dirname, '..', 'es5-polyfills.js')
];
if (!fullDifferential && differentialLoadingNeeded) { if (!fullDifferential && differentialLoadingNeeded) {
// Add zone.js legacy support to the es5 polyfills // Add zone.js legacy support to the es5 polyfills
// This is a noop execution-wise if zone-evergreen is not used. // This is a noop execution-wise if zone-evergreen is not used.
@ -109,13 +126,19 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
if (!buildOptions.aot) { if (!buildOptions.aot) {
// If not performing a full differential build the JIT polyfills need to be added to ES5 // If not performing a full differential build the JIT polyfills need to be added to ES5
if (!fullDifferential && differentialLoadingNeeded) { if (!fullDifferential && differentialLoadingNeeded) {
entryPoints[polyfillsChunkName].push(path.join(__dirname, '..', 'jit-polyfills.js')); entryPoints[polyfillsChunkName].push(
path.join(__dirname, '..', 'jit-polyfills.js')
);
} }
entryPoints[polyfillsChunkName].push(path.join(__dirname, '..', 'es5-jit-polyfills.js')); entryPoints[polyfillsChunkName].push(
path.join(__dirname, '..', 'es5-jit-polyfills.js')
);
} }
// If not performing a full differential build the polyfills need to be added to ES5 bundle // If not performing a full differential build the polyfills need to be added to ES5 bundle
if (!fullDifferential && buildOptions.polyfills) { if (!fullDifferential && buildOptions.polyfills) {
entryPoints[polyfillsChunkName].push(path.resolve(root, buildOptions.polyfills)); entryPoints[polyfillsChunkName].push(
path.resolve(root, buildOptions.polyfills)
);
} }
} }
} }
@ -123,14 +146,14 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
if (buildOptions.polyfills) { if (buildOptions.polyfills) {
entryPoints['polyfills'] = [ entryPoints['polyfills'] = [
...(entryPoints['polyfills'] || []), ...(entryPoints['polyfills'] || []),
path.resolve(root, buildOptions.polyfills), path.resolve(root, buildOptions.polyfills)
]; ];
} }
if (!buildOptions.aot) { if (!buildOptions.aot) {
entryPoints['polyfills'] = [ entryPoints['polyfills'] = [
...(entryPoints['polyfills'] || []), ...(entryPoints['polyfills'] || []),
path.join(__dirname, '..', 'jit-polyfills.js'), path.join(__dirname, '..', 'jit-polyfills.js')
]; ];
} }
} }
@ -138,8 +161,11 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
if (buildOptions.profile || process.env['NG_BUILD_PROFILING']) { if (buildOptions.profile || process.env['NG_BUILD_PROFILING']) {
extraPlugins.push( extraPlugins.push(
new debug.ProfilingPlugin({ new debug.ProfilingPlugin({
outputPath: path.resolve(root, `chrome-profiler-events${targetInFileName}.json`), outputPath: path.resolve(
}), root,
`chrome-profiler-events${targetInFileName}.json`
)
})
); );
} }
@ -149,30 +175,36 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
// process global scripts // process global scripts
const globalScriptsByBundleName = normalizeExtraEntryPoints( const globalScriptsByBundleName = normalizeExtraEntryPoints(
buildOptions.scripts, buildOptions.scripts,
'scripts', 'scripts'
).reduce((prev: { bundleName: string; paths: string[]; inject: boolean }[], curr) => { ).reduce(
const bundleName = curr.bundleName; (
const resolvedPath = path.resolve(root, curr.input); prev: { bundleName: string; paths: string[]; inject: boolean }[],
const existingEntry = prev.find(el => el.bundleName === bundleName); curr
if (existingEntry) { ) => {
if (existingEntry.inject && !curr.inject) { const bundleName = curr.bundleName;
// All entries have to be lazy for the bundle to be lazy. const resolvedPath = path.resolve(root, curr.input);
throw new Error( const existingEntry = prev.find(el => el.bundleName === bundleName);
`The ${curr.bundleName} bundle is mixing injected and non-injected scripts.`, if (existingEntry) {
); if (existingEntry.inject && !curr.inject) {
// All entries have to be lazy for the bundle to be lazy.
throw new Error(
`The ${curr.bundleName} bundle is mixing injected and non-injected scripts.`
);
}
existingEntry.paths.push(resolvedPath);
} else {
prev.push({
bundleName,
paths: [resolvedPath],
inject: curr.inject
});
} }
existingEntry.paths.push(resolvedPath); return prev;
} else { },
prev.push({ []
bundleName, );
paths: [resolvedPath],
inject: curr.inject,
});
}
return prev;
}, []);
if (globalScriptsByBundleName.length > 0) { if (globalScriptsByBundleName.length > 0) {
// Add a new asset for each entry. // Add a new asset for each entry.
@ -187,42 +219,51 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
sourceMap: scriptsSourceMap, sourceMap: scriptsSourceMap,
filename: `${path.basename(bundleName)}${hash}.js`, filename: `${path.basename(bundleName)}${hash}.js`,
scripts: script.paths, scripts: script.paths,
basePath: projectRoot, basePath: projectRoot
}), })
); );
}); });
} }
// process asset entries // process asset entries
if (buildOptions.assets) { if (buildOptions.assets) {
const copyWebpackPluginPatterns = buildOptions.assets.map((asset: AssetPatternClass) => { const copyWebpackPluginPatterns = buildOptions.assets.map(
// Resolve input paths relative to workspace root and add slash at the end. (asset: AssetPatternClass) => {
asset.input = path.resolve(root, asset.input).replace(/\\/g, '/'); // Resolve input paths relative to workspace root and add slash at the end.
asset.input = asset.input.endsWith('/') ? asset.input : asset.input + '/'; asset.input = path.resolve(root, asset.input).replace(/\\/g, '/');
asset.output = asset.output.endsWith('/') ? asset.output : asset.output + '/'; asset.input = asset.input.endsWith('/')
? asset.input
: asset.input + '/';
asset.output = asset.output.endsWith('/')
? asset.output
: asset.output + '/';
if (asset.output.startsWith('..')) { if (asset.output.startsWith('..')) {
const message = 'An asset cannot be written to a location outside of the output path.'; const message =
throw new Error(message); 'An asset cannot be written to a location outside of the output path.';
throw new Error(message);
}
return {
context: asset.input,
// Now we remove starting slash to make Webpack place it from the output root.
to: asset.output.replace(/^\//, ''),
ignore: asset.ignore,
from: {
glob: asset.glob,
dot: true
}
};
} }
);
return { const copyWebpackPluginOptions = {
context: asset.input, ignore: ['.gitkeep', '**/.DS_Store', '**/Thumbs.db']
// Now we remove starting slash to make Webpack place it from the output root. };
to: asset.output.replace(/^\//, ''),
ignore: asset.ignore,
from: {
glob: asset.glob,
dot: true,
},
};
});
const copyWebpackPluginOptions = { ignore: ['.gitkeep', '**/.DS_Store', '**/Thumbs.db'] };
const copyWebpackPluginInstance = new CopyWebpackPlugin( const copyWebpackPluginInstance = new CopyWebpackPlugin(
copyWebpackPluginPatterns, copyWebpackPluginPatterns,
copyWebpackPluginOptions, copyWebpackPluginOptions
); );
extraPlugins.push(copyWebpackPluginInstance); extraPlugins.push(copyWebpackPluginInstance);
} }
@ -234,8 +275,8 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
if (buildOptions.showCircularDependencies) { if (buildOptions.showCircularDependencies) {
extraPlugins.push( extraPlugins.push(
new CircularDependencyPlugin({ new CircularDependencyPlugin({
exclude: /([\\\/]node_modules[\\\/])|(ngfactory\.js$)/, exclude: /([\\\/]node_modules[\\\/])|(ngfactory\.js$)/
}), })
); );
} }
@ -244,11 +285,15 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
new (class { new (class {
apply(compiler: Compiler) { apply(compiler: Compiler) {
compiler.hooks.emit.tap('angular-cli-stats', compilation => { compiler.hooks.emit.tap('angular-cli-stats', compilation => {
const data = JSON.stringify(compilation.getStats().toJson('verbose')); const data = JSON.stringify(
compilation.assets[`stats${targetInFileName}.json`] = new RawSource(data); compilation.getStats().toJson('verbose')
);
compilation.assets[`stats${targetInFileName}.json`] = new RawSource(
data
);
}); });
} }
})(), })()
); );
} }
@ -261,9 +306,9 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
sourceMapUseRule = { sourceMapUseRule = {
use: [ use: [
{ {
loader: 'source-map-loader', loader: 'source-map-loader'
}, }
], ]
}; };
} }
@ -274,9 +319,9 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
use: [ use: [
{ {
loader: buildOptimizerLoaderPath, loader: buildOptimizerLoaderPath,
options: { sourceMap: scriptsSourceMap }, options: { sourceMap: scriptsSourceMap }
}, }
], ]
}; };
} }
@ -293,7 +338,9 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
const rxjsPathMappingImport = wco.supportES2015 const rxjsPathMappingImport = wco.supportES2015
? 'rxjs/_esm2015/path-mapping' ? 'rxjs/_esm2015/path-mapping'
: 'rxjs/_esm5/path-mapping'; : 'rxjs/_esm5/path-mapping';
const rxPaths = require(require.resolve(rxjsPathMappingImport, { paths: [projectRoot] })); const rxPaths = require(require.resolve(rxjsPathMappingImport, {
paths: [projectRoot]
}));
alias = rxPaths(nodeModules); alias = rxPaths(nodeModules);
} catch {} } catch {}
@ -303,19 +350,20 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
new CleanCssWebpackPlugin({ new CleanCssWebpackPlugin({
sourceMap: stylesSourceMap, sourceMap: stylesSourceMap,
// component styles retain their original file name // component styles retain their original file name
test: file => /\.(?:css|scss|sass|less|styl)$/.test(file), test: file => /\.(?:css|scss|sass|less|styl)$/.test(file)
}), })
); );
} }
if (scriptsOptimization) { if (scriptsOptimization) {
let angularGlobalDefinitions = { let angularGlobalDefinitions = {
ngDevMode: false, ngDevMode: false,
ngI18nClosureMode: false, ngI18nClosureMode: false
}; };
// Try to load known global definitions from @angular/compiler-cli. // Try to load known global definitions from @angular/compiler-cli.
const GLOBAL_DEFS_FOR_TERSER = require('@angular/compiler-cli').GLOBAL_DEFS_FOR_TERSER; const GLOBAL_DEFS_FOR_TERSER = require('@angular/compiler-cli')
.GLOBAL_DEFS_FOR_TERSER;
if (GLOBAL_DEFS_FOR_TERSER) { if (GLOBAL_DEFS_FOR_TERSER) {
angularGlobalDefinitions = GLOBAL_DEFS_FOR_TERSER; angularGlobalDefinitions = GLOBAL_DEFS_FOR_TERSER;
} }
@ -327,7 +375,7 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
if (GLOBAL_DEFS_FOR_TERSER_WITH_AOT) { if (GLOBAL_DEFS_FOR_TERSER_WITH_AOT) {
angularGlobalDefinitions = { angularGlobalDefinitions = {
...angularGlobalDefinitions, ...angularGlobalDefinitions,
...GLOBAL_DEFS_FOR_TERSER_WITH_AOT, ...GLOBAL_DEFS_FOR_TERSER_WITH_AOT
}; };
} }
} }
@ -341,7 +389,7 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
output: { output: {
ecma: terserEcma, ecma: terserEcma,
comments: false, comments: false,
webkit: true, webkit: true
}, },
// On server, we don't want to compress anything. We still set the ngDevMode = false for it // On server, we don't want to compress anything. We still set the ngDevMode = false for it
// to remove dev code, and ngI18nClosureMode to remove Closure compiler i18n code // to remove dev code, and ngI18nClosureMode to remove Closure compiler i18n code
@ -350,7 +398,7 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
? { ? {
ecma: terserEcma, ecma: terserEcma,
global_defs: angularGlobalDefinitions, global_defs: angularGlobalDefinitions,
keep_fnames: true, keep_fnames: true
} }
: { : {
ecma: terserEcma, ecma: terserEcma,
@ -358,14 +406,15 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
// PURE comments work best with 3 passes. // PURE comments work best with 3 passes.
// See https://github.com/webpack/webpack/issues/2899#issuecomment-317425926. // See https://github.com/webpack/webpack/issues/2899#issuecomment-317425926.
passes: buildOptions.buildOptimizer ? 3 : 1, passes: buildOptions.buildOptimizer ? 3 : 1,
global_defs: angularGlobalDefinitions, global_defs: angularGlobalDefinitions
}, },
// We also want to avoid mangling on server. // We also want to avoid mangling on server.
// Name mangling is handled within the browser builder // Name mangling is handled within the browser builder
mangle: mangle:
!manglingDisabled && !manglingDisabled &&
buildOptions.platform !== 'server' && buildOptions.platform !== 'server' &&
(!differentialLoadingNeeded || (differentialLoadingNeeded && fullDifferential)), (!differentialLoadingNeeded ||
(differentialLoadingNeeded && fullDifferential))
}; };
extraMinimizers.push( extraMinimizers.push(
@ -375,7 +424,7 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
cache: true, cache: true,
chunkFilter: (chunk: compilation.Chunk) => chunkFilter: (chunk: compilation.Chunk) =>
!globalScriptsByBundleName.some(s => s.bundleName === chunk.name), !globalScriptsByBundleName.some(s => s.bundleName === chunk.name),
terserOptions, terserOptions
}), }),
// Script bundles are fully optimized here in one step since they are never downleveled. // Script bundles are fully optimized here in one step since they are never downleveled.
// They are shared between ES2015 & ES5 outputs so must support ES5. // They are shared between ES2015 & ES5 outputs so must support ES5.
@ -389,15 +438,15 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
...terserOptions, ...terserOptions,
compress: { compress: {
...terserOptions.compress, ...terserOptions.compress,
ecma: 5, ecma: 5
}, },
output: { output: {
...terserOptions.output, ...terserOptions.output,
ecma: 5, ecma: 5
}, },
mangle: !manglingDisabled && buildOptions.platform !== 'server', mangle: !manglingDisabled && buildOptions.platform !== 'server'
}, }
}), })
); );
} }
@ -413,17 +462,18 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
} }
return { return {
mode: scriptsOptimization || stylesOptimization ? 'production' : 'development', mode:
scriptsOptimization || stylesOptimization ? 'production' : 'development',
devtool: false, devtool: false,
profile: buildOptions.statsJson, profile: buildOptions.statsJson,
resolve: { resolve: {
extensions: ['.ts', '.tsx', '.mjs', '.js'], extensions: ['.ts', '.tsx', '.mjs', '.js'],
symlinks: !buildOptions.preserveSymlinks, symlinks: !buildOptions.preserveSymlinks,
modules: [wco.tsConfig.options.baseUrl || projectRoot, 'node_modules'], modules: [wco.tsConfig.options.baseUrl || projectRoot, 'node_modules'],
alias, alias
}, },
resolveLoader: { resolveLoader: {
modules: loaderNodeModules, modules: loaderNodeModules
}, },
context: projectRoot, context: projectRoot,
entry: entryPoints, entry: entryPoints,
@ -431,14 +481,14 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
futureEmitAssets: true, futureEmitAssets: true,
path: path.resolve(root, buildOptions.outputPath as string), path: path.resolve(root, buildOptions.outputPath as string),
publicPath: buildOptions.deployUrl, publicPath: buildOptions.deployUrl,
filename: `[name]${targetInFileName}${hashFormat.chunk}.js`, filename: `[name]${targetInFileName}${hashFormat.chunk}.js`
}, },
watch: buildOptions.watch, watch: buildOptions.watch,
watchOptions: { watchOptions: {
poll: buildOptions.poll, poll: buildOptions.poll
}, },
performance: { performance: {
hints: false, hints: false
}, },
module: { module: {
// Show an error for missing exports instead of a warning. // Show an error for missing exports instead of a warning.
@ -448,36 +498,36 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
test: /\.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)$/, test: /\.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)$/,
loader: 'file-loader', loader: 'file-loader',
options: { options: {
name: `[name]${hashFormat.file}.[ext]`, name: `[name]${hashFormat.file}.[ext]`
}, }
}, },
{ {
// Mark files inside `@angular/core` as using SystemJS style dynamic imports. // Mark files inside `@angular/core` as using SystemJS style dynamic imports.
// Removing this will cause deprecation warnings to appear. // Removing this will cause deprecation warnings to appear.
test: /[\/\\]@angular[\/\\]core[\/\\].+\.js$/, test: /[\/\\]@angular[\/\\]core[\/\\].+\.js$/,
parser: { system: true }, parser: { system: true }
}, },
{ {
test: /[\/\\]hot[\/\\]emitter\.js$/, test: /[\/\\]hot[\/\\]emitter\.js$/,
parser: { node: { events: true } }, parser: { node: { events: true } }
}, },
{ {
test: /[\/\\]webpack-dev-server[\/\\]client[\/\\]utils[\/\\]createSocketUrl\.js$/, test: /[\/\\]webpack-dev-server[\/\\]client[\/\\]utils[\/\\]createSocketUrl\.js$/,
parser: { node: { querystring: true } }, parser: { node: { querystring: true } }
}, },
{ {
test: /\.js$/, test: /\.js$/,
// Factory files are processed by BO in the rules added in typescript.ts. // Factory files are processed by BO in the rules added in typescript.ts.
exclude: /(ngfactory|ngstyle)\.js$/, exclude: /(ngfactory|ngstyle)\.js$/,
...buildOptimizerUseRule, ...buildOptimizerUseRule
}, },
{ {
test: /\.js$/, test: /\.js$/,
exclude: /(ngfactory|ngstyle)\.js$/, exclude: /(ngfactory|ngstyle)\.js$/,
enforce: 'pre', enforce: 'pre',
...sourceMapUseRule, ...sourceMapUseRule
}, }
], ]
}, },
optimization: { optimization: {
noEmitOnErrors: true, noEmitOnErrors: true,
@ -485,15 +535,15 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
new HashedModuleIdsPlugin(), new HashedModuleIdsPlugin(),
// TODO: check with Mike what this feature needs. // TODO: check with Mike what this feature needs.
new BundleBudgetPlugin({ budgets: buildOptions.budgets }), new BundleBudgetPlugin({ budgets: buildOptions.budgets }),
...extraMinimizers, ...extraMinimizers
], ]
}, },
plugins: [ plugins: [
// Always replace the context for the System.import in angular/core to prevent warnings. // Always replace the context for the System.import in angular/core to prevent warnings.
// https://github.com/angular/angular/issues/11580 // https://github.com/angular/angular/issues/11580
// With VE the correct context is added in @ngtools/webpack, but Ivy doesn't need it at all. // With VE the correct context is added in @ngtools/webpack, but Ivy doesn't need it at all.
new ContextReplacementPlugin(/\@angular(\\|\/)core(\\|\/)/), new ContextReplacementPlugin(/\@angular(\\|\/)core(\\|\/)/),
...extraPlugins, ...extraPlugins
], ]
}; };
} }

View File

@ -19,25 +19,31 @@ export function getServerConfig(wco: WebpackConfigOptions): Configuration {
if (wco.buildOptions.sourceMap) { if (wco.buildOptions.sourceMap) {
const { scripts, styles, hidden } = wco.buildOptions.sourceMap; const { scripts, styles, hidden } = wco.buildOptions.sourceMap;
extraPlugins.push(getSourceMapDevTool(scripts || false, styles || false, hidden || false)); extraPlugins.push(
getSourceMapDevTool(scripts || false, styles || false, hidden || false)
);
} }
const config: Configuration = { const config: Configuration = {
resolve: { resolve: {
mainFields: [...(wco.supportES2015 ? ['es2015'] : []), 'main', 'module'], mainFields: [...(wco.supportES2015 ? ['es2015'] : []), 'main', 'module']
}, },
target: 'node', target: 'node',
output: { output: {
libraryTarget: 'commonjs', libraryTarget: 'commonjs'
}, },
plugins: extraPlugins, plugins: extraPlugins,
node: false, node: false
}; };
if (wco.buildOptions.bundleDependencies == 'none') { if (wco.buildOptions.bundleDependencies == 'none') {
config.externals = [ config.externals = [
/^@angular/, /^@angular/,
(context: string, request: string, callback: (error?: null, result?: string) => void) => { (
context: string,
request: string,
callback: (error?: null, result?: string) => void
) => {
// Absolute & Relative paths are not externals // Absolute & Relative paths are not externals
if (/^\.{0,2}\//.test(request) || isAbsolute(request)) { if (/^\.{0,2}\//.test(request) || isAbsolute(request)) {
return callback(); return callback();
@ -50,7 +56,7 @@ export function getServerConfig(wco: WebpackConfigOptions): Configuration {
// Node couldn't find it, so it must be user-aliased // Node couldn't find it, so it must be user-aliased
callback(); callback();
} }
}, }
]; ];
} }

View File

@ -22,7 +22,7 @@ const webpackOutputOptions = {
assets: true, // required by custom stat output assets: true, // required by custom stat output
version: false, version: false,
errorDetails: false, errorDetails: false,
moduleTrace: false, moduleTrace: false
}; };
const verboseWebpackOutputOptions = { const verboseWebpackOutputOptions = {
@ -37,7 +37,7 @@ const verboseWebpackOutputOptions = {
version: true, version: true,
chunkModules: true, chunkModules: true,
errorDetails: true, errorDetails: true,
moduleTrace: true, moduleTrace: true
}; };
export function getWebpackStatsConfig(verbose = false) { export function getWebpackStatsConfig(verbose = false) {

View File

@ -12,7 +12,7 @@ import {
PostcssCliResources, PostcssCliResources,
RawCssLoader, RawCssLoader,
RemoveHashPlugin, RemoveHashPlugin,
SuppressExtractedTextChunksWebpackPlugin, SuppressExtractedTextChunksWebpackPlugin
} from '../../plugins/webpack'; } from '../../plugins/webpack';
import { WebpackConfigOptions } from '../build-options'; import { WebpackConfigOptions } from '../build-options';
import { getOutputHashFormat, normalizeExtraEntryPoints } from './utils'; import { getOutputHashFormat, normalizeExtraEntryPoints } from './utils';
@ -63,7 +63,7 @@ export function getStylesConfig(wco: WebpackConfigOptions) {
resolve(content); resolve(content);
}); });
}); });
}, }
}), }),
PostcssCliResources({ PostcssCliResources({
baseHref: buildOptions.baseHref, baseHref: buildOptions.baseHref,
@ -71,9 +71,9 @@ export function getStylesConfig(wco: WebpackConfigOptions) {
resourcesOutputPath: buildOptions.resourcesOutputPath, resourcesOutputPath: buildOptions.resourcesOutputPath,
loader, loader,
rebaseRootRelative: buildOptions.rebaseRootRelativeCssUrls, rebaseRootRelative: buildOptions.rebaseRootRelativeCssUrls,
filename: `[name]${hashFormat.file}.[ext]`, filename: `[name]${hashFormat.file}.[ext]`
}), }),
autoprefixer(), autoprefixer()
]; ];
}; };
@ -86,11 +86,12 @@ export function getStylesConfig(wco: WebpackConfigOptions) {
buildOptions.stylePreprocessorOptions.includePaths && buildOptions.stylePreprocessorOptions.includePaths &&
buildOptions.stylePreprocessorOptions.includePaths.length > 0 buildOptions.stylePreprocessorOptions.includePaths.length > 0
) { ) {
buildOptions.stylePreprocessorOptions.includePaths.forEach((includePath: string) => buildOptions.stylePreprocessorOptions.includePaths.forEach(
includePaths.push(path.resolve(root, includePath)), (includePath: string) =>
includePaths.push(path.resolve(root, includePath))
); );
lessPathOptions = { lessPathOptions = {
paths: includePaths, paths: includePaths
}; };
} }
@ -150,10 +151,10 @@ export function getStylesConfig(wco: WebpackConfigOptions) {
sourceMap: cssSourceMap, sourceMap: cssSourceMap,
// bootstrap-sass requires a minimum precision of 8 // bootstrap-sass requires a minimum precision of 8
precision: 8, precision: 8,
includePaths, includePaths
}, }
}, }
], ]
}, },
{ {
test: /\.less$/, test: /\.less$/,
@ -163,10 +164,10 @@ export function getStylesConfig(wco: WebpackConfigOptions) {
options: { options: {
sourceMap: cssSourceMap, sourceMap: cssSourceMap,
javascriptEnabled: true, javascriptEnabled: true,
...lessPathOptions, ...lessPathOptions
}, }
}, }
], ]
}, },
{ {
test: /\.styl$/, test: /\.styl$/,
@ -175,11 +176,11 @@ export function getStylesConfig(wco: WebpackConfigOptions) {
loader: 'stylus-loader', loader: 'stylus-loader',
options: { options: {
sourceMap: cssSourceMap, sourceMap: cssSourceMap,
paths: includePaths, paths: includePaths
}, }
}, }
], ]
}, }
]; ];
// load component css as raw strings // load component css as raw strings
@ -193,17 +194,20 @@ export function getStylesConfig(wco: WebpackConfigOptions) {
options: { options: {
ident: 'embedded', ident: 'embedded',
plugins: postcssPluginCreator, plugins: postcssPluginCreator,
sourceMap: cssSourceMap sourceMap:
cssSourceMap &&
// Never use component css sourcemap when style optimizations are on. // Never use component css sourcemap when style optimizations are on.
// It will just increase bundle size without offering good debug experience. // It will just increase bundle size without offering good debug experience.
&& !buildOptions.optimization.styles !buildOptions.optimization.styles &&
// Inline all sourcemap types except hidden ones, which are the same as no sourcemaps // Inline all sourcemap types except hidden ones, which are the same as no sourcemaps
// for component css. // for component css.
&& !buildOptions.sourceMap.hidden ? 'inline' : false, !buildOptions.sourceMap.hidden
}, ? 'inline'
: false
}
}, },
...(use as webpack.Loader[]), ...(use as webpack.Loader[])
], ]
})); }));
// load global css as css files // load global css as css files
@ -214,7 +218,9 @@ export function getStylesConfig(wco: WebpackConfigOptions) {
include: globalStylePaths, include: globalStylePaths,
test, test,
use: [ use: [
buildOptions.extractCss ? MiniCssExtractPlugin.loader : 'style-loader', buildOptions.extractCss
? MiniCssExtractPlugin.loader
: 'style-loader',
RawCssLoader, RawCssLoader,
{ {
loader: 'postcss-loader', loader: 'postcss-loader',
@ -222,15 +228,17 @@ export function getStylesConfig(wco: WebpackConfigOptions) {
ident: buildOptions.extractCss ? 'extracted' : 'embedded', ident: buildOptions.extractCss ? 'extracted' : 'embedded',
plugins: postcssPluginCreator, plugins: postcssPluginCreator,
sourceMap: sourceMap:
cssSourceMap && !buildOptions.extractCss && !buildOptions.sourceMap.hidden cssSourceMap &&
!buildOptions.extractCss &&
!buildOptions.sourceMap.hidden
? 'inline' ? 'inline'
: cssSourceMap, : cssSourceMap
}, }
}, },
...(use as webpack.Loader[]), ...(use as webpack.Loader[])
], ]
}; };
}), })
); );
} }
@ -239,13 +247,13 @@ export function getStylesConfig(wco: WebpackConfigOptions) {
// extract global css from js files into own css file // extract global css from js files into own css file
new MiniCssExtractPlugin({ filename: `[name]${hashFormat.extract}.css` }), new MiniCssExtractPlugin({ filename: `[name]${hashFormat.extract}.css` }),
// suppress empty .js files in css only entry points // suppress empty .js files in css only entry points
new SuppressExtractedTextChunksWebpackPlugin(), new SuppressExtractedTextChunksWebpackPlugin()
); );
} }
return { return {
entry: entryPoints, entry: entryPoints,
module: { rules }, module: { rules },
plugins: extraPlugins, plugins: extraPlugins
}; };
} }

View File

@ -12,7 +12,6 @@ import * as webpack from 'webpack';
import { WebpackConfigOptions, WebpackTestOptions } from '../build-options'; import { WebpackConfigOptions, WebpackTestOptions } from '../build-options';
import { getSourceMapDevTool, isPolyfillsEntry } from './utils'; import { getSourceMapDevTool, isPolyfillsEntry } from './utils';
/** /**
* Enumerate loaders and their dependencies from this file to let the dependency validator * Enumerate loaders and their dependencies from this file to let the dependency validator
* know they are used. * know they are used.
@ -22,7 +21,7 @@ import { getSourceMapDevTool, isPolyfillsEntry } from './utils';
*/ */
export function getTestConfig( export function getTestConfig(
wco: WebpackConfigOptions<WebpackTestOptions>, wco: WebpackConfigOptions<WebpackTestOptions>
): webpack.Configuration { ): webpack.Configuration {
const { root, buildOptions, sourceRoot: include } = wco; const { root, buildOptions, sourceRoot: include } = wco;
@ -32,10 +31,7 @@ export function getTestConfig(
// if (buildOptions.codeCoverage && CliConfig.fromProject()) { // if (buildOptions.codeCoverage && CliConfig.fromProject()) {
if (buildOptions.codeCoverage) { if (buildOptions.codeCoverage) {
const codeCoverageExclude = buildOptions.codeCoverageExclude; const codeCoverageExclude = buildOptions.codeCoverageExclude;
const exclude: (string | RegExp)[] = [ const exclude: (string | RegExp)[] = [/\.(e2e|spec)\.ts$/, /node_modules/];
/\.(e2e|spec)\.ts$/,
/node_modules/,
];
if (codeCoverageExclude) { if (codeCoverageExclude) {
codeCoverageExclude.forEach((excludeGlob: string) => { codeCoverageExclude.forEach((excludeGlob: string) => {
@ -52,19 +48,16 @@ export function getTestConfig(
options: { esModules: true }, options: { esModules: true },
enforce: 'post', enforce: 'post',
exclude, exclude,
include, include
}); });
} }
if (wco.buildOptions.sourceMap) { if (wco.buildOptions.sourceMap) {
const { styles, scripts } = wco.buildOptions.sourceMap; const { styles, scripts } = wco.buildOptions.sourceMap;
extraPlugins.push(getSourceMapDevTool( extraPlugins.push(
scripts || false, getSourceMapDevTool(scripts || false, styles || false, false, true)
styles || false, );
false,
true,
));
} }
return { return {
@ -72,34 +65,43 @@ export function getTestConfig(
resolve: { resolve: {
mainFields: [ mainFields: [
...(wco.supportES2015 ? ['es2015'] : []), ...(wco.supportES2015 ? ['es2015'] : []),
'browser', 'module', 'main', 'browser',
], 'module',
'main'
]
}, },
devtool: buildOptions.sourceMap ? false : 'eval', devtool: buildOptions.sourceMap ? false : 'eval',
entry: { entry: {
main: path.resolve(root, buildOptions.main), main: path.resolve(root, buildOptions.main)
}, },
module: { module: {
rules: extraRules, rules: extraRules
}, },
plugins: extraPlugins, plugins: extraPlugins,
optimization: { optimization: {
splitChunks: { splitChunks: {
chunks: ((chunk: { name: string }) => !isPolyfillsEntry(chunk.name)), chunks: (chunk: { name: string }) => !isPolyfillsEntry(chunk.name),
cacheGroups: { cacheGroups: {
vendors: false, vendors: false,
vendor: { vendor: {
name: 'vendor', name: 'vendor',
chunks: 'initial', chunks: 'initial',
test: (module: { nameForCondition?: () => string }, chunks: { name: string }[]) => { test: (
const moduleName = module.nameForCondition ? module.nameForCondition() : ''; module: { nameForCondition?: () => string },
chunks: { name: string }[]
) => {
const moduleName = module.nameForCondition
? module.nameForCondition()
: '';
return /[\\/]node_modules[\\/]/.test(moduleName) return (
&& !chunks.some(({ name }) => isPolyfillsEntry(name)); /[\\/]node_modules[\\/]/.test(moduleName) &&
}, !chunks.some(({ name }) => isPolyfillsEntry(name))
}, );
}, }
}, }
}, }
}
}
}; };
} }

View File

@ -24,7 +24,7 @@ function _pluginOptionsOverrides(
): AngularCompilerPluginOptions { ): AngularCompilerPluginOptions {
const compilerOptions = { const compilerOptions = {
...(pluginOptions.compilerOptions || {}) ...(pluginOptions.compilerOptions || {})
} };
const hostReplacementPaths: { [replace: string]: string } = {}; const hostReplacementPaths: { [replace: string]: string } = {};
if (buildOptions.fileReplacements) { if (buildOptions.fileReplacements) {
@ -51,7 +51,7 @@ function _pluginOptionsOverrides(
function _createAotPlugin( function _createAotPlugin(
wco: WebpackConfigOptions, wco: WebpackConfigOptions,
options: AngularCompilerPluginOptions, options: AngularCompilerPluginOptions,
i18nExtract = false, i18nExtract = false
) { ) {
const { root, buildOptions } = wco; const { root, buildOptions } = wco;
@ -61,20 +61,18 @@ function _createAotPlugin(
const i18nFileAndFormat = i18nExtract const i18nFileAndFormat = i18nExtract
? { ? {
i18nOutFile: buildOptions.i18nFile, i18nOutFile: buildOptions.i18nFile,
i18nOutFormat: buildOptions.i18nFormat, i18nOutFormat: buildOptions.i18nFormat
} : { }
i18nInFile: i18nInFile, : {
i18nInFormat: buildOptions.i18nFormat, i18nInFile: i18nInFile,
}; i18nInFormat: buildOptions.i18nFormat
};
const additionalLazyModules: { [module: string]: string } = {}; const additionalLazyModules: { [module: string]: string } = {};
if (buildOptions.lazyModules) { if (buildOptions.lazyModules) {
for (const lazyModule of buildOptions.lazyModules) { for (const lazyModule of buildOptions.lazyModules) {
additionalLazyModules[lazyModule] = path.resolve( additionalLazyModules[lazyModule] = path.resolve(root, lazyModule);
root,
lazyModule,
);
} }
} }
@ -82,7 +80,8 @@ function _createAotPlugin(
mainPath: path.join(root, buildOptions.main), mainPath: path.join(root, buildOptions.main),
...i18nFileAndFormat, ...i18nFileAndFormat,
locale: buildOptions.i18nLocale, locale: buildOptions.i18nLocale,
platform: buildOptions.platform === 'server' ? PLATFORM.Server : PLATFORM.Browser, platform:
buildOptions.platform === 'server' ? PLATFORM.Server : PLATFORM.Browser,
missingTranslation: buildOptions.i18nMissingTranslation, missingTranslation: buildOptions.i18nMissingTranslation,
sourceMap: buildOptions.sourceMap.scripts, sourceMap: buildOptions.sourceMap.scripts,
additionalLazyModules, additionalLazyModules,
@ -91,7 +90,7 @@ function _createAotPlugin(
contextElementDependencyConstructor: require('webpack/lib/dependencies/ContextElementDependency'), contextElementDependencyConstructor: require('webpack/lib/dependencies/ContextElementDependency'),
logger: wco.logger, logger: wco.logger,
directTemplateLoading: true, directTemplateLoading: true,
...options, ...options
}; };
pluginOptions = _pluginOptionsOverrides(buildOptions, pluginOptions); pluginOptions = _pluginOptionsOverrides(buildOptions, pluginOptions);
@ -127,7 +126,10 @@ export function getAotConfig(wco: WebpackConfigOptions, i18nExtract = false) {
}; };
} }
export function getTypescriptWorkerPlugin(wco: WebpackConfigOptions, workerTsConfigPath: string) { export function getTypescriptWorkerPlugin(
wco: WebpackConfigOptions,
workerTsConfigPath: string
) {
const { buildOptions } = wco; const { buildOptions } = wco;
let pluginOptions: AngularCompilerPluginOptions = { let pluginOptions: AngularCompilerPluginOptions = {
@ -142,7 +144,7 @@ export function getTypescriptWorkerPlugin(wco: WebpackConfigOptions, workerTsCon
// Run no transformers. // Run no transformers.
platformTransformers: [], platformTransformers: [],
// Don't attempt lazy route discovery. // Don't attempt lazy route discovery.
discoverLazyRoutes: false, discoverLazyRoutes: false
}; };
pluginOptions = _pluginOptionsOverrides(buildOptions, pluginOptions); pluginOptions = _pluginOptionsOverrides(buildOptions, pluginOptions);

View File

@ -28,14 +28,14 @@ export function getOutputHashFormat(option: string, length = 20): HashFormat {
chunk: `.[chunkhash:${length}]`, chunk: `.[chunkhash:${length}]`,
extract: `.[contenthash:${length}]`, extract: `.[contenthash:${length}]`,
file: '', file: '',
script: `.[hash:${length}]`, script: `.[hash:${length}]`
}, },
all: { all: {
chunk: `.[chunkhash:${length}]`, chunk: `.[chunkhash:${length}]`,
extract: `.[contenthash:${length}]`, extract: `.[contenthash:${length}]`,
file: `.[hash:${length}]`, file: `.[hash:${length}]`,
script: `.[hash:${length}]`, script: `.[hash:${length}]`
}, }
}; };
return hashFormats[option] || hashFormats['none']; return hashFormats[option] || hashFormats['none'];
} }
@ -46,12 +46,16 @@ export type NormalizedEntryPoint = Required<Omit<ExtraEntryPointClass, 'lazy'>>;
export function normalizeExtraEntryPoints( export function normalizeExtraEntryPoints(
extraEntryPoints: ExtraEntryPoint[], extraEntryPoints: ExtraEntryPoint[],
defaultBundleName: string, defaultBundleName: string
): NormalizedEntryPoint[] { ): NormalizedEntryPoint[] {
return extraEntryPoints.map(entry => { return extraEntryPoints.map(entry => {
let normalizedEntry; let normalizedEntry;
if (typeof entry === 'string') { if (typeof entry === 'string') {
normalizedEntry = { input: entry, inject: true, bundleName: defaultBundleName }; normalizedEntry = {
input: entry,
inject: true,
bundleName: defaultBundleName
};
} else { } else {
const { lazy, inject = true, ...newEntry } = entry; const { lazy, inject = true, ...newEntry } = entry;
const injectNormalized = entry.lazy !== undefined ? !entry.lazy : inject; const injectNormalized = entry.lazy !== undefined ? !entry.lazy : inject;
@ -62,7 +66,7 @@ export function normalizeExtraEntryPoints(
} else if (!injectNormalized) { } else if (!injectNormalized) {
// Lazy entry points use the file name as bundle name. // Lazy entry points use the file name as bundle name.
bundleName = basename( bundleName = basename(
normalize(entry.input.replace(/\.(js|css|scss|sass|less|styl)$/i, '')), normalize(entry.input.replace(/\.(js|css|scss|sass|less|styl)$/i, ''))
); );
} else { } else {
bundleName = defaultBundleName; bundleName = defaultBundleName;
@ -79,7 +83,7 @@ export function getSourceMapDevTool(
scriptsSourceMap: boolean, scriptsSourceMap: boolean,
stylesSourceMap: boolean, stylesSourceMap: boolean,
hiddenSourceMap = false, hiddenSourceMap = false,
inlineSourceMap = false, inlineSourceMap = false
): SourceMapDevToolPlugin { ): SourceMapDevToolPlugin {
const include = []; const include = [];
if (scriptsSourceMap) { if (scriptsSourceMap) {
@ -99,7 +103,7 @@ export function getSourceMapDevTool(
// there is no way to set the 'webRoot' // there is no way to set the 'webRoot'
sourceRoot: inlineSourceMap ? '' : 'webpack:///', sourceRoot: inlineSourceMap ? '' : 'webpack:///',
moduleFilenameTemplate: '[resource-path]', moduleFilenameTemplate: '[resource-path]',
append: hiddenSourceMap ? false : undefined, append: hiddenSourceMap ? false : undefined
}); });
} }
@ -108,7 +112,7 @@ export function getSourceMapDevTool(
*/ */
export function getEsVersionForFileName( export function getEsVersionForFileName(
scriptTargetOverride: ScriptTarget | undefined, scriptTargetOverride: ScriptTarget | undefined,
esVersionInFileName = false, esVersionInFileName = false
): string { ): string {
return scriptTargetOverride && esVersionInFileName return scriptTargetOverride && esVersionInFileName
? '-' + ScriptTarget[scriptTargetOverride].toLowerCase() ? '-' + ScriptTarget[scriptTargetOverride].toLowerCase()

View File

@ -12,7 +12,6 @@ import { getTypescriptWorkerPlugin } from './typescript';
const WorkerPlugin = require('worker-plugin'); const WorkerPlugin = require('worker-plugin');
export function getWorkerConfig(wco: WebpackConfigOptions): Configuration { export function getWorkerConfig(wco: WebpackConfigOptions): Configuration {
const { buildOptions } = wco; const { buildOptions } = wco;
@ -27,9 +26,11 @@ export function getWorkerConfig(wco: WebpackConfigOptions): Configuration {
const workerTsConfigPath = resolve(wco.root, buildOptions.webWorkerTsConfig); const workerTsConfigPath = resolve(wco.root, buildOptions.webWorkerTsConfig);
return { return {
plugins: [new WorkerPlugin({ plugins: [
globalObject: false, new WorkerPlugin({
plugins: [getTypescriptWorkerPlugin(wco, workerTsConfigPath)], globalObject: false,
})], plugins: [getTypescriptWorkerPlugin(wco, workerTsConfigPath)]
})
]
}; };
} }

View File

@ -7,7 +7,11 @@
*/ */
import { Compiler, compilation } from 'webpack'; import { Compiler, compilation } from 'webpack';
import { Budget, Type } from '../../browser/schema'; import { Budget, Type } from '../../browser/schema';
import { Size, calculateBytes, calculateSizes } from '../utilities/bundle-calculator'; import {
Size,
calculateBytes,
calculateSizes
} from '../utilities/bundle-calculator';
import { formatSize } from '../utilities/stats'; import { formatSize } from '../utilities/stats';
interface Thresholds { interface Thresholds {
@ -26,7 +30,7 @@ export interface BundleBudgetPluginOptions {
} }
export class BundleBudgetPlugin { export class BundleBudgetPlugin {
constructor(private options: BundleBudgetPluginOptions) { } constructor(private options: BundleBudgetPluginOptions) {}
apply(compiler: Compiler): void { apply(compiler: Compiler): void {
const { budgets } = this.options; const { budgets } = this.options;
@ -35,42 +39,70 @@ export class BundleBudgetPlugin {
return; return;
} }
compiler.hooks.compilation.tap('BundleBudgetPlugin', (compilation: compilation.Compilation) => { compiler.hooks.compilation.tap(
compilation.hooks.afterOptimizeChunkAssets.tap('BundleBudgetPlugin', () => { 'BundleBudgetPlugin',
// In AOT compilations component styles get processed in child compilations. (compilation: compilation.Compilation) => {
// tslint:disable-next-line: no-any compilation.hooks.afterOptimizeChunkAssets.tap(
const parentCompilation = (compilation.compiler as any).parentCompilation; 'BundleBudgetPlugin',
if (!parentCompilation) { () => {
return; // In AOT compilations component styles get processed in child compilations.
} // tslint:disable-next-line: no-any
const parentCompilation = (compilation.compiler as any)
.parentCompilation;
if (!parentCompilation) {
return;
}
const filteredBudgets = budgets.filter(budget => budget.type === Type.AnyComponentStyle); const filteredBudgets = budgets.filter(
budget => budget.type === Type.AnyComponentStyle
);
this.runChecks(filteredBudgets, compilation);
}
);
}
);
compiler.hooks.afterEmit.tap(
'BundleBudgetPlugin',
(compilation: compilation.Compilation) => {
const filteredBudgets = budgets.filter(
budget => budget.type !== Type.AnyComponentStyle
);
this.runChecks(filteredBudgets, compilation); this.runChecks(filteredBudgets, compilation);
}); }
}); );
compiler.hooks.afterEmit.tap('BundleBudgetPlugin', (compilation: compilation.Compilation) => {
const filteredBudgets = budgets.filter(budget => budget.type !== Type.AnyComponentStyle);
this.runChecks(filteredBudgets, compilation);
});
} }
private checkMinimum(threshold: number | undefined, size: Size, messages: string[]) { private checkMinimum(
threshold: number | undefined,
size: Size,
messages: string[]
) {
if (threshold) { if (threshold) {
if (threshold > size.size) { if (threshold > size.size) {
const sizeDifference = formatSize(threshold - size.size); const sizeDifference = formatSize(threshold - size.size);
messages.push(`budgets, minimum exceeded for ${size.label}. ` messages.push(
+ `Budget ${formatSize(threshold)} was not reached by ${sizeDifference}.`); `budgets, minimum exceeded for ${size.label}. ` +
`Budget ${formatSize(
threshold
)} was not reached by ${sizeDifference}.`
);
} }
} }
} }
private checkMaximum(threshold: number | undefined, size: Size, messages: string[]) { private checkMaximum(
threshold: number | undefined,
size: Size,
messages: string[]
) {
if (threshold) { if (threshold) {
if (threshold < size.size) { if (threshold < size.size) {
const sizeDifference = formatSize(size.size - threshold); const sizeDifference = formatSize(size.size - threshold);
messages.push(`budgets, maximum exceeded for ${size.label}. ` messages.push(
+ `Budget ${formatSize(threshold)} was exceeded by ${sizeDifference}.`); `budgets, maximum exceeded for ${size.label}. ` +
`Budget ${formatSize(threshold)} was exceeded by ${sizeDifference}.`
);
} }
} }
} }
@ -78,27 +110,51 @@ export class BundleBudgetPlugin {
private calculate(budget: Budget): Thresholds { private calculate(budget: Budget): Thresholds {
const thresholds: Thresholds = {}; const thresholds: Thresholds = {};
if (budget.maximumWarning) { if (budget.maximumWarning) {
thresholds.maximumWarning = calculateBytes(budget.maximumWarning, budget.baseline, 1); thresholds.maximumWarning = calculateBytes(
budget.maximumWarning,
budget.baseline,
1
);
} }
if (budget.maximumError) { if (budget.maximumError) {
thresholds.maximumError = calculateBytes(budget.maximumError, budget.baseline, 1); thresholds.maximumError = calculateBytes(
budget.maximumError,
budget.baseline,
1
);
} }
if (budget.minimumWarning) { if (budget.minimumWarning) {
thresholds.minimumWarning = calculateBytes(budget.minimumWarning, budget.baseline, -1); thresholds.minimumWarning = calculateBytes(
budget.minimumWarning,
budget.baseline,
-1
);
} }
if (budget.minimumError) { if (budget.minimumError) {
thresholds.minimumError = calculateBytes(budget.minimumError, budget.baseline, -1); thresholds.minimumError = calculateBytes(
budget.minimumError,
budget.baseline,
-1
);
} }
if (budget.warning) { if (budget.warning) {
thresholds.warningLow = calculateBytes(budget.warning, budget.baseline, -1); thresholds.warningLow = calculateBytes(
budget.warning,
budget.baseline,
-1
);
} }
if (budget.warning) { if (budget.warning) {
thresholds.warningHigh = calculateBytes(budget.warning, budget.baseline, 1); thresholds.warningHigh = calculateBytes(
budget.warning,
budget.baseline,
1
);
} }
if (budget.error) { if (budget.error) {
@ -117,18 +173,50 @@ export class BundleBudgetPlugin {
.map(budget => ({ .map(budget => ({
budget, budget,
thresholds: this.calculate(budget), thresholds: this.calculate(budget),
sizes: calculateSizes(budget, compilation), sizes: calculateSizes(budget, compilation)
})) }))
.forEach(budgetCheck => { .forEach(budgetCheck => {
budgetCheck.sizes.forEach(size => { budgetCheck.sizes.forEach(size => {
this.checkMaximum(budgetCheck.thresholds.maximumWarning, size, compilation.warnings); this.checkMaximum(
this.checkMaximum(budgetCheck.thresholds.maximumError, size, compilation.errors); budgetCheck.thresholds.maximumWarning,
this.checkMinimum(budgetCheck.thresholds.minimumWarning, size, compilation.warnings); size,
this.checkMinimum(budgetCheck.thresholds.minimumError, size, compilation.errors); compilation.warnings
this.checkMinimum(budgetCheck.thresholds.warningLow, size, compilation.warnings); );
this.checkMaximum(budgetCheck.thresholds.warningHigh, size, compilation.warnings); this.checkMaximum(
this.checkMinimum(budgetCheck.thresholds.errorLow, size, compilation.errors); budgetCheck.thresholds.maximumError,
this.checkMaximum(budgetCheck.thresholds.errorHigh, size, compilation.errors); size,
compilation.errors
);
this.checkMinimum(
budgetCheck.thresholds.minimumWarning,
size,
compilation.warnings
);
this.checkMinimum(
budgetCheck.thresholds.minimumError,
size,
compilation.errors
);
this.checkMinimum(
budgetCheck.thresholds.warningLow,
size,
compilation.warnings
);
this.checkMaximum(
budgetCheck.thresholds.warningHigh,
size,
compilation.warnings
);
this.checkMinimum(
budgetCheck.thresholds.errorLow,
size,
compilation.errors
);
this.checkMaximum(
budgetCheck.thresholds.errorHigh,
size,
compilation.errors
);
}); });
}); });
} }

View File

@ -18,16 +18,17 @@ function hook(
compiler: Compiler, compiler: Compiler,
action: ( action: (
compilation: compilation.Compilation, compilation: compilation.Compilation,
chunks: compilation.Chunk[], chunks: compilation.Chunk[]
) => Promise<void | void[]>, ) => Promise<void | void[]>
) { ) {
compiler.hooks.compilation.tap( compiler.hooks.compilation.tap(
'cleancss-webpack-plugin', 'cleancss-webpack-plugin',
(compilation: compilation.Compilation) => { (compilation: compilation.Compilation) => {
compilation.hooks.optimizeChunkAssets.tapPromise('cleancss-webpack-plugin', chunks => compilation.hooks.optimizeChunkAssets.tapPromise(
action(compilation, chunks), 'cleancss-webpack-plugin',
chunks => action(compilation, chunks)
); );
}, }
); );
} }
@ -38,95 +39,100 @@ export class CleanCssWebpackPlugin {
this._options = { this._options = {
sourceMap: false, sourceMap: false,
test: file => file.endsWith('.css'), test: file => file.endsWith('.css'),
...options, ...options
}; };
} }
apply(compiler: Compiler): void { apply(compiler: Compiler): void {
hook(compiler, (compilation: compilation.Compilation, chunks: compilation.Chunk[]) => { hook(
const cleancss = new CleanCSS({ compiler,
compatibility: 'ie9', (compilation: compilation.Compilation, chunks: compilation.Chunk[]) => {
level: { const cleancss = new CleanCSS({
2: { compatibility: 'ie9',
skipProperties: [ level: {
'transition', // Fixes #12408 2: {
'font', // Fixes #9648 skipProperties: [
], 'transition', // Fixes #12408
'font' // Fixes #9648
]
}
}, },
}, inline: false,
inline: false, returnPromise: true,
returnPromise: true, sourceMap: this._options.sourceMap
sourceMap: this._options.sourceMap,
});
const files: string[] = [...compilation.additionalChunkAssets];
chunks.forEach(chunk => {
if (chunk.files && chunk.files.length > 0) {
files.push(...chunk.files);
}
});
const actions = files
.filter(file => this._options.test(file))
.map(async file => {
const asset = compilation.assets[file] as Source;
if (!asset) {
return;
}
let content: string;
// tslint:disable-next-line: no-any
let map: any;
if (this._options.sourceMap && asset.sourceAndMap) {
const sourceAndMap = asset.sourceAndMap();
content = sourceAndMap.source;
map = sourceAndMap.map;
} else {
content = asset.source();
}
if (content.length === 0) {
return;
}
const output = await cleancss.minify(content, map);
let hasWarnings = false;
if (output.warnings && output.warnings.length > 0) {
compilation.warnings.push(...output.warnings);
hasWarnings = true;
}
if (output.errors && output.errors.length > 0) {
output.errors.forEach((error: string) => compilation.errors.push(new Error(error)));
return;
}
// generally means invalid syntax so bail
if (hasWarnings && output.stats.minifiedSize === 0) {
return;
}
let newSource;
if (output.sourceMap) {
newSource = new SourceMapSource(
output.styles,
file,
// tslint:disable-next-line: no-any
output.sourceMap.toString() as any,
content,
map,
);
} else {
newSource = new RawSource(output.styles);
}
compilation.assets[file] = newSource;
}); });
return Promise.all(actions); const files: string[] = [...compilation.additionalChunkAssets];
});
chunks.forEach(chunk => {
if (chunk.files && chunk.files.length > 0) {
files.push(...chunk.files);
}
});
const actions = files
.filter(file => this._options.test(file))
.map(async file => {
const asset = compilation.assets[file] as Source;
if (!asset) {
return;
}
let content: string;
// tslint:disable-next-line: no-any
let map: any;
if (this._options.sourceMap && asset.sourceAndMap) {
const sourceAndMap = asset.sourceAndMap();
content = sourceAndMap.source;
map = sourceAndMap.map;
} else {
content = asset.source();
}
if (content.length === 0) {
return;
}
const output = await cleancss.minify(content, map);
let hasWarnings = false;
if (output.warnings && output.warnings.length > 0) {
compilation.warnings.push(...output.warnings);
hasWarnings = true;
}
if (output.errors && output.errors.length > 0) {
output.errors.forEach((error: string) =>
compilation.errors.push(new Error(error))
);
return;
}
// generally means invalid syntax so bail
if (hasWarnings && output.stats.minifiedSize === 0) {
return;
}
let newSource;
if (output.sourceMap) {
newSource = new SourceMapSource(
output.styles,
file,
// tslint:disable-next-line: no-any
output.sourceMap.toString() as any,
content,
map
);
} else {
newSource = new RawSource(output.styles);
}
compilation.assets[file] = newSource;
});
return Promise.all(actions);
}
);
} }
} }

View File

@ -11,7 +11,7 @@ import { RawSource } from 'webpack-sources';
import { import {
CrossOriginValue, CrossOriginValue,
FileInfo, FileInfo,
augmentIndexHtml, augmentIndexHtml
} from '../utilities/index-file/augment-index-html'; } from '../utilities/index-file/augment-index-html';
import { IndexHtmlTransform } from '../utilities/index-file/write-index-html'; import { IndexHtmlTransform } from '../utilities/index-file/write-index-html';
import { stripBom } from '../utilities/strip-bom'; import { stripBom } from '../utilities/strip-bom';
@ -29,17 +29,23 @@ export interface IndexHtmlWebpackPluginOptions {
crossOrigin?: CrossOriginValue; crossOrigin?: CrossOriginValue;
} }
function readFile(filename: string, compilation: compilation.Compilation): Promise<string> { function readFile(
filename: string,
compilation: compilation.Compilation
): Promise<string> {
return new Promise<string>((resolve, reject) => { return new Promise<string>((resolve, reject) => {
compilation.inputFileSystem.readFile(filename, (err: Error, data: Buffer) => { compilation.inputFileSystem.readFile(
if (err) { filename,
reject(err); (err: Error, data: Buffer) => {
if (err) {
reject(err);
return; return;
}
resolve(stripBom(data.toString()));
} }
);
resolve(stripBom(data.toString()));
});
}); });
} }
@ -54,60 +60,67 @@ export class IndexHtmlWebpackPlugin {
noModuleEntrypoints: [], noModuleEntrypoints: [],
moduleEntrypoints: [], moduleEntrypoints: [],
sri: false, sri: false,
...options, ...options
}; };
} }
apply(compiler: Compiler) { apply(compiler: Compiler) {
compiler.hooks.emit.tapPromise('index-html-webpack-plugin', async compilation => { compiler.hooks.emit.tapPromise(
// Get input html file 'index-html-webpack-plugin',
const inputContent = await readFile(this._options.input, compilation); async compilation => {
compilation.fileDependencies.add(this._options.input); // Get input html file
const inputContent = await readFile(this._options.input, compilation);
compilation.fileDependencies.add(this._options.input);
// Get all files for selected entrypoints // Get all files for selected entrypoints
const files: FileInfo[] = []; const files: FileInfo[] = [];
const noModuleFiles: FileInfo[] = []; const noModuleFiles: FileInfo[] = [];
const moduleFiles: FileInfo[] = []; const moduleFiles: FileInfo[] = [];
for (const [entryName, entrypoint] of compilation.entrypoints) { for (const [entryName, entrypoint] of compilation.entrypoints) {
const entryFiles: FileInfo[] = ((entrypoint && entrypoint.getFiles()) || []).map( const entryFiles: FileInfo[] = (
(f: string): FileInfo => ({ (entrypoint && entrypoint.getFiles()) ||
name: entryName, []
file: f, ).map(
extension: path.extname(f), (f: string): FileInfo => ({
}), name: entryName,
); file: f,
extension: path.extname(f)
})
);
if (this._options.noModuleEntrypoints.includes(entryName)) { if (this._options.noModuleEntrypoints.includes(entryName)) {
noModuleFiles.push(...entryFiles); noModuleFiles.push(...entryFiles);
} else if (this._options.moduleEntrypoints.includes(entryName)) { } else if (this._options.moduleEntrypoints.includes(entryName)) {
moduleFiles.push(...entryFiles); moduleFiles.push(...entryFiles);
} else { } else {
files.push(...entryFiles); files.push(...entryFiles);
}
} }
const loadOutputFile = (name: string) =>
compilation.assets[name].source();
let indexSource = await augmentIndexHtml({
input: this._options.input,
inputContent,
baseHref: this._options.baseHref,
deployUrl: this._options.deployUrl,
sri: this._options.sri,
crossOrigin: this._options.crossOrigin,
files,
noModuleFiles,
loadOutputFile,
moduleFiles,
entrypoints: this._options.entrypoints
});
if (this._options.postTransform) {
indexSource = await this._options.postTransform(indexSource);
}
// Add to compilation assets
compilation.assets[this._options.output] = new RawSource(indexSource);
} }
);
const loadOutputFile = (name: string) => compilation.assets[name].source();
let indexSource = await augmentIndexHtml({
input: this._options.input,
inputContent,
baseHref: this._options.baseHref,
deployUrl: this._options.deployUrl,
sri: this._options.sri,
crossOrigin: this._options.crossOrigin,
files,
noModuleFiles,
loadOutputFile,
moduleFiles,
entrypoints: this._options.entrypoints,
});
if (this._options.postTransform) {
indexSource = await this._options.postTransform(indexSource);
}
// Add to compilation assets
compilation.assets[this._options.output] = new RawSource(indexSource);
});
} }
} }

View File

@ -14,26 +14,28 @@ const ImportDependenciesBlock = require('webpack/lib/dependencies/ImportDependen
const Template = require('webpack/lib/Template'); const Template = require('webpack/lib/Template');
export class NamedLazyChunksPlugin { export class NamedLazyChunksPlugin {
constructor() { } constructor() {}
apply(compiler: Compiler): void { apply(compiler: Compiler): void {
compiler.hooks.compilation.tap('named-lazy-chunks-plugin', compilation => { compiler.hooks.compilation.tap('named-lazy-chunks-plugin', compilation => {
// The dependencyReference hook isn't in the webpack typings so we have to type it as any. // The dependencyReference hook isn't in the webpack typings so we have to type it as any.
// tslint:disable-next-line: no-any // tslint:disable-next-line: no-any
(compilation.hooks as any).dependencyReference.tap('named-lazy-chunks-plugin', (compilation.hooks as any).dependencyReference.tap(
'named-lazy-chunks-plugin',
// tslint:disable-next-line: no-any // tslint:disable-next-line: no-any
(_: any, dependency: any) => { (_: any, dependency: any) => {
if ( if (
// Check this dependency is from an `import()` statement. // Check this dependency is from an `import()` statement.
dependency instanceof ImportDependency dependency instanceof ImportDependency &&
&& dependency.block instanceof ImportDependenciesBlock dependency.block instanceof ImportDependenciesBlock &&
// Don't rename chunks that already have a name. // Don't rename chunks that already have a name.
&& dependency.block.chunkName === null dependency.block.chunkName === null
) { ) {
// Convert the request to a valid chunk name using the same logic used // Convert the request to a valid chunk name using the same logic used
// in webpack/lib/ContextModule.js // in webpack/lib/ContextModule.js
dependency.block.chunkName = Template.toPath(dependency.request); dependency.block.chunkName = Template.toPath(dependency.request);
} }
}); }
);
}); });
} }
} }

View File

@ -13,7 +13,7 @@ import * as webpack from 'webpack';
function wrapUrl(url: string): string { function wrapUrl(url: string): string {
let wrappedUrl; let wrappedUrl;
const hasSingleQuotes = url.indexOf('\'') >= 0; const hasSingleQuotes = url.indexOf("'") >= 0;
if (hasSingleQuotes) { if (hasSingleQuotes) {
wrappedUrl = `"${url}"`; wrappedUrl = `"${url}"`;
@ -36,7 +36,7 @@ export interface PostcssCliResourcesOptions {
async function resolve( async function resolve(
file: string, file: string,
base: string, base: string,
resolver: (file: string, base: string) => Promise<string>, resolver: (file: string, base: string) => Promise<string>
): Promise<string> { ): Promise<string> {
try { try {
return await resolver('./' + file, base); return await resolver('./' + file, base);
@ -45,171 +45,191 @@ async function resolve(
} }
} }
export default postcss.plugin('postcss-cli-resources', (options: PostcssCliResourcesOptions) => { export default postcss.plugin(
const { 'postcss-cli-resources',
deployUrl = '', (options: PostcssCliResourcesOptions) => {
baseHref = '', const {
resourcesOutputPath = '', deployUrl = '',
rebaseRootRelative = false, baseHref = '',
filename, resourcesOutputPath = '',
loader, rebaseRootRelative = false,
} = options; filename,
loader
} = options;
const dedupeSlashes = (url: string) => url.replace(/\/\/+/g, '/'); const dedupeSlashes = (url: string) => url.replace(/\/\/+/g, '/');
const process = async (inputUrl: string, context: string, resourceCache: Map<string, string>) => { const process = async (
// If root-relative, absolute or protocol relative url, leave as is inputUrl: string,
if (/^((?:\w+:)?\/\/|data:|chrome:|#)/.test(inputUrl)) { context: string,
return inputUrl; resourceCache: Map<string, string>
} ) => {
// If root-relative, absolute or protocol relative url, leave as is
if (!rebaseRootRelative && /^\//.test(inputUrl)) { if (/^((?:\w+:)?\/\/|data:|chrome:|#)/.test(inputUrl)) {
return inputUrl; return inputUrl;
}
// If starts with a caret, remove and return remainder
// this supports bypassing asset processing
if (inputUrl.startsWith('^')) {
return inputUrl.substr(1);
}
const cacheKey = path.resolve(context, inputUrl);
const cachedUrl = resourceCache.get(cacheKey);
if (cachedUrl) {
return cachedUrl;
}
if (inputUrl.startsWith('~')) {
inputUrl = inputUrl.substr(1);
}
if (inputUrl.startsWith('/')) {
let outputUrl = '';
if (deployUrl.match(/:\/\//) || deployUrl.startsWith('/')) {
// If deployUrl is absolute or root relative, ignore baseHref & use deployUrl as is.
outputUrl = `${deployUrl.replace(/\/$/, '')}${inputUrl}`;
} else if (baseHref.match(/:\/\//)) {
// If baseHref contains a scheme, include it as is.
outputUrl = baseHref.replace(/\/$/, '') + dedupeSlashes(`/${deployUrl}/${inputUrl}`);
} else {
// Join together base-href, deploy-url and the original URL.
outputUrl = dedupeSlashes(`/${baseHref}/${deployUrl}/${inputUrl}`);
} }
resourceCache.set(cacheKey, outputUrl); if (!rebaseRootRelative && /^\//.test(inputUrl)) {
return inputUrl;
}
return outputUrl; // If starts with a caret, remove and return remainder
} // this supports bypassing asset processing
if (inputUrl.startsWith('^')) {
return inputUrl.substr(1);
}
const { pathname, hash, search } = url.parse(inputUrl.replace(/\\/g, '/')); const cacheKey = path.resolve(context, inputUrl);
const resolver = (file: string, base: string) => new Promise<string>((resolve, reject) => { const cachedUrl = resourceCache.get(cacheKey);
loader.resolve(base, decodeURI(file), (err, result) => { if (cachedUrl) {
if (err) { return cachedUrl;
reject(err); }
return; if (inputUrl.startsWith('~')) {
} inputUrl = inputUrl.substr(1);
resolve(result); }
});
});
const result = await resolve(pathname as string, context, resolver); if (inputUrl.startsWith('/')) {
let outputUrl = '';
return new Promise<string>((resolve, reject) => { if (deployUrl.match(/:\/\//) || deployUrl.startsWith('/')) {
loader.fs.readFile(result, (err: Error, content: Buffer) => { // If deployUrl is absolute or root relative, ignore baseHref & use deployUrl as is.
if (err) { outputUrl = `${deployUrl.replace(/\/$/, '')}${inputUrl}`;
reject(err); } else if (baseHref.match(/:\/\//)) {
// If baseHref contains a scheme, include it as is.
return; outputUrl =
} baseHref.replace(/\/$/, '') +
dedupeSlashes(`/${deployUrl}/${inputUrl}`);
let outputPath = interpolateName( } else {
{ resourcePath: result } as webpack.loader.LoaderContext, // Join together base-href, deploy-url and the original URL.
filename, outputUrl = dedupeSlashes(`/${baseHref}/${deployUrl}/${inputUrl}`);
{ content },
);
if (resourcesOutputPath) {
outputPath = path.posix.join(resourcesOutputPath, outputPath);
}
loader.addDependency(result);
loader.emitFile(outputPath, content, undefined);
let outputUrl = outputPath.replace(/\\/g, '/');
if (hash || search) {
outputUrl = url.format({ pathname: outputUrl, hash, search });
}
if (deployUrl && loader.loaders[loader.loaderIndex].options.ident !== 'extracted') {
outputUrl = url.resolve(deployUrl, outputUrl);
} }
resourceCache.set(cacheKey, outputUrl); resourceCache.set(cacheKey, outputUrl);
resolve(outputUrl);
return outputUrl;
}
const { pathname, hash, search } = url.parse(
inputUrl.replace(/\\/g, '/')
);
const resolver = (file: string, base: string) =>
new Promise<string>((resolve, reject) => {
loader.resolve(base, decodeURI(file), (err, result) => {
if (err) {
reject(err);
return;
}
resolve(result);
});
});
const result = await resolve(pathname as string, context, resolver);
return new Promise<string>((resolve, reject) => {
loader.fs.readFile(result, (err: Error, content: Buffer) => {
if (err) {
reject(err);
return;
}
let outputPath = interpolateName(
{ resourcePath: result } as webpack.loader.LoaderContext,
filename,
{ content }
);
if (resourcesOutputPath) {
outputPath = path.posix.join(resourcesOutputPath, outputPath);
}
loader.addDependency(result);
loader.emitFile(outputPath, content, undefined);
let outputUrl = outputPath.replace(/\\/g, '/');
if (hash || search) {
outputUrl = url.format({ pathname: outputUrl, hash, search });
}
if (
deployUrl &&
loader.loaders[loader.loaderIndex].options.ident !== 'extracted'
) {
outputUrl = url.resolve(deployUrl, outputUrl);
}
resourceCache.set(cacheKey, outputUrl);
resolve(outputUrl);
});
}); });
}); };
};
return (root) => { return root => {
const urlDeclarations: Array<postcss.Declaration> = []; const urlDeclarations: Array<postcss.Declaration> = [];
root.walkDecls(decl => { root.walkDecls(decl => {
if (decl.value && decl.value.includes('url')) { if (decl.value && decl.value.includes('url')) {
urlDeclarations.push(decl); urlDeclarations.push(decl);
}
});
if (urlDeclarations.length === 0) {
return;
}
const resourceCache = new Map<string, string>();
return Promise.all(urlDeclarations.map(async decl => {
const value = decl.value;
const urlRegex = /url\(\s*(?:"([^"]+)"|'([^']+)'|(.+?))\s*\)/g;
const segments: string[] = [];
let match;
let lastIndex = 0;
let modified = false;
// We want to load it relative to the file that imports
const inputFile = decl.source && decl.source.input.file;
const context = inputFile && path.dirname(inputFile) || loader.context;
// tslint:disable-next-line:no-conditional-assignment
while (match = urlRegex.exec(value)) {
const originalUrl = match[1] || match[2] || match[3];
let processedUrl;
try {
processedUrl = await process(originalUrl, context, resourceCache);
} catch (err) {
loader.emitError(decl.error(err.message, { word: originalUrl }).toString());
continue;
} }
});
if (lastIndex < match.index) { if (urlDeclarations.length === 0) {
segments.push(value.slice(lastIndex, match.index)); return;
}
if (!processedUrl || originalUrl === processedUrl) {
segments.push(match[0]);
} else {
segments.push(wrapUrl(processedUrl));
modified = true;
}
lastIndex = match.index + match[0].length;
} }
if (lastIndex < value.length) { const resourceCache = new Map<string, string>();
segments.push(value.slice(lastIndex));
}
if (modified) { return Promise.all(
decl.value = segments.join(''); urlDeclarations.map(async decl => {
} const value = decl.value;
})); const urlRegex = /url\(\s*(?:"([^"]+)"|'([^']+)'|(.+?))\s*\)/g;
}; const segments: string[] = [];
});
let match;
let lastIndex = 0;
let modified = false;
// We want to load it relative to the file that imports
const inputFile = decl.source && decl.source.input.file;
const context =
(inputFile && path.dirname(inputFile)) || loader.context;
// tslint:disable-next-line:no-conditional-assignment
while ((match = urlRegex.exec(value))) {
const originalUrl = match[1] || match[2] || match[3];
let processedUrl;
try {
processedUrl = await process(originalUrl, context, resourceCache);
} catch (err) {
loader.emitError(
decl.error(err.message, { word: originalUrl }).toString()
);
continue;
}
if (lastIndex < match.index) {
segments.push(value.slice(lastIndex, match.index));
}
if (!processedUrl || originalUrl === processedUrl) {
segments.push(match[0]);
} else {
segments.push(wrapUrl(processedUrl));
modified = true;
}
lastIndex = match.index + match[0].length;
}
if (lastIndex < value.length) {
segments.push(value.slice(lastIndex));
}
if (modified) {
decl.value = segments.join('');
}
})
);
};
}
);

View File

@ -8,15 +8,13 @@
import { Compiler, compilation } from 'webpack'; import { Compiler, compilation } from 'webpack';
import { HashFormat } from '../models/webpack-configs/utils'; import { HashFormat } from '../models/webpack-configs/utils';
export interface RemoveHashPluginOptions { export interface RemoveHashPluginOptions {
chunkNames: string[]; chunkNames: string[];
hashFormat: HashFormat; hashFormat: HashFormat;
} }
export class RemoveHashPlugin { export class RemoveHashPlugin {
constructor(private options: RemoveHashPluginOptions) {}
constructor(private options: RemoveHashPluginOptions) { }
apply(compiler: Compiler): void { apply(compiler: Compiler): void {
compiler.hooks.compilation.tap('remove-hash-plugin', compilation => { compiler.hooks.compilation.tap('remove-hash-plugin', compilation => {
@ -24,7 +22,8 @@ export class RemoveHashPlugin {
hooks: compilation.CompilationHooks; hooks: compilation.CompilationHooks;
}; };
mainTemplate.hooks.assetPath.tap('remove-hash-plugin', mainTemplate.hooks.assetPath.tap(
'remove-hash-plugin',
(path: string, data: { chunk?: { name: string } }) => { (path: string, data: { chunk?: { name: string } }) => {
const chunkName = data.chunk && data.chunk.name; const chunkName = data.chunk && data.chunk.name;
const { chunkNames, hashFormat } = this.options; const { chunkNames, hashFormat } = this.options;
@ -37,7 +36,7 @@ export class RemoveHashPlugin {
} }
return path; return path;
}, }
); );
}); });
} }

View File

@ -16,7 +16,13 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import { Compiler, loader } from 'webpack'; import { Compiler, loader } from 'webpack';
import { CachedSource, ConcatSource, OriginalSource, RawSource, Source } from 'webpack-sources'; import {
CachedSource,
ConcatSource,
OriginalSource,
RawSource,
Source
} from 'webpack-sources';
import { interpolateName } from 'loader-utils'; import { interpolateName } from 'loader-utils';
import * as path from 'path'; import * as path from 'path';
@ -42,20 +48,26 @@ function addDependencies(compilation: any, scripts: string[]): void {
} }
} }
function hook(compiler: any, action: (compilation: any, callback: (err?: Error) => void) => void) { function hook(
compiler.hooks.thisCompilation.tap('scripts-webpack-plugin', (compilation: any) => { compiler: any,
compilation.hooks.additionalAssets.tapAsync( action: (compilation: any, callback: (err?: Error) => void) => void
'scripts-webpack-plugin', ) {
(callback: (err?: Error) => void) => action(compilation, callback), compiler.hooks.thisCompilation.tap(
); 'scripts-webpack-plugin',
}); (compilation: any) => {
compilation.hooks.additionalAssets.tapAsync(
'scripts-webpack-plugin',
(callback: (err?: Error) => void) => action(compilation, callback)
);
}
);
} }
export class ScriptsWebpackPlugin { export class ScriptsWebpackPlugin {
private _lastBuildTime?: number; private _lastBuildTime?: number;
private _cachedOutput?: ScriptOutput; private _cachedOutput?: ScriptOutput;
constructor(private options: Partial<ScriptsWebpackPluginOptions> = {}) { } constructor(private options: Partial<ScriptsWebpackPluginOptions> = {}) {}
shouldSkip(compilation: any, scripts: string[]): boolean { shouldSkip(compilation: any, scripts: string[]): boolean {
if (this._lastBuildTime == undefined) { if (this._lastBuildTime == undefined) {
@ -74,7 +86,11 @@ export class ScriptsWebpackPlugin {
return true; return true;
} }
private _insertOutput(compilation: any, { filename, source }: ScriptOutput, cached = false) { private _insertOutput(
compilation: any,
{ filename, source }: ScriptOutput,
cached = false
) {
const chunk = new Chunk(this.options.name); const chunk = new Chunk(this.options.name);
chunk.rendered = !cached; chunk.rendered = !cached;
chunk.id = this.options.name; chunk.id = this.options.name;
@ -112,29 +128,32 @@ export class ScriptsWebpackPlugin {
const sourceGetters = scripts.map(fullPath => { const sourceGetters = scripts.map(fullPath => {
return new Promise<Source>((resolve, reject) => { return new Promise<Source>((resolve, reject) => {
compilation.inputFileSystem.readFile(fullPath, (err: Error, data: Buffer) => { compilation.inputFileSystem.readFile(
if (err) { fullPath,
reject(err); (err: Error, data: Buffer) => {
return; if (err) {
} reject(err);
return;
const content = data.toString();
let source;
if (this.options.sourceMap) {
// TODO: Look for source map file (for '.min' scripts, etc.)
let adjustedPath = fullPath;
if (this.options.basePath) {
adjustedPath = path.relative(this.options.basePath, fullPath);
} }
source = new OriginalSource(content, adjustedPath);
} else {
source = new RawSource(content);
}
resolve(source); const content = data.toString();
});
let source;
if (this.options.sourceMap) {
// TODO: Look for source map file (for '.min' scripts, etc.)
let adjustedPath = fullPath;
if (this.options.basePath) {
adjustedPath = path.relative(this.options.basePath, fullPath);
}
source = new OriginalSource(content, adjustedPath);
} else {
source = new RawSource(content);
}
resolve(source);
}
);
}); });
}); });
@ -150,7 +169,7 @@ export class ScriptsWebpackPlugin {
const filename = interpolateName( const filename = interpolateName(
{ resourcePath: 'scripts.js' } as loader.LoaderContext, { resourcePath: 'scripts.js' } as loader.LoaderContext,
this.options.filename as string, this.options.filename as string,
{ content: combinedSource.source() }, { content: combinedSource.source() }
); );
const output = { filename, source: combinedSource }; const output = { filename, source: combinedSource };

View File

@ -15,7 +15,9 @@ export interface SingleTestTransformLoaderOptions {
logger: logging.Logger; logger: logging.Logger;
} }
export const SingleTestTransformLoader = require.resolve(join(__dirname, 'single-test-transform')); export const SingleTestTransformLoader = require.resolve(
join(__dirname, 'single-test-transform')
);
/** /**
* This loader transforms the default test file to only run tests * This loader transforms the default test file to only run tests
@ -46,12 +48,13 @@ export default function loader(this: loader.LoaderContext, source: string) {
const message = [ const message = [
`The 'include' option requires that the 'main' file for tests include the line below:`, `The 'include' option requires that the 'main' file for tests include the line below:`,
`const context = require.context('./', true, /\.spec\.ts$/);`, `const context = require.context('./', true, /\.spec\.ts$/);`,
`Arguments passed to require.context are not strict and can be changed`, `Arguments passed to require.context are not strict and can be changed`
]; ];
options.logger.error(message.join(lineSeparator)); options.logger.error(message.join(lineSeparator));
} }
const mockedRequireContext = '{ keys: () => ({ map: (_a) => { } }) };' + lineSeparator; const mockedRequireContext =
'{ keys: () => ({ map: (_a) => { } }) };' + lineSeparator;
source = source.replace(regex, mockedRequireContext + targettedImports); source = source.replace(regex, mockedRequireContext + targettedImports);
return source; return source;

View File

@ -12,51 +12,57 @@
// To be used together with ExtractTextPlugin. // To be used together with ExtractTextPlugin.
export class SuppressExtractedTextChunksWebpackPlugin { export class SuppressExtractedTextChunksWebpackPlugin {
constructor() { } constructor() {}
apply(compiler: any): void { apply(compiler: any): void {
compiler.hooks.compilation.tap('SuppressExtractedTextChunks', (compilation: any) => { compiler.hooks.compilation.tap(
// find which chunks have css only entry points 'SuppressExtractedTextChunks',
const cssOnlyChunks: string[] = []; (compilation: any) => {
const entryPoints = compilation.options.entry; // find which chunks have css only entry points
// determine which entry points are composed entirely of css files const cssOnlyChunks: string[] = [];
for (let entryPoint of Object.keys(entryPoints)) { const entryPoints = compilation.options.entry;
let entryFiles: string[] | string = entryPoints[entryPoint]; // determine which entry points are composed entirely of css files
// when type of entryFiles is not array, make it as an array for (let entryPoint of Object.keys(entryPoints)) {
entryFiles = entryFiles instanceof Array ? entryFiles : [entryFiles]; let entryFiles: string[] | string = entryPoints[entryPoint];
if (entryFiles.every((el: string) => // when type of entryFiles is not array, make it as an array
el.match(/\.(css|scss|sass|less|styl)$/) !== null)) { entryFiles = entryFiles instanceof Array ? entryFiles : [entryFiles];
cssOnlyChunks.push(entryPoint); if (
entryFiles.every(
(el: string) => el.match(/\.(css|scss|sass|less|styl)$/) !== null
)
) {
cssOnlyChunks.push(entryPoint);
}
} }
} // Remove the js file for supressed chunks
// Remove the js file for supressed chunks compilation.hooks.afterSeal.tap('SuppressExtractedTextChunks', () => {
compilation.hooks.afterSeal.tap('SuppressExtractedTextChunks', () => { compilation.chunks
compilation.chunks .filter((chunk: any) => cssOnlyChunks.indexOf(chunk.name) !== -1)
.filter((chunk: any) => cssOnlyChunks.indexOf(chunk.name) !== -1) .forEach((chunk: any) => {
.forEach((chunk: any) => { let newFiles: string[] = [];
let newFiles: string[] = []; chunk.files.forEach((file: string) => {
chunk.files.forEach((file: string) => { if (file.match(/\.js(\.map)?$/)) {
if (file.match(/\.js(\.map)?$/)) { // remove js files
// remove js files delete compilation.assets[file];
delete compilation.assets[file]; } else {
} else { newFiles.push(file);
newFiles.push(file); }
} });
chunk.files = newFiles;
}); });
chunk.files = newFiles; });
}); // Remove scripts tags with a css file as source, because HtmlWebpackPlugin will use
}); // a css file as a script for chunks without js files.
// Remove scripts tags with a css file as source, because HtmlWebpackPlugin will use // TODO: Enable this once HtmlWebpackPlugin supports Webpack 4
// a css file as a script for chunks without js files. // compilation.plugin('html-webpack-plugin-alter-asset-tags',
// TODO: Enable this once HtmlWebpackPlugin supports Webpack 4 // (htmlPluginData: any, callback: any) => {
// compilation.plugin('html-webpack-plugin-alter-asset-tags', // const filterFn = (tag: any) =>
// (htmlPluginData: any, callback: any) => { // !(tag.tagName === 'script' && tag.attributes.src.match(/\.css$/));
// const filterFn = (tag: any) => // htmlPluginData.head = htmlPluginData.head.filter(filterFn);
// !(tag.tagName === 'script' && tag.attributes.src.match(/\.css$/)); // htmlPluginData.body = htmlPluginData.body.filter(filterFn);
// htmlPluginData.head = htmlPluginData.head.filter(filterFn); // callback(null, htmlPluginData);
// htmlPluginData.body = htmlPluginData.body.filter(filterFn); // });
// callback(null, htmlPluginData); }
// }); );
});
} }
} }

View File

@ -7,15 +7,28 @@
*/ */
// Exports the webpack plugins we use internally. // Exports the webpack plugins we use internally.
export { CleanCssWebpackPlugin, CleanCssWebpackPluginOptions } from './cleancss-webpack-plugin'; export {
CleanCssWebpackPlugin,
CleanCssWebpackPluginOptions
} from './cleancss-webpack-plugin';
export { BundleBudgetPlugin, BundleBudgetPluginOptions } from './bundle-budget'; export { BundleBudgetPlugin, BundleBudgetPluginOptions } from './bundle-budget';
export { ScriptsWebpackPlugin, ScriptsWebpackPluginOptions } from './scripts-webpack-plugin'; export {
export { SuppressExtractedTextChunksWebpackPlugin } from './suppress-entry-chunks-webpack-plugin'; ScriptsWebpackPlugin,
export { RemoveHashPlugin, RemoveHashPluginOptions } from './remove-hash-plugin'; ScriptsWebpackPluginOptions
export { NamedLazyChunksPlugin as NamedChunksPlugin } from './named-chunks-plugin'; } from './scripts-webpack-plugin';
export {
SuppressExtractedTextChunksWebpackPlugin
} from './suppress-entry-chunks-webpack-plugin';
export {
RemoveHashPlugin,
RemoveHashPluginOptions
} from './remove-hash-plugin';
export {
NamedLazyChunksPlugin as NamedChunksPlugin
} from './named-chunks-plugin';
export { export {
default as PostcssCliResources, default as PostcssCliResources,
PostcssCliResourcesOptions, PostcssCliResourcesOptions
} from './postcss-cli-resources'; } from './postcss-cli-resources';
import { join } from 'path'; import { join } from 'path';

View File

@ -9,7 +9,7 @@ import { Budget } from '../../browser/schema';
export interface Compilation { export interface Compilation {
assets: { [name: string]: { size: () => number } }; assets: { [name: string]: { size: () => number } };
chunks: { name: string, files: string[], isOnlyInitial: () => boolean }[]; chunks: { name: string; files: string[]; isOnlyInitial: () => boolean }[];
warnings: string[]; warnings: string[];
errors: string[]; errors: string[];
} }
@ -19,7 +19,10 @@ export interface Size {
label?: string; label?: string;
} }
export function calculateSizes(budget: Budget, compilation: Compilation): Size[] { export function calculateSizes(
budget: Budget,
compilation: Compilation
): Size[] {
const calculatorMap = { const calculatorMap = {
all: AllCalculator, all: AllCalculator,
allScript: AllScriptCalculator, allScript: AllScriptCalculator,
@ -27,7 +30,7 @@ export function calculateSizes(budget: Budget, compilation: Compilation): Size[]
anyScript: AnyScriptCalculator, anyScript: AnyScriptCalculator,
anyComponentStyle: AnyComponentStyleCalculator, anyComponentStyle: AnyComponentStyleCalculator,
bundle: BundleCalculator, bundle: BundleCalculator,
initial: InitialCalculator, initial: InitialCalculator
}; };
const ctor = calculatorMap[budget.type]; const ctor = calculatorMap[budget.type];
@ -37,7 +40,7 @@ export function calculateSizes(budget: Budget, compilation: Compilation): Size[]
} }
export abstract class Calculator { export abstract class Calculator {
constructor (protected budget: Budget, protected compilation: Compilation) {} constructor(protected budget: Budget, protected compilation: Compilation) {}
abstract calculate(): Size[]; abstract calculate(): Size[];
} }
@ -54,7 +57,7 @@ class BundleCalculator extends Calculator {
.map((file: string) => this.compilation.assets[file].size()) .map((file: string) => this.compilation.assets[file].size())
.reduce((total: number, size: number) => total + size, 0); .reduce((total: number, size: number) => total + size, 0);
return [{size, label: this.budget.name}]; return [{ size, label: this.budget.name }];
} }
} }
@ -63,14 +66,16 @@ class BundleCalculator extends Calculator {
*/ */
class InitialCalculator extends Calculator { class InitialCalculator extends Calculator {
calculate() { calculate() {
const initialChunks = this.compilation.chunks.filter(chunk => chunk.isOnlyInitial()); const initialChunks = this.compilation.chunks.filter(chunk =>
chunk.isOnlyInitial()
);
const size: number = initialChunks const size: number = initialChunks
.reduce((files, chunk) => [...files, ...chunk.files], []) .reduce((files, chunk) => [...files, ...chunk.files], [])
.filter((file: string) => !file.endsWith('.map')) .filter((file: string) => !file.endsWith('.map'))
.map((file: string) => this.compilation.assets[file].size()) .map((file: string) => this.compilation.assets[file].size())
.reduce((total: number, size: number) => total + size, 0); .reduce((total: number, size: number) => total + size, 0);
return [{size, label: 'initial'}]; return [{ size, label: 'initial' }];
} }
} }
@ -85,7 +90,7 @@ class AllScriptCalculator extends Calculator {
.map(asset => asset.size()) .map(asset => asset.size())
.reduce((total: number, size: number) => total + size, 0); .reduce((total: number, size: number) => total + size, 0);
return [{size, label: 'total scripts'}]; return [{ size, label: 'total scripts' }];
} }
} }
@ -99,7 +104,7 @@ class AllCalculator extends Calculator {
.map(key => this.compilation.assets[key].size()) .map(key => this.compilation.assets[key].size())
.reduce((total: number, size: number) => total + size, 0); .reduce((total: number, size: number) => total + size, 0);
return [{size, label: 'total'}]; return [{ size, label: 'total' }];
} }
} }
@ -112,7 +117,7 @@ class AnyComponentStyleCalculator extends Calculator {
.filter(key => key.endsWith('.css')) .filter(key => key.endsWith('.css'))
.map(key => ({ .map(key => ({
size: this.compilation.assets[key].size(), size: this.compilation.assets[key].size(),
label: key, label: key
})); }));
} }
} }
@ -129,7 +134,7 @@ class AnyScriptCalculator extends Calculator {
return { return {
size: asset.size(), size: asset.size(),
label: key, label: key
}; };
}); });
} }
@ -147,7 +152,7 @@ class AnyCalculator extends Calculator {
return { return {
size: asset.size(), size: asset.size(),
label: key, label: key
}; };
}); });
} }
@ -159,19 +164,21 @@ class AnyCalculator extends Calculator {
export function calculateBytes( export function calculateBytes(
input: string, input: string,
baseline?: string, baseline?: string,
factor: 1 | -1 = 1, factor: 1 | -1 = 1
): number { ): number {
const matches = input.match(/^\s*(\d+(?:\.\d+)?)\s*(%|(?:[mM]|[kK]|[gG])?[bB])?\s*$/); const matches = input.match(
/^\s*(\d+(?:\.\d+)?)\s*(%|(?:[mM]|[kK]|[gG])?[bB])?\s*$/
);
if (!matches) { if (!matches) {
return NaN; return NaN;
} }
const baselineBytes = baseline && calculateBytes(baseline) || 0; const baselineBytes = (baseline && calculateBytes(baseline)) || 0;
let value = Number(matches[1]); let value = Number(matches[1]);
switch (matches[2] && matches[2].toLowerCase()) { switch (matches[2] && matches[2].toLowerCase()) {
case '%': case '%':
value = baselineBytes * value / 100; value = (baselineBytes * value) / 100;
break; break;
case 'kb': case 'kb':
value *= 1024; value *= 1024;

View File

@ -67,16 +67,16 @@ describe('bundle-calculator', () => {
expect(calculateBytes('25.0gB')).toBe(25 * 1024 * 1024 * 1024); expect(calculateBytes('25.0gB')).toBe(25 * 1024 * 1024 * 1024);
}); });
it ('converts a decimal with mb and baseline', () => { it('converts a decimal with mb and baseline', () => {
expect(calculateBytes('3mb', '5mb', -1)).toBe(2 * 1024 * 1024); expect(calculateBytes('3mb', '5mb', -1)).toBe(2 * 1024 * 1024);
}); });
it ('converts a percentage with baseline', () => { it('converts a percentage with baseline', () => {
expect(calculateBytes('20%', '1mb')).toBe(1024 * 1024 * 1.2); expect(calculateBytes('20%', '1mb')).toBe(1024 * 1024 * 1.2);
expect(calculateBytes('20%', '1mb', -1)).toBe(1024 * 1024 * 0.8); expect(calculateBytes('20%', '1mb', -1)).toBe(1024 * 1024 * 0.8);
}); });
it ('supports whitespace', () => { it('supports whitespace', () => {
expect(calculateBytes(' 5kb ')).toBe(5 * 1024); expect(calculateBytes(' 5kb ')).toBe(5 * 1024);
expect(calculateBytes('0.25 MB')).toBe(0.25 * 1024 * 1024); expect(calculateBytes('0.25 MB')).toBe(0.25 * 1024 * 1024);
expect(calculateBytes(' 20 % ', ' 1 mb ')).toBe(1024 * 1024 * 1.2); expect(calculateBytes(' 20 % ', ' 1 mb ')).toBe(1024 * 1024 * 1.2);

View File

@ -7,7 +7,11 @@
*/ */
import * as net from 'net'; import * as net from 'net';
export function checkPort(port: number, host: string, basePort = 49152): Promise<number> { export function checkPort(
port: number,
host: string,
basePort = 49152
): Promise<number> {
return new Promise<number>((resolve, reject) => { return new Promise<number>((resolve, reject) => {
function _getPort(portNumber: number) { function _getPort(portNumber: number) {
if (portNumber > 65535) { if (portNumber > 65535) {
@ -16,23 +20,26 @@ export function checkPort(port: number, host: string, basePort = 49152): Promise
const server = net.createServer(); const server = net.createServer();
server.once('error', (err: Error & {code: string}) => { server
if (err.code !== 'EADDRINUSE') { .once('error', (err: Error & { code: string }) => {
reject(err); if (err.code !== 'EADDRINUSE') {
} else if (port === 0) { reject(err);
_getPort(portNumber + 1); } else if (port === 0) {
} else { _getPort(portNumber + 1);
// If the port isn't available and we weren't looking for any port, throw error. } else {
reject( // If the port isn't available and we weren't looking for any port, throw error.
new Error(`Port ${port} is already in use. Use '--port' to specify a different port.`), reject(
); new Error(
} `Port ${port} is already in use. Use '--port' to specify a different port.`
}) )
.once('listening', () => { );
server.close(); }
resolve(portNumber); })
}) .once('listening', () => {
.listen(portNumber, host); server.close();
resolve(portNumber);
})
.listen(portNumber, host);
} }
_getPort(port || basePort); _getPort(port || basePort);

View File

@ -11,7 +11,11 @@ import { basename, dirname, extname, join } from 'path';
import { isDirectory } from './is-directory'; import { isDirectory } from './is-directory';
// go through all patterns and find unique list of files // go through all patterns and find unique list of files
export function findTests(patterns: string[], cwd: string, workspaceRoot: string): string[] { export function findTests(
patterns: string[],
cwd: string,
workspaceRoot: string
): string[] {
return patterns.reduce( return patterns.reduce(
(files, pattern) => { (files, pattern) => {
const relativePathToMain = cwd.replace(workspaceRoot, '').substr(1); // remove leading slash const relativePathToMain = cwd.replace(workspaceRoot, '').substr(1); // remove leading slash
@ -24,11 +28,15 @@ export function findTests(patterns: string[], cwd: string, workspaceRoot: string
return files; return files;
}, },
[] as string[], [] as string[]
); );
} }
function findMatchingTests(pattern: string, cwd: string, relativePathToMain: string): string[] { function findMatchingTests(
pattern: string,
cwd: string,
relativePathToMain: string
): string[] {
// normalize pattern, glob lib only accepts forward slashes // normalize pattern, glob lib only accepts forward slashes
pattern = pattern.replace(/\\/g, '/'); pattern = pattern.replace(/\\/g, '/');
relativePathToMain = relativePathToMain.replace(/\\/g, '/'); relativePathToMain = relativePathToMain.replace(/\\/g, '/');
@ -55,7 +63,7 @@ function findMatchingTests(pattern: string, cwd: string, relativePathToMain: str
} }
const files = glob.sync(pattern, { const files = glob.sync(pattern, {
cwd, cwd
}); });
return files; return files;

View File

@ -9,7 +9,11 @@ import { existsSync } from 'fs';
import * as path from 'path'; import * as path from 'path';
import { isDirectory } from './is-directory'; import { isDirectory } from './is-directory';
export function findUp(names: string | string[], from: string, stopOnNodeModules = false) { export function findUp(
names: string | string[],
from: string,
stopOnNodeModules = false
) {
if (!Array.isArray(names)) { if (!Array.isArray(names)) {
names = [names]; names = [names];
} }

View File

@ -56,8 +56,16 @@ export interface FileInfo {
* after processing several configurations in order to build different sets of * after processing several configurations in order to build different sets of
* bundles for differential serving. * bundles for differential serving.
*/ */
export async function augmentIndexHtml(params: AugmentIndexHtmlOptions): Promise<string> { export async function augmentIndexHtml(
const { loadOutputFile, files, noModuleFiles = [], moduleFiles = [], entrypoints } = params; params: AugmentIndexHtmlOptions
): Promise<string> {
const {
loadOutputFile,
files,
noModuleFiles = [],
moduleFiles = [],
entrypoints
} = params;
let { crossOrigin = 'none' } = params; let { crossOrigin = 'none' } = params;
if (params.sri && crossOrigin === 'none') { if (params.sri && crossOrigin === 'none') {
@ -88,7 +96,10 @@ export async function augmentIndexHtml(params: AugmentIndexHtmlOptions): Promise
// Find the head and body elements // Find the head and body elements
const treeAdapter = parse5.treeAdapters.default; const treeAdapter = parse5.treeAdapters.default;
const document = parse5.parse(params.inputContent, { treeAdapter, locationInfo: true }); const document = parse5.parse(params.inputContent, {
treeAdapter,
locationInfo: true
});
let headElement; let headElement;
let bodyElement; let bodyElement;
for (const docChild of document.childNodes) { for (const docChild of document.childNodes) {
@ -127,12 +138,15 @@ export async function augmentIndexHtml(params: AugmentIndexHtmlOptions): Promise
} }
// Inject into the html // Inject into the html
const indexSource = new ReplaceSource(new RawSource(params.inputContent), params.input); const indexSource = new ReplaceSource(
new RawSource(params.inputContent),
params.input
);
let scriptElements = ''; let scriptElements = '';
for (const script of scripts) { for (const script of scripts) {
const attrs: { name: string; value: string | null }[] = [ const attrs: { name: string; value: string | null }[] = [
{ name: 'src', value: (params.deployUrl || '') + script }, { name: 'src', value: (params.deployUrl || '') + script }
]; ];
if (crossOrigin !== 'none') { if (crossOrigin !== 'none') {
@ -169,7 +183,9 @@ export async function augmentIndexHtml(params: AugmentIndexHtmlOptions): Promise
} }
const attributes = attrs const attributes = attrs
.map(attr => (attr.value === null ? attr.name : `${attr.name}="${attr.value}"`)) .map(attr =>
attr.value === null ? attr.name : `${attr.name}="${attr.value}"`
)
.join(' '); .join(' ');
scriptElements += `<script ${attributes}></script>`; scriptElements += `<script ${attributes}></script>`;
} }
@ -189,13 +205,13 @@ export async function augmentIndexHtml(params: AugmentIndexHtmlOptions): Promise
if (!baseElement) { if (!baseElement) {
baseElement = treeAdapter.createElement('base', undefined, [ baseElement = treeAdapter.createElement('base', undefined, [
{ name: 'href', value: params.baseHref }, { name: 'href', value: params.baseHref }
]); ]);
treeAdapter.appendChild(baseFragment, baseElement); treeAdapter.appendChild(baseFragment, baseElement);
indexSource.insert( indexSource.insert(
headElement.__location.startTag.endOffset, headElement.__location.startTag.endOffset,
parse5.serialize(baseFragment, { treeAdapter }), parse5.serialize(baseFragment, { treeAdapter })
); );
} else { } else {
let hrefAttribute; let hrefAttribute;
@ -214,7 +230,7 @@ export async function augmentIndexHtml(params: AugmentIndexHtmlOptions): Promise
indexSource.replace( indexSource.replace(
baseElement.__location.startOffset, baseElement.__location.startOffset,
baseElement.__location.endOffset, baseElement.__location.endOffset,
parse5.serialize(baseFragment, { treeAdapter }), parse5.serialize(baseFragment, { treeAdapter })
); );
} }
} }
@ -223,7 +239,7 @@ export async function augmentIndexHtml(params: AugmentIndexHtmlOptions): Promise
for (const stylesheet of stylesheets) { for (const stylesheet of stylesheets) {
const attrs = [ const attrs = [
{ name: 'rel', value: 'stylesheet' }, { name: 'rel', value: 'stylesheet' },
{ name: 'href', value: (params.deployUrl || '') + stylesheet }, { name: 'href', value: (params.deployUrl || '') + stylesheet }
]; ];
if (crossOrigin !== 'none') { if (crossOrigin !== 'none') {
@ -239,7 +255,10 @@ export async function augmentIndexHtml(params: AugmentIndexHtmlOptions): Promise
treeAdapter.appendChild(styleElements, element); treeAdapter.appendChild(styleElements, element);
} }
indexSource.insert(styleInsertionPoint, parse5.serialize(styleElements, { treeAdapter })); indexSource.insert(
styleInsertionPoint,
parse5.serialize(styleElements, { treeAdapter })
);
return indexSource.source(); return indexSource.source();
} }

View File

@ -6,7 +6,11 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import { tags } from '@angular-devkit/core'; import { tags } from '@angular-devkit/core';
import { AugmentIndexHtmlOptions, FileInfo, augmentIndexHtml } from './augment-index-html'; import {
AugmentIndexHtmlOptions,
FileInfo,
augmentIndexHtml
} from './augment-index-html';
describe('augment-index-html', () => { describe('augment-index-html', () => {
const indexGeneratorOptions: AugmentIndexHtmlOptions = { const indexGeneratorOptions: AugmentIndexHtmlOptions = {
@ -16,7 +20,7 @@ describe('augment-index-html', () => {
sri: false, sri: false,
files: [], files: [],
loadOutputFile: async (_fileName: string) => '', loadOutputFile: async (_fileName: string) => '',
entrypoints: ['scripts', 'polyfills', 'main', 'styles'], entrypoints: ['scripts', 'polyfills', 'main', 'styles']
}; };
const oneLineHtml = (html: TemplateStringsArray) => const oneLineHtml = (html: TemplateStringsArray) =>
@ -30,8 +34,8 @@ describe('augment-index-html', () => {
{ file: 'runtime.js', extension: '.js', name: 'main' }, { file: 'runtime.js', extension: '.js', name: 'main' },
{ file: 'main.js', extension: '.js', name: 'main' }, { file: 'main.js', extension: '.js', name: 'main' },
{ file: 'runtime.js', extension: '.js', name: 'polyfills' }, { file: 'runtime.js', extension: '.js', name: 'polyfills' },
{ file: 'polyfills.js', extension: '.js', name: 'polyfills' }, { file: 'polyfills.js', extension: '.js', name: 'polyfills' }
], ]
}); });
const html = await source; const html = await source;
@ -54,24 +58,24 @@ describe('augment-index-html', () => {
{ file: 'runtime-es2015.js', extension: '.js', name: 'main' }, { file: 'runtime-es2015.js', extension: '.js', name: 'main' },
{ file: 'main-es2015.js', extension: '.js', name: 'main' }, { file: 'main-es2015.js', extension: '.js', name: 'main' },
{ file: 'runtime-es2015.js', extension: '.js', name: 'polyfills' }, { file: 'runtime-es2015.js', extension: '.js', name: 'polyfills' },
{ file: 'polyfills-es2015.js', extension: '.js', name: 'polyfills' }, { file: 'polyfills-es2015.js', extension: '.js', name: 'polyfills' }
]; ];
const es5JsFiles: FileInfo[] = [ const es5JsFiles: FileInfo[] = [
{ file: 'runtime-es5.js', extension: '.js', name: 'main' }, { file: 'runtime-es5.js', extension: '.js', name: 'main' },
{ file: 'main-es5.js', extension: '.js', name: 'main' }, { file: 'main-es5.js', extension: '.js', name: 'main' },
{ file: 'runtime-es5.js', extension: '.js', name: 'polyfills' }, { file: 'runtime-es5.js', extension: '.js', name: 'polyfills' },
{ file: 'polyfills-es5.js', extension: '.js', name: 'polyfills' }, { file: 'polyfills-es5.js', extension: '.js', name: 'polyfills' }
]; ];
const source = augmentIndexHtml({ const source = augmentIndexHtml({
...indexGeneratorOptions, ...indexGeneratorOptions,
files: [ files: [
{ file: 'styles.css', extension: '.css', name: 'styles' }, { file: 'styles.css', extension: '.css', name: 'styles' },
{ file: 'styles.css', extension: '.css', name: 'styles' }, { file: 'styles.css', extension: '.css', name: 'styles' }
], ],
moduleFiles: es2015JsFiles, moduleFiles: es2015JsFiles,
noModuleFiles: es5JsFiles, noModuleFiles: es5JsFiles
}); });
const html = await source; const html = await source;
@ -93,31 +97,29 @@ describe('augment-index-html', () => {
`); `);
}); });
it( it(`should not add 'module' and 'non-module' attr to js files which are in both module formats`, async () => {
`should not add 'module' and 'non-module' attr to js files which are in both module formats`, const es2015JsFiles: FileInfo[] = [
async () => { { file: 'scripts.js', extension: '.js', name: 'scripts' },
const es2015JsFiles: FileInfo[] = [ { file: 'main-es2015.js', extension: '.js', name: 'main' }
{ file: 'scripts.js', extension: '.js', name: 'scripts' }, ];
{ file: 'main-es2015.js', extension: '.js', name: 'main' },
];
const es5JsFiles: FileInfo[] = [ const es5JsFiles: FileInfo[] = [
{ file: 'scripts.js', extension: '.js', name: 'scripts' }, { file: 'scripts.js', extension: '.js', name: 'scripts' },
{ file: 'main-es5.js', extension: '.js', name: 'main' }, { file: 'main-es5.js', extension: '.js', name: 'main' }
]; ];
const source = augmentIndexHtml({ const source = augmentIndexHtml({
...indexGeneratorOptions, ...indexGeneratorOptions,
files: [ files: [
{ file: 'styles.css', extension: '.css', name: 'styles' }, { file: 'styles.css', extension: '.css', name: 'styles' },
{ file: 'styles.css', extension: '.css', name: 'styles' }, { file: 'styles.css', extension: '.css', name: 'styles' }
], ],
moduleFiles: es2015JsFiles, moduleFiles: es2015JsFiles,
noModuleFiles: es5JsFiles, noModuleFiles: es5JsFiles
}); });
const html = await source; const html = await source;
expect(html).toEqual(oneLineHtml` expect(html).toEqual(oneLineHtml`
<html> <html>
<head> <head>
<base href="/"> <base href="/">
@ -130,6 +132,5 @@ describe('augment-index-html', () => {
</body> </body>
</html> </html>
`); `);
}); });
}); });

View File

@ -7,13 +7,23 @@
*/ */
import { EmittedFiles } from '@angular-devkit/build-webpack'; import { EmittedFiles } from '@angular-devkit/build-webpack';
import { Path, dirname, getSystemPath, join, virtualFs } from '@angular-devkit/core'; import {
Path,
dirname,
getSystemPath,
join,
virtualFs
} from '@angular-devkit/core';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { map, switchMap } from 'rxjs/operators'; import { map, switchMap } from 'rxjs/operators';
import { ExtraEntryPoint } from '../../../browser/schema'; import { ExtraEntryPoint } from '../../../browser/schema';
import { generateEntryPoints } from '../package-chunk-sort'; import { generateEntryPoints } from '../package-chunk-sort';
import { stripBom } from '../strip-bom'; import { stripBom } from '../strip-bom';
import { CrossOriginValue, FileInfo, augmentIndexHtml } from './augment-index-html'; import {
CrossOriginValue,
FileInfo,
augmentIndexHtml
} from './augment-index-html';
type ExtensionFilter = '.js' | '.css'; type ExtensionFilter = '.js' | '.css';
@ -48,7 +58,7 @@ export function writeIndexHtml({
scripts = [], scripts = [],
styles = [], styles = [],
postTransform, postTransform,
crossOrigin, crossOrigin
}: WriteIndexHtmlOptions): Observable<void> { }: WriteIndexHtmlOptions): Observable<void> {
return host.read(indexPath).pipe( return host.read(indexPath).pipe(
map(content => stripBom(virtualFs.fileBufferToString(content))), map(content => stripBom(virtualFs.fileBufferToString(content))),
@ -69,18 +79,20 @@ export function writeIndexHtml({
.read(join(dirname(outputPath), filePath)) .read(join(dirname(outputPath), filePath))
.pipe(map(data => virtualFs.fileBufferToString(data))) .pipe(map(data => virtualFs.fileBufferToString(data)))
.toPromise(); .toPromise();
}, }
}), })
),
switchMap(content =>
postTransform ? postTransform(content) : of(content)
), ),
switchMap(content => (postTransform ? postTransform(content) : of(content))),
map(content => virtualFs.stringToFileBuffer(content)), map(content => virtualFs.stringToFileBuffer(content)),
switchMap(content => host.write(outputPath, content)), switchMap(content => host.write(outputPath, content))
); );
} }
function filterAndMapBuildFiles( function filterAndMapBuildFiles(
files: EmittedFiles[], files: EmittedFiles[],
extensionFilter: ExtensionFilter | ExtensionFilter[], extensionFilter: ExtensionFilter | ExtensionFilter[]
): FileInfo[] { ): FileInfo[] {
const filteredFiles: FileInfo[] = []; const filteredFiles: FileInfo[] = [];
const validExtensions: string[] = Array.isArray(extensionFilter) const validExtensions: string[] = Array.isArray(extensionFilter)

View File

@ -15,9 +15,12 @@ export function generateEntryPoints(appConfig: {
// Add all styles/scripts, except lazy-loaded ones. // Add all styles/scripts, except lazy-loaded ones.
const extraEntryPoints = ( const extraEntryPoints = (
extraEntryPoints: ExtraEntryPoint[], extraEntryPoints: ExtraEntryPoint[],
defaultBundleName: string, defaultBundleName: string
): string[] => { ): string[] => {
const entryPoints = normalizeExtraEntryPoints(extraEntryPoints, defaultBundleName) const entryPoints = normalizeExtraEntryPoints(
extraEntryPoints,
defaultBundleName
)
.filter(entry => entry.inject) .filter(entry => entry.inject)
.map(entry => entry.bundleName); .map(entry => entry.bundleName);
@ -34,15 +37,21 @@ export function generateEntryPoints(appConfig: {
...extraEntryPoints(appConfig.styles, 'styles'), ...extraEntryPoints(appConfig.styles, 'styles'),
...extraEntryPoints(appConfig.scripts, 'scripts'), ...extraEntryPoints(appConfig.scripts, 'scripts'),
'vendor', 'vendor',
'main', 'main'
]; ];
const duplicates = [ const duplicates = [
...new Set(entryPoints.filter(x => entryPoints.indexOf(x) !== entryPoints.lastIndexOf(x))), ...new Set(
entryPoints.filter(
x => entryPoints.indexOf(x) !== entryPoints.lastIndexOf(x)
)
)
]; ];
if (duplicates.length > 0) { if (duplicates.length > 0) {
throw new Error(`Multiple bundles have been named the same: '${duplicates.join(`', '`)}'.`); throw new Error(
`Multiple bundles have been named the same: '${duplicates.join(`', '`)}'.`
);
} }
return entryPoints; return entryPoints;

View File

@ -16,7 +16,10 @@ import * as path from 'path';
* @param workspaceRoot - workspaceRoot root location when provided * @param workspaceRoot - workspaceRoot root location when provided
* it will resolve 'tsconfigPath' from this path. * it will resolve 'tsconfigPath' from this path.
*/ */
export function readTsconfig(tsconfigPath: string, workspaceRoot?: string): ParsedConfiguration { export function readTsconfig(
tsconfigPath: string,
workspaceRoot?: string
): ParsedConfiguration {
const tsConfigFullPath = workspaceRoot const tsConfigFullPath = workspaceRoot
? path.resolve(workspaceRoot, tsconfigPath) ? path.resolve(workspaceRoot, tsconfigPath)
: tsconfigPath; : tsconfigPath;

View File

@ -13,16 +13,13 @@ import {
normalize, normalize,
relative, relative,
tags, tags,
virtualFs, virtualFs
} from '@angular-devkit/core'; } from '@angular-devkit/core';
import { import { Filesystem, Generator } from '@angular/service-worker/config'; // tslint:disable-line:no-implicit-dependencies
Filesystem,
Generator,
} from '@angular/service-worker/config'; // tslint:disable-line:no-implicit-dependencies
import * as crypto from 'crypto'; import * as crypto from 'crypto';
class CliFilesystem implements Filesystem { class CliFilesystem implements Filesystem {
constructor(private _host: virtualFs.Host, private base: string) { } constructor(private _host: virtualFs.Host, private base: string) {}
list(path: string): Promise<string[]> { list(path: string): Promise<string[]> {
return this._recursiveList(this._resolve(path), []).catch(() => []); return this._recursiveList(this._resolve(path), []).catch(() => []);
@ -40,7 +37,8 @@ class CliFilesystem implements Filesystem {
} }
write(path: string, content: string): Promise<void> { write(path: string, content: string): Promise<void> {
return this._host.write(this._resolve(path), virtualFs.stringToFileBuffer(content)) return this._host
.write(this._resolve(path), virtualFs.stringToFileBuffer(content))
.toPromise(); .toPromise();
} }
@ -75,19 +73,20 @@ export async function augmentAppWithServiceWorker(
appRoot: Path, appRoot: Path,
outputPath: Path, outputPath: Path,
baseHref: string, baseHref: string,
ngswConfigPath?: string, ngswConfigPath?: string
): Promise<void> { ): Promise<void> {
const distPath = normalize(outputPath); const distPath = normalize(outputPath);
const systemProjectRoot = getSystemPath(projectRoot); const systemProjectRoot = getSystemPath(projectRoot);
// Find the service worker package // Find the service worker package
const workerPath = normalize( const workerPath = normalize(
require.resolve('@angular/service-worker/ngsw-worker.js', { paths: [systemProjectRoot] }), require.resolve('@angular/service-worker/ngsw-worker.js', {
); paths: [systemProjectRoot]
const swConfigPath = require.resolve( })
'@angular/service-worker/config',
{ paths: [systemProjectRoot] },
); );
const swConfigPath = require.resolve('@angular/service-worker/config', {
paths: [systemProjectRoot]
});
// Determine the configuration file path // Determine the configuration file path
let configPath; let configPath;
@ -102,22 +101,32 @@ export async function augmentAppWithServiceWorker(
if (!configExists) { if (!configExists) {
throw new Error(tags.oneLine` throw new Error(tags.oneLine`
Error: Expected to find an ngsw-config.json configuration Error: Expected to find an ngsw-config.json configuration
file in the ${getSystemPath(appRoot)} folder. Either provide one or disable Service Worker file in the ${getSystemPath(
appRoot
)} folder. Either provide one or disable Service Worker
in your angular.json configuration file. in your angular.json configuration file.
`); `);
} }
// Read the configuration file // Read the configuration file
const config = JSON.parse(virtualFs.fileBufferToString(await host.read(configPath).toPromise())); const config = JSON.parse(
virtualFs.fileBufferToString(await host.read(configPath).toPromise())
);
// Generate the manifest // Generate the manifest
const GeneratorConstructor = require(swConfigPath).Generator as typeof Generator; const GeneratorConstructor = require(swConfigPath)
const generator = new GeneratorConstructor(new CliFilesystem(host, outputPath), baseHref); .Generator as typeof Generator;
const generator = new GeneratorConstructor(
new CliFilesystem(host, outputPath),
baseHref
);
const output = await generator.process(config); const output = await generator.process(config);
// Write the manifest // Write the manifest
const manifest = JSON.stringify(output, null, 2); const manifest = JSON.stringify(output, null, 2);
await host.write(join(distPath, 'ngsw.json'), virtualFs.stringToFileBuffer(manifest)).toPromise(); await host
.write(join(distPath, 'ngsw.json'), virtualFs.stringToFileBuffer(manifest))
.toPromise();
// Write the worker code // Write the worker code
// NOTE: This is inefficient (kernel -> userspace -> kernel). // NOTE: This is inefficient (kernel -> userspace -> kernel).
@ -130,7 +139,11 @@ export async function augmentAppWithServiceWorker(
if (await host.exists(safetyPath).toPromise()) { if (await host.exists(safetyPath).toPromise()) {
const safetyCode = await host.read(safetyPath).toPromise(); const safetyCode = await host.read(safetyPath).toPromise();
await host.write(join(distPath, 'worker-basic.min.js'), safetyCode).toPromise(); await host
await host.write(join(distPath, 'safety-worker.js'), safetyCode).toPromise(); .write(join(distPath, 'worker-basic.min.js'), safetyCode)
.toPromise();
await host
.write(join(distPath, 'safety-worker.js'), safetyCode)
.toPromise();
} }
} }

View File

@ -10,7 +10,6 @@
import { tags, terminal } from '@angular-devkit/core'; import { tags, terminal } from '@angular-devkit/core';
import * as path from 'path'; import * as path from 'path';
const { bold, green, red, reset, white, yellow } = terminal; const { bold, green, red, reset, white, yellow } = terminal;
export function formatSize(size: number): string { export function formatSize(size: number): string {
@ -21,7 +20,9 @@ export function formatSize(size: number): string {
const abbreviations = ['bytes', 'kB', 'MB', 'GB']; const abbreviations = ['bytes', 'kB', 'MB', 'GB'];
const index = Math.floor(Math.log(size) / Math.log(1024)); const index = Math.floor(Math.log(size) / Math.log(1024));
return `${+(size / Math.pow(1024, index)).toPrecision(3)} ${abbreviations[index]}`; return `${+(size / Math.pow(1024, index)).toPrecision(3)} ${
abbreviations[index]
}`;
} }
export function generateBundleStats( export function generateBundleStats(
@ -34,7 +35,7 @@ export function generateBundleStats(
initial: boolean; initial: boolean;
rendered?: boolean; rendered?: boolean;
}, },
colors: boolean, colors: boolean
): string { ): string {
const g = (x: string) => (colors ? bold(green(x)) : x); const g = (x: string) => (colors ? bold(green(x)) : x);
const y = (x: string) => (colors ? bold(yellow(x)) : x); const y = (x: string) => (colors ? bold(yellow(x)) : x);
@ -47,55 +48,81 @@ export function generateBundleStats(
.map(f => (f && (info as any)[f] ? g(` [${f}]`) : '')) .map(f => (f && (info as any)[f] ? g(` [${f}]`) : ''))
.join(''); .join('');
return `chunk {${y(info.id.toString())}} ${g(files)}${names}${size} ${initial}${flags}`; return `chunk {${y(info.id.toString())}} ${g(
files
)}${names}${size} ${initial}${flags}`;
} }
export function generateBuildStats(hash: string, time: number, colors: boolean): string { export function generateBuildStats(
const w = (x: string) => colors ? bold(white(x)) : x; hash: string,
return `Date: ${w(new Date().toISOString())} - Hash: ${w(hash)} - Time: ${w('' + time)}ms` time: number,
colors: boolean
): string {
const w = (x: string) => (colors ? bold(white(x)) : x);
return `Date: ${w(new Date().toISOString())} - Hash: ${w(hash)} - Time: ${w(
'' + time
)}ms`;
} }
export function statsToString(json: any, statsConfig: any) { export function statsToString(json: any, statsConfig: any) {
const colors = statsConfig.colors; const colors = statsConfig.colors;
const rs = (x: string) => colors ? reset(x) : x; const rs = (x: string) => (colors ? reset(x) : x);
const w = (x: string) => colors ? bold(white(x)) : x; const w = (x: string) => (colors ? bold(white(x)) : x);
const changedChunksStats = json.chunks const changedChunksStats = json.chunks
.filter((chunk: any) => chunk.rendered) .filter((chunk: any) => chunk.rendered)
.map((chunk: any) => { .map((chunk: any) => {
const asset = json.assets.filter((x: any) => x.name == chunk.files[0])[0]; const asset = json.assets.filter((x: any) => x.name == chunk.files[0])[0];
return generateBundleStats({ ...chunk, size: asset && asset.size }, colors); return generateBundleStats(
{ ...chunk, size: asset && asset.size },
colors
);
}); });
const unchangedChunkNumber = json.chunks.length - changedChunksStats.length; const unchangedChunkNumber = json.chunks.length - changedChunksStats.length;
if (unchangedChunkNumber > 0) { if (unchangedChunkNumber > 0) {
return '\n' + rs(tags.stripIndents` return (
'\n' +
rs(tags.stripIndents`
Date: ${w(new Date().toISOString())} - Hash: ${w(json.hash)} Date: ${w(new Date().toISOString())} - Hash: ${w(json.hash)}
${unchangedChunkNumber} unchanged chunks ${unchangedChunkNumber} unchanged chunks
${changedChunksStats.join('\n')} ${changedChunksStats.join('\n')}
Time: ${w('' + json.time)}ms Time: ${w('' + json.time)}ms
`); `)
);
} else { } else {
return '\n' + rs(tags.stripIndents` return (
'\n' +
rs(tags.stripIndents`
${changedChunksStats.join('\n')} ${changedChunksStats.join('\n')}
Date: ${w(new Date().toISOString())} - Hash: ${w(json.hash)} - Time: ${w('' + json.time)}ms Date: ${w(new Date().toISOString())} - Hash: ${w(json.hash)} - Time: ${w(
`); '' + json.time
)}ms
`)
);
} }
} }
export function statsWarningsToString(json: any, statsConfig: any) { export function statsWarningsToString(json: any, statsConfig: any) {
const colors = statsConfig.colors; const colors = statsConfig.colors;
const rs = (x: string) => colors ? reset(x) : x; const rs = (x: string) => (colors ? reset(x) : x);
const y = (x: string) => colors ? bold(yellow(x)) : x; const y = (x: string) => (colors ? bold(yellow(x)) : x);
return rs('\n' + json.warnings.map((warning: any) => y(`WARNING in ${warning}`)).join('\n\n')); return rs(
'\n' +
json.warnings
.map((warning: any) => y(`WARNING in ${warning}`))
.join('\n\n')
);
} }
export function statsErrorsToString(json: any, statsConfig: any) { export function statsErrorsToString(json: any, statsConfig: any) {
const colors = statsConfig.colors; const colors = statsConfig.colors;
const rs = (x: string) => colors ? reset(x) : x; const rs = (x: string) => (colors ? reset(x) : x);
const r = (x: string) => colors ? bold(red(x)) : x; const r = (x: string) => (colors ? bold(red(x)) : x);
return rs('\n' + json.errors.map((error: any) => r(`ERROR in ${error}`)).join('\n')); return rs(
'\n' + json.errors.map((error: any) => r(`ERROR in ${error}`)).join('\n')
);
} }

View File

@ -9,7 +9,11 @@ import { createHash } from 'crypto';
import * as findCacheDirectory from 'find-cache-dir'; import * as findCacheDirectory from 'find-cache-dir';
import * as fs from 'fs'; import * as fs from 'fs';
import { manglingDisabled } from '../utils/mangle-options'; import { manglingDisabled } from '../utils/mangle-options';
import { CacheKey, ProcessBundleOptions, ProcessBundleResult } from '../utils/process-bundle'; import {
CacheKey,
ProcessBundleOptions,
ProcessBundleResult
} from '../utils/process-bundle';
const cacache = require('cacache'); const cacache = require('cacache');
const cacheDownlevelPath = findCacheDirectory({ name: 'angular-build-dl' }); const cacheDownlevelPath = findCacheDirectory({ name: 'angular-build-dl' });
@ -20,7 +24,11 @@ const packageVersion = require('../../package.json').version;
let copyFileWorkaround = false; let copyFileWorkaround = false;
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
const version = process.versions.node.split('.').map(part => Number(part)); const version = process.versions.node.split('.').map(part => Number(part));
if (version[0] < 10 || version[0] === 11 || (version[0] === 10 && version[1] < 16)) { if (
version[0] < 10 ||
version[0] === 11 ||
(version[0] === 10 && version[1] < 16)
) {
copyFileWorkaround = true; copyFileWorkaround = true;
} }
} }
@ -44,7 +52,7 @@ export class BundleActionCache {
fs.copyFileSync( fs.copyFileSync(
typeof entry === 'string' ? entry : entry.path, typeof entry === 'string' ? entry : entry.path,
dest, dest,
fs.constants.COPYFILE_EXCL, fs.constants.COPYFILE_EXCL
); );
if (process.platform !== 'win32') { if (process.platform !== 'win32') {
// The cache writes entries as readonly and when using copyFile the permissions will also be copied. // The cache writes entries as readonly and when using copyFile the permissions will also be copied.
@ -74,7 +82,8 @@ export class BundleActionCache {
// Postfix added to sourcemap cache keys when vendor sourcemaps are present // Postfix added to sourcemap cache keys when vendor sourcemaps are present
// Allows non-destructive caching of both variants // Allows non-destructive caching of both variants
const SourceMapVendorPostfix = !!action.sourceMaps && action.vendorSourceMaps ? '|vendor' : ''; const SourceMapVendorPostfix =
!!action.sourceMaps && action.vendorSourceMaps ? '|vendor' : '';
// Determine cache entries required based on build settings // Determine cache entries required based on build settings
const cacheKeys = []; const cacheKeys = [];
@ -85,7 +94,8 @@ export class BundleActionCache {
// If sourcemaps are enabled, add original sourcemap as required // If sourcemaps are enabled, add original sourcemap as required
if (action.sourceMaps) { if (action.sourceMaps) {
cacheKeys[CacheKey.OriginalMap] = baseCacheKey + SourceMapVendorPostfix + '|orig-map'; cacheKeys[CacheKey.OriginalMap] =
baseCacheKey + SourceMapVendorPostfix + '|orig-map';
} }
} }
// If not only optimizing, add downlevel as required // If not only optimizing, add downlevel as required
@ -94,14 +104,17 @@ export class BundleActionCache {
// If sourcemaps are enabled, add downlevel sourcemap as required // If sourcemaps are enabled, add downlevel sourcemap as required
if (action.sourceMaps) { if (action.sourceMaps) {
cacheKeys[CacheKey.DownlevelMap] = baseCacheKey + SourceMapVendorPostfix + '|dl-map'; cacheKeys[CacheKey.DownlevelMap] =
baseCacheKey + SourceMapVendorPostfix + '|dl-map';
} }
} }
return cacheKeys; return cacheKeys;
} }
async getCacheEntries(cacheKeys: (string | null)[]): Promise<(CacheEntry | null)[] | false> { async getCacheEntries(
cacheKeys: (string | null)[]
): Promise<(CacheEntry | null)[] | false> {
// Attempt to get required cache entries // Attempt to get required cache entries
const cacheEntries = []; const cacheEntries = [];
for (const key of cacheKeys) { for (const key of cacheKeys) {
@ -113,7 +126,7 @@ export class BundleActionCache {
cacheEntries.push({ cacheEntries.push({
path: entry.path, path: entry.path,
size: entry.size, size: entry.size,
integrity: entry.metadata && entry.metadata.integrity, integrity: entry.metadata && entry.metadata.integrity
}); });
} else { } else {
cacheEntries.push(null); cacheEntries.push(null);
@ -123,8 +136,11 @@ export class BundleActionCache {
return cacheEntries; return cacheEntries;
} }
async getCachedBundleResult(action: ProcessBundleOptions): Promise<ProcessBundleResult | null> { async getCachedBundleResult(
const entries = action.cacheKeys && await this.getCacheEntries(action.cacheKeys); action: ProcessBundleOptions
): Promise<ProcessBundleResult | null> {
const entries =
action.cacheKeys && (await this.getCacheEntries(action.cacheKeys));
if (!entries) { if (!entries) {
return null; return null;
} }
@ -136,7 +152,7 @@ export class BundleActionCache {
result.original = { result.original = {
filename: action.filename, filename: action.filename,
size: cacheEntry.size, size: cacheEntry.size,
integrity: cacheEntry.integrity, integrity: cacheEntry.integrity
}; };
BundleActionCache.copyEntryContent(cacheEntry, result.original.filename); BundleActionCache.copyEntryContent(cacheEntry, result.original.filename);
@ -145,10 +161,13 @@ export class BundleActionCache {
if (cacheEntry) { if (cacheEntry) {
result.original.map = { result.original.map = {
filename: action.filename + '.map', filename: action.filename + '.map',
size: cacheEntry.size, size: cacheEntry.size
}; };
BundleActionCache.copyEntryContent(cacheEntry, result.original.filename + '.map'); BundleActionCache.copyEntryContent(
cacheEntry,
result.original.filename + '.map'
);
} }
} else if (!action.ignoreOriginal) { } else if (!action.ignoreOriginal) {
// If the original wasn't processed (and therefore not cached), add info // If the original wasn't processed (and therefore not cached), add info
@ -160,8 +179,8 @@ export class BundleActionCache {
? undefined ? undefined
: { : {
filename: action.filename + '.map', filename: action.filename + '.map',
size: Buffer.byteLength(action.map, 'utf8'), size: Buffer.byteLength(action.map, 'utf8')
}, }
}; };
} }
@ -170,7 +189,7 @@ export class BundleActionCache {
result.downlevel = { result.downlevel = {
filename: action.filename.replace('es2015', 'es5'), filename: action.filename.replace('es2015', 'es5'),
size: cacheEntry.size, size: cacheEntry.size,
integrity: cacheEntry.integrity, integrity: cacheEntry.integrity
}; };
BundleActionCache.copyEntryContent(cacheEntry, result.downlevel.filename); BundleActionCache.copyEntryContent(cacheEntry, result.downlevel.filename);
@ -179,10 +198,13 @@ export class BundleActionCache {
if (cacheEntry) { if (cacheEntry) {
result.downlevel.map = { result.downlevel.map = {
filename: action.filename.replace('es2015', 'es5') + '.map', filename: action.filename.replace('es2015', 'es5') + '.map',
size: cacheEntry.size, size: cacheEntry.size
}; };
BundleActionCache.copyEntryContent(cacheEntry, result.downlevel.filename + '.map'); BundleActionCache.copyEntryContent(
cacheEntry,
result.downlevel.filename + '.map'
);
} }
} }

View File

@ -8,7 +8,10 @@
import JestWorker from 'jest-worker'; import JestWorker from 'jest-worker';
import * as os from 'os'; import * as os from 'os';
import * as path from 'path'; import * as path from 'path';
import { ProcessBundleOptions, ProcessBundleResult } from '../utils/process-bundle'; import {
ProcessBundleOptions,
ProcessBundleResult
} from '../utils/process-bundle';
import { BundleActionCache } from './action-cache'; import { BundleActionCache } from './action-cache';
let workerFile = require.resolve('../utils/process-bundle'); let workerFile = require.resolve('../utils/process-bundle');
@ -25,13 +28,19 @@ export class BundleActionExecutor {
constructor( constructor(
private workerOptions: unknown, private workerOptions: unknown,
integrityAlgorithm?: string, integrityAlgorithm?: string,
private readonly sizeThreshold = 32 * 1024, private readonly sizeThreshold = 32 * 1024
) { ) {
this.cache = new BundleActionCache(integrityAlgorithm); this.cache = new BundleActionCache(integrityAlgorithm);
} }
private static executeMethod<O>(worker: JestWorker, method: string, input: unknown): Promise<O> { private static executeMethod<O>(
return ((worker as unknown) as Record<string, (i: unknown) => Promise<O>>)[method](input); worker: JestWorker,
method: string,
input: unknown
): Promise<O> {
return ((worker as unknown) as Record<string, (i: unknown) => Promise<O>>)[
method
](input);
} }
private ensureLarge(): JestWorker { private ensureLarge(): JestWorker {
@ -42,7 +51,7 @@ export class BundleActionExecutor {
// larger files are processed in a separate process to limit memory usage in the main process // larger files are processed in a separate process to limit memory usage in the main process
return (this.largeWorker = new JestWorker(workerFile, { return (this.largeWorker = new JestWorker(workerFile, {
exposedMethods: ['process'], exposedMethods: ['process'],
setupArgs: [this.workerOptions], setupArgs: [this.workerOptions]
})); }));
} }
@ -58,16 +67,27 @@ export class BundleActionExecutor {
setupArgs: [this.workerOptions], setupArgs: [this.workerOptions],
numWorkers: os.cpus().length < 2 ? 1 : 2, numWorkers: os.cpus().length < 2 ? 1 : 2,
// Will automatically fallback to processes if not supported // Will automatically fallback to processes if not supported
enableWorkerThreads: true, enableWorkerThreads: true
})); }));
} }
private executeAction<O>(method: string, action: { code: string }): Promise<O> { private executeAction<O>(
method: string,
action: { code: string }
): Promise<O> {
// code.length is not an exact byte count but close enough for this // code.length is not an exact byte count but close enough for this
if (action.code.length > this.sizeThreshold) { if (action.code.length > this.sizeThreshold) {
return BundleActionExecutor.executeMethod<O>(this.ensureLarge(), method, action); return BundleActionExecutor.executeMethod<O>(
this.ensureLarge(),
method,
action
);
} else { } else {
return BundleActionExecutor.executeMethod<O>(this.ensureSmall(), method, action); return BundleActionExecutor.executeMethod<O>(
this.ensureSmall(),
method,
action
);
} }
} }
@ -87,7 +107,10 @@ export class BundleActionExecutor {
} }
async *processAll(actions: Iterable<ProcessBundleOptions>) { async *processAll(actions: Iterable<ProcessBundleOptions>) {
const executions = new Map<Promise<ProcessBundleResult>, Promise<ProcessBundleResult>>(); const executions = new Map<
Promise<ProcessBundleResult>,
Promise<ProcessBundleResult>
>();
for (const action of actions) { for (const action of actions) {
const execution = this.process(action); const execution = this.process(action);
executions.set( executions.set(
@ -96,7 +119,7 @@ export class BundleActionExecutor {
executions.delete(execution); executions.delete(execution);
return result; return result;
}), })
); );
} }

View File

@ -5,12 +5,16 @@
* Use of this source code is governed by an MIT-style license that can be * Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect'; import {
BuilderContext,
BuilderOutput,
createBuilder
} from '@angular-devkit/architect';
import { import {
BuildResult, BuildResult,
EmittedFiles, EmittedFiles,
WebpackLoggingCallback, WebpackLoggingCallback,
runWebpack, runWebpack
} from '@angular-devkit/build-webpack'; } from '@angular-devkit/build-webpack';
import { import {
experimental, experimental,
@ -21,7 +25,7 @@ import {
normalize, normalize,
resolve, resolve,
tags, tags,
virtualFs, virtualFs
} from '@angular-devkit/core'; } from '@angular-devkit/core';
import { NodeJsSyncHost } from '@angular-devkit/core/node'; import { NodeJsSyncHost } from '@angular-devkit/core/node';
import { createHash } from 'crypto'; import { createHash } from 'crypto';
@ -30,7 +34,14 @@ import * as fs from 'fs';
import * as os from 'os'; import * as os from 'os';
import * as path from 'path'; import * as path from 'path';
import { from, of } from 'rxjs'; import { from, of } from 'rxjs';
import { bufferCount, catchError, concatMap, map, mergeScan, switchMap } from 'rxjs/operators'; import {
bufferCount,
catchError,
concatMap,
map,
mergeScan,
switchMap
} from 'rxjs/operators';
import { ScriptTarget } from 'typescript'; import { ScriptTarget } from 'typescript';
import * as webpack from 'webpack'; import * as webpack from 'webpack';
// import { NgBuildAnalyticsPlugin } from '../../plugins/webpack/analytics'; // import { NgBuildAnalyticsPlugin } from '../../plugins/webpack/analytics';
@ -43,11 +54,11 @@ import {
getStatsConfig, getStatsConfig,
getStylesConfig, getStylesConfig,
getWorkerConfig, getWorkerConfig,
normalizeExtraEntryPoints, normalizeExtraEntryPoints
} from '../angular-cli-files/models/webpack-configs'; } from '../angular-cli-files/models/webpack-configs';
import { import {
IndexHtmlTransform, IndexHtmlTransform,
writeIndexHtml, writeIndexHtml
} from '../angular-cli-files/utilities/index-file/write-index-html'; } from '../angular-cli-files/utilities/index-file/write-index-html';
import { readTsconfig } from '../angular-cli-files/utilities/read-tsconfig'; import { readTsconfig } from '../angular-cli-files/utilities/read-tsconfig';
import { augmentAppWithServiceWorker } from '../angular-cli-files/utilities/service-worker'; import { augmentAppWithServiceWorker } from '../angular-cli-files/utilities/service-worker';
@ -56,7 +67,7 @@ import {
generateBundleStats, generateBundleStats,
statsErrorsToString, statsErrorsToString,
statsToString, statsToString,
statsWarningsToString, statsWarningsToString
} from '../angular-cli-files/utilities/stats'; } from '../angular-cli-files/utilities/stats';
import { ExecutionTransformer } from '../transforms'; import { ExecutionTransformer } from '../transforms';
import { import {
@ -64,18 +75,18 @@ import {
deleteOutputDir, deleteOutputDir,
fullDifferential, fullDifferential,
normalizeOptimization, normalizeOptimization,
normalizeSourceMaps, normalizeSourceMaps
} from '../utils'; } from '../utils';
import { import {
ProcessBundleFile, ProcessBundleFile,
ProcessBundleOptions, ProcessBundleOptions,
ProcessBundleResult, ProcessBundleResult
} from '../utils/process-bundle'; } from '../utils/process-bundle';
import { assertCompatibleAngularVersion } from '../utils/version'; import { assertCompatibleAngularVersion } from '../utils/version';
import { import {
generateBrowserWebpackConfigFromContext, generateBrowserWebpackConfigFromContext,
getIndexInputFile, getIndexInputFile,
getIndexOutputFile, getIndexOutputFile
} from '../utils/webpack-browser-config'; } from '../utils/webpack-browser-config';
import { BundleActionExecutor } from './action-executor'; import { BundleActionExecutor } from './action-executor';
import { Schema as BrowserBuilderSchema } from './schema'; import { Schema as BrowserBuilderSchema } from './schema';
@ -91,7 +102,7 @@ export type BrowserBuilderOutput = json.JsonObject &
export function createBrowserLoggingCallback( export function createBrowserLoggingCallback(
verbose: boolean, verbose: boolean,
logger: logging.LoggerApi, logger: logging.LoggerApi
): WebpackLoggingCallback { ): WebpackLoggingCallback {
return (stats, config) => { return (stats, config) => {
// config.stats contains our own stats settings, added during buildWebpackConfig(). // config.stats contains our own stats settings, added during buildWebpackConfig().
@ -114,8 +125,11 @@ export function createBrowserLoggingCallback(
export async function buildBrowserWebpackConfigFromContext( export async function buildBrowserWebpackConfigFromContext(
options: BrowserBuilderSchema, options: BrowserBuilderSchema,
context: BuilderContext, context: BuilderContext,
host: virtualFs.Host<fs.Stats> = new NodeJsSyncHost(), host: virtualFs.Host<fs.Stats> = new NodeJsSyncHost()
): Promise<{ workspace: experimental.workspace.Workspace; config: webpack.Configuration[] }> { ): Promise<{
workspace: experimental.workspace.Workspace;
config: webpack.Configuration[];
}> {
return generateBrowserWebpackConfigFromContext( return generateBrowserWebpackConfigFromContext(
options, options,
context, context,
@ -126,15 +140,15 @@ export async function buildBrowserWebpackConfigFromContext(
getStatsConfig(wco), getStatsConfig(wco),
getAnalyticsConfig(wco, context), getAnalyticsConfig(wco, context),
getCompilerConfig(wco), getCompilerConfig(wco),
wco.buildOptions.webWorkerTsConfig ? getWorkerConfig(wco) : {}, wco.buildOptions.webWorkerTsConfig ? getWorkerConfig(wco) : {}
], ],
host, host
); );
} }
function getAnalyticsConfig( function getAnalyticsConfig(
wco: WebpackConfigOptions, wco: WebpackConfigOptions,
context: BuilderContext, context: BuilderContext
): webpack.Configuration { ): webpack.Configuration {
if (context.analytics) { if (context.analytics) {
// If there's analytics, add our plugin. Otherwise no need to slow down the build. // If there's analytics, add our plugin. Otherwise no need to slow down the build.
@ -142,12 +156,14 @@ function getAnalyticsConfig(
if (context.builder) { if (context.builder) {
// We already vetted that this is a "safe" package, otherwise the analytics would be noop. // We already vetted that this is a "safe" package, otherwise the analytics would be noop.
category = category =
context.builder.builderName.split(':')[1] || context.builder.builderName || 'build'; context.builder.builderName.split(':')[1] ||
context.builder.builderName ||
'build';
} }
// The category is the builder name if it's an angular builder. // The category is the builder name if it's an angular builder.
return { return {
plugins: [], plugins: []
}; };
} }
@ -166,9 +182,16 @@ async function initialize(
options: BrowserBuilderSchema, options: BrowserBuilderSchema,
context: BuilderContext, context: BuilderContext,
host: virtualFs.Host<fs.Stats>, host: virtualFs.Host<fs.Stats>,
webpackConfigurationTransform?: ExecutionTransformer<webpack.Configuration>, webpackConfigurationTransform?: ExecutionTransformer<webpack.Configuration>
): Promise<{ workspace: experimental.workspace.Workspace; config: webpack.Configuration[] }> { ): Promise<{
const { config, workspace } = await buildBrowserWebpackConfigFromContext(options, context, host); workspace: experimental.workspace.Workspace;
config: webpack.Configuration[];
}> {
const { config, workspace } = await buildBrowserWebpackConfigFromContext(
options,
context,
host
);
let transformedConfig; let transformedConfig;
if (webpackConfigurationTransform) { if (webpackConfigurationTransform) {
@ -182,7 +205,7 @@ async function initialize(
await deleteOutputDir( await deleteOutputDir(
normalize(context.workspaceRoot), normalize(context.workspaceRoot),
normalize(options.outputPath), normalize(options.outputPath),
host, host
).toPromise(); ).toPromise();
} }
@ -197,7 +220,7 @@ export function buildWebpackBrowser(
webpackConfiguration?: ExecutionTransformer<webpack.Configuration>; webpackConfiguration?: ExecutionTransformer<webpack.Configuration>;
logging?: WebpackLoggingCallback; logging?: WebpackLoggingCallback;
indexHtml?: IndexHtmlTransform; indexHtml?: IndexHtmlTransform;
} = {}, } = {}
) { ) {
const host = new NodeJsSyncHost(); const host = new NodeJsSyncHost();
const root = normalize(context.workspaceRoot); const root = normalize(context.workspaceRoot);
@ -205,7 +228,9 @@ export function buildWebpackBrowser(
// Check Angular version. // Check Angular version.
assertCompatibleAngularVersion(context.workspaceRoot, context.logger); assertCompatibleAngularVersion(context.workspaceRoot, context.logger);
return from(initialize(options, context, host, transforms.webpackConfiguration)).pipe( return from(
initialize(options, context, host, transforms.webpackConfiguration)
).pipe(
// tslint:disable-next-line: no-big-function // tslint:disable-next-line: no-big-function
switchMap(({ workspace, config: configs }) => { switchMap(({ workspace, config: configs }) => {
const projectName = context.target const projectName = context.target
@ -213,17 +238,22 @@ export function buildWebpackBrowser(
: workspace.getDefaultProjectName(); : workspace.getDefaultProjectName();
if (!projectName) { if (!projectName) {
throw new Error('Must either have a target from the context or a default project.'); throw new Error(
'Must either have a target from the context or a default project.'
);
} }
const projectRoot = resolve( const projectRoot = resolve(
workspace.root, workspace.root,
normalize(workspace.getProject(projectName).root), normalize(workspace.getProject(projectName).root)
); );
const tsConfig = readTsconfig(options.tsConfig, context.workspaceRoot); const tsConfig = readTsconfig(options.tsConfig, context.workspaceRoot);
const target = tsConfig.options.target || ScriptTarget.ES5; const target = tsConfig.options.target || ScriptTarget.ES5;
const buildBrowserFeatures = new BuildBrowserFeatures(getSystemPath(projectRoot), target); const buildBrowserFeatures = new BuildBrowserFeatures(
getSystemPath(projectRoot),
target
);
const isDifferentialLoadingNeeded = buildBrowserFeatures.isDifferentialLoadingNeeded(); const isDifferentialLoadingNeeded = buildBrowserFeatures.isDifferentialLoadingNeeded();
@ -251,14 +281,17 @@ export function buildWebpackBrowser(
transforms.logging || transforms.logging ||
(useBundleDownleveling (useBundleDownleveling
? () => {} ? () => {}
: createBrowserLoggingCallback(!!options.verbose, context.logger)), : createBrowserLoggingCallback(
!!options.verbose,
context.logger
))
}); });
} else { } else {
return of(); return of();
} }
}, },
{ success: true } as BuildResult, { success: true } as BuildResult,
1, 1
), ),
bufferCount(configs.length), bufferCount(configs.length),
// tslint:disable-next-line: no-big-function // tslint:disable-next-line: no-big-function
@ -270,10 +303,14 @@ export function buildWebpackBrowser(
// If it fails show any diagnostic messages and bail // If it fails show any diagnostic messages and bail
const webpackStats = buildEvents[0].webpackStats; const webpackStats = buildEvents[0].webpackStats;
if (webpackStats && webpackStats.warnings.length > 0) { if (webpackStats && webpackStats.warnings.length > 0) {
context.logger.warn(statsWarningsToString(webpackStats, { colors: true })); context.logger.warn(
statsWarningsToString(webpackStats, { colors: true })
);
} }
if (webpackStats && webpackStats.errors.length > 0) { if (webpackStats && webpackStats.errors.length > 0) {
context.logger.error(statsErrorsToString(webpackStats, { colors: true })); context.logger.error(
statsErrorsToString(webpackStats, { colors: true })
);
} }
return { success }; return { success };
@ -284,14 +321,19 @@ export function buildWebpackBrowser(
const scriptsEntryPointName = normalizeExtraEntryPoints( const scriptsEntryPointName = normalizeExtraEntryPoints(
options.scripts || [], options.scripts || [],
'scripts', 'scripts'
).map(x => x.bundleName); ).map(x => x.bundleName);
const [firstBuild, secondBuild] = buildEvents; const [firstBuild, secondBuild] = buildEvents;
if (isDifferentialLoadingNeeded && (fullDifferential || options.watch)) { if (
isDifferentialLoadingNeeded &&
(fullDifferential || options.watch)
) {
moduleFiles = firstBuild.emittedFiles || []; moduleFiles = firstBuild.emittedFiles || [];
files = moduleFiles.filter( files = moduleFiles.filter(
x => x.extension === '.css' || (x.name && scriptsEntryPointName.includes(x.name)), x =>
x.extension === '.css' ||
(x.name && scriptsEntryPointName.includes(x.name))
); );
if (buildEvents.length === 2) { if (buildEvents.length === 2) {
@ -303,13 +345,17 @@ export function buildWebpackBrowser(
noModuleFiles = []; noModuleFiles = [];
// Common options for all bundle process actions // Common options for all bundle process actions
const sourceMapOptions = normalizeSourceMaps(options.sourceMap || false); const sourceMapOptions = normalizeSourceMaps(
options.sourceMap || false
);
const actionOptions: Partial<ProcessBundleOptions> = { const actionOptions: Partial<ProcessBundleOptions> = {
optimize: normalizeOptimization(options.optimization).scripts, optimize: normalizeOptimization(options.optimization).scripts,
sourceMaps: sourceMapOptions.scripts, sourceMaps: sourceMapOptions.scripts,
hiddenSourceMaps: sourceMapOptions.hidden, hiddenSourceMaps: sourceMapOptions.hidden,
vendorSourceMaps: sourceMapOptions.vendor, vendorSourceMaps: sourceMapOptions.vendor,
integrityAlgorithm: options.subresourceIntegrity ? 'sha384' : undefined, integrityAlgorithm: options.subresourceIntegrity
? 'sha384'
: undefined
}; };
const actions: ProcessBundleOptions[] = []; const actions: ProcessBundleOptions[] = [];
@ -347,14 +393,20 @@ export function buildWebpackBrowser(
} }
// If not optimizing then ES2015 polyfills do not need processing // If not optimizing then ES2015 polyfills do not need processing
// Unlike other module scripts, it is never downleveled // Unlike other module scripts, it is never downleveled
const es2015Polyfills = file.file.startsWith('polyfills-es2015'); const es2015Polyfills = file.file.startsWith(
'polyfills-es2015'
);
if (!actionOptions.optimize && es2015Polyfills) { if (!actionOptions.optimize && es2015Polyfills) {
continue; continue;
} }
// Retrieve the content/map for the file // Retrieve the content/map for the file
// NOTE: Additional future optimizations will read directly from memory // NOTE: Additional future optimizations will read directly from memory
let filename = path.resolve(getSystemPath(root), options.outputPath, file.file); let filename = path.resolve(
getSystemPath(root),
options.outputPath,
file.file
);
const code = fs.readFileSync(filename, 'utf8'); const code = fs.readFileSync(filename, 'utf8');
let map; let map;
if (actionOptions.sourceMaps) { if (actionOptions.sourceMaps) {
@ -383,7 +435,7 @@ export function buildWebpackBrowser(
name: file.id!, name: file.id!,
runtime: file.file.startsWith('runtime'), runtime: file.file.startsWith('runtime'),
ignoreOriginal: es5Polyfills, ignoreOriginal: es5Polyfills,
optimizeOnly: es2015Polyfills, optimizeOnly: es2015Polyfills
}); });
// ES2015 polyfills are only optimized; optimization check was performed above // ES2015 polyfills are only optimized; optimization check was performed above
@ -399,7 +451,9 @@ export function buildWebpackBrowser(
} }
// Execute the bundle processing actions // Execute the bundle processing actions
context.logger.info('Generating ES5 bundles for differential loading...'); context.logger.info(
'Generating ES5 bundles for differential loading...'
);
const processActions: typeof actions = []; const processActions: typeof actions = [];
let processRuntimeAction: ProcessBundleOptions | undefined; let processRuntimeAction: ProcessBundleOptions | undefined;
@ -416,11 +470,13 @@ export function buildWebpackBrowser(
const executor = new BundleActionExecutor( const executor = new BundleActionExecutor(
{ cachePath: cacheDownlevelPath }, { cachePath: cacheDownlevelPath },
options.subresourceIntegrity ? 'sha384' : undefined, options.subresourceIntegrity ? 'sha384' : undefined
); );
try { try {
for await (const result of executor.processAll(processActions)) { for await (const result of executor.processAll(
processActions
)) {
processResults.push(result); processResults.push(result);
} }
} finally { } finally {
@ -431,32 +487,40 @@ export function buildWebpackBrowser(
if (processRuntimeAction) { if (processRuntimeAction) {
const runtimeOptions = { const runtimeOptions = {
...processRuntimeAction, ...processRuntimeAction,
runtimeData: processResults, runtimeData: processResults
}; };
processResults.push( processResults.push(
await import('../utils/process-bundle').then(m => m.process(runtimeOptions)), await import('../utils/process-bundle').then(m =>
m.process(runtimeOptions)
)
); );
} }
context.logger.info('ES5 bundle generation complete.'); context.logger.info('ES5 bundle generation complete.');
type ArrayElement<A> = A extends ReadonlyArray<infer T> ? T : never; type ArrayElement<A> = A extends ReadonlyArray<infer T>
? T
: never;
function generateBundleInfoStats( function generateBundleInfoStats(
id: string | number, id: string | number,
bundle: ProcessBundleFile, bundle: ProcessBundleFile,
chunk: ArrayElement<webpack.Stats.ToJsonOutput['chunks']> | undefined, chunk:
| ArrayElement<webpack.Stats.ToJsonOutput['chunks']>
| undefined
): string { ): string {
return generateBundleStats( return generateBundleStats(
{ {
id, id,
size: bundle.size, size: bundle.size,
files: bundle.map ? [bundle.filename, bundle.map.filename] : [bundle.filename], files: bundle.map
? [bundle.filename, bundle.map.filename]
: [bundle.filename],
names: chunk && chunk.names, names: chunk && chunk.names,
entry: !!chunk && chunk.names.includes('runtime'), entry: !!chunk && chunk.names.includes('runtime'),
initial: !!chunk && chunk.initial, initial: !!chunk && chunk.initial,
rendered: true, rendered: true
}, },
true, true
); );
} }
@ -468,14 +532,26 @@ export function buildWebpackBrowser(
const chunk = const chunk =
webpackStats && webpackStats &&
webpackStats.chunks && webpackStats.chunks &&
webpackStats.chunks.find(c => result.name === c.id.toString()); webpackStats.chunks.find(
c => result.name === c.id.toString()
);
if (result.original) { if (result.original) {
bundleInfoText += bundleInfoText +=
'\n' + generateBundleInfoStats(result.name, result.original, chunk); '\n' +
generateBundleInfoStats(
result.name,
result.original,
chunk
);
} }
if (result.downlevel) { if (result.downlevel) {
bundleInfoText += bundleInfoText +=
'\n' + generateBundleInfoStats(result.name, result.downlevel, chunk); '\n' +
generateBundleInfoStats(
result.name,
result.downlevel,
chunk
);
} }
} }
@ -486,9 +562,14 @@ export function buildWebpackBrowser(
} }
const asset = const asset =
webpackStats.assets && webpackStats.assets.find(a => a.name === chunk.files[0]); webpackStats.assets &&
webpackStats.assets.find(a => a.name === chunk.files[0]);
bundleInfoText += bundleInfoText +=
'\n' + generateBundleStats({ ...chunk, size: asset && asset.size }, true); '\n' +
generateBundleStats(
{ ...chunk, size: asset && asset.size },
true
);
} }
} }
@ -497,19 +578,25 @@ export function buildWebpackBrowser(
generateBuildStats( generateBuildStats(
(webpackStats && webpackStats.hash) || '<unknown>', (webpackStats && webpackStats.hash) || '<unknown>',
Date.now() - startTime, Date.now() - startTime,
true, true
); );
context.logger.info(bundleInfoText); context.logger.info(bundleInfoText);
if (webpackStats && webpackStats.warnings.length > 0) { if (webpackStats && webpackStats.warnings.length > 0) {
context.logger.warn(statsWarningsToString(webpackStats, { colors: true })); context.logger.warn(
statsWarningsToString(webpackStats, { colors: true })
);
} }
if (webpackStats && webpackStats.errors.length > 0) { if (webpackStats && webpackStats.errors.length > 0) {
context.logger.error(statsErrorsToString(webpackStats, { colors: true })); context.logger.error(
statsErrorsToString(webpackStats, { colors: true })
);
} }
} else { } else {
const { emittedFiles = [] } = firstBuild; const { emittedFiles = [] } = firstBuild;
files = emittedFiles.filter(x => x.name !== 'polyfills-es5'); files = emittedFiles.filter(x => x.name !== 'polyfills-es5');
noModuleFiles = emittedFiles.filter(x => x.name === 'polyfills-es5'); noModuleFiles = emittedFiles.filter(
x => x.name === 'polyfills-es5'
);
} }
if (options.index) { if (options.index) {
@ -517,7 +604,10 @@ export function buildWebpackBrowser(
host, host,
outputPath: resolve( outputPath: resolve(
root, root,
join(normalize(options.outputPath), getIndexOutputFile(options)), join(
normalize(options.outputPath),
getIndexOutputFile(options)
)
), ),
indexPath: join(root, getIndexInputFile(options)), indexPath: join(root, getIndexInputFile(options)),
files, files,
@ -529,11 +619,13 @@ export function buildWebpackBrowser(
scripts: options.scripts, scripts: options.scripts,
styles: options.styles, styles: options.styles,
postTransform: transforms.indexHtml, postTransform: transforms.indexHtml,
crossOrigin: options.crossOrigin, crossOrigin: options.crossOrigin
}) })
.pipe( .pipe(
map(() => ({ success: true })), map(() => ({ success: true })),
catchError(error => of({ success: false, error: mapErrorToMessage(error) })), catchError(error =>
of({ success: false, error: mapErrorToMessage(error) })
)
) )
.toPromise(); .toPromise();
} else { } else {
@ -552,11 +644,11 @@ export function buildWebpackBrowser(
projectRoot, projectRoot,
resolve(root, normalize(options.outputPath)), resolve(root, normalize(options.outputPath)),
options.baseHref || '/', options.baseHref || '/',
options.ngswConfigPath, options.ngswConfigPath
).then( ).then(
() => ({ success: true }), () => ({ success: true }),
error => ({ success: false, error: mapErrorToMessage(error) }), error => ({ success: false, error: mapErrorToMessage(error) })
), )
); );
} else { } else {
return of(buildEvent); return of(buildEvent);
@ -567,11 +659,14 @@ export function buildWebpackBrowser(
({ ({
...event, ...event,
// If we use differential loading, both configs have the same outputs // If we use differential loading, both configs have the same outputs
outputPath: path.resolve(context.workspaceRoot, options.outputPath), outputPath: path.resolve(
} as BrowserBuilderOutput), context.workspaceRoot,
), options.outputPath
)
} as BrowserBuilderOutput)
)
); );
}), })
); );
} }
@ -587,4 +682,6 @@ function mapErrorToMessage(error: unknown): string | undefined {
return undefined; return undefined;
} }
export default createBuilder<json.JsonObject & BrowserBuilderSchema>(buildWebpackBrowser); export default createBuilder<json.JsonObject & BrowserBuilderSchema>(
buildWebpackBrowser
);

View File

@ -1,4 +1,3 @@
// THIS FILE IS AUTOMATICALLY GENERATED. TO UPDATE THIS FILE YOU NEED TO CHANGE THE // THIS FILE IS AUTOMATICALLY GENERATED. TO UPDATE THIS FILE YOU NEED TO CHANGE THE
// CORRESPONDING JSON SCHEMA FILE, THEN RUN devkit-admin build (or bazel build ...). // CORRESPONDING JSON SCHEMA FILE, THEN RUN devkit-admin build (or bazel build ...).
@ -9,324 +8,324 @@
* Browser target options * Browser target options
*/ */
export interface Schema { export interface Schema {
/** /**
* Build using Ahead of Time compilation. * Build using Ahead of Time compilation.
*/ */
aot?: boolean; aot?: boolean;
/** /**
* List of static application assets. * List of static application assets.
*/ */
assets?: AssetPattern[]; assets?: AssetPattern[];
/** /**
* Base url for the application being built. * Base url for the application being built.
*/ */
baseHref?: string; baseHref?: string;
/** /**
* Budget thresholds to ensure parts of your application stay within boundaries which you * Budget thresholds to ensure parts of your application stay within boundaries which you
* set. * set.
*/ */
budgets?: Budget[]; budgets?: Budget[];
/** /**
* Enables '@angular-devkit/build-optimizer' optimizations when using the 'aot' option. * Enables '@angular-devkit/build-optimizer' optimizations when using the 'aot' option.
*/ */
buildOptimizer?: boolean; buildOptimizer?: boolean;
/** /**
* Use a separate bundle containing code used across multiple bundles. * Use a separate bundle containing code used across multiple bundles.
*/ */
commonChunk?: boolean; commonChunk?: boolean;
/** /**
* Define the crossorigin attribute setting of elements that provide CORS support. * Define the crossorigin attribute setting of elements that provide CORS support.
*/ */
crossOrigin?: CrossOrigin; crossOrigin?: CrossOrigin;
/** /**
* Delete the output path before building. * Delete the output path before building.
*/ */
deleteOutputPath?: boolean; deleteOutputPath?: boolean;
/** /**
* URL where files will be deployed. * URL where files will be deployed.
*/ */
deployUrl?: string; deployUrl?: string;
/** /**
* Enables conditionally loaded ES2015 polyfills. * Enables conditionally loaded ES2015 polyfills.
* @deprecated This will be determined from the list of supported browsers specified in the * @deprecated This will be determined from the list of supported browsers specified in the
* 'browserslist' file. * 'browserslist' file.
*/ */
es5BrowserSupport?: boolean; es5BrowserSupport?: boolean;
/** /**
* Output in-file eval sourcemaps. * Output in-file eval sourcemaps.
* @deprecated * @deprecated
*/ */
evalSourceMap?: boolean; evalSourceMap?: boolean;
/** /**
* Concatenate modules with Rollup before bundling them with Webpack. * Concatenate modules with Rollup before bundling them with Webpack.
*/ */
experimentalRollupPass?: boolean; experimentalRollupPass?: boolean;
/** /**
* Extract css from global styles into css files instead of js ones. * Extract css from global styles into css files instead of js ones.
*/ */
extractCss?: boolean; extractCss?: boolean;
/** /**
* Extract all licenses in a separate file. * Extract all licenses in a separate file.
*/ */
extractLicenses?: boolean; extractLicenses?: boolean;
/** /**
* Replace files with other files in the build. * Replace files with other files in the build.
*/ */
fileReplacements?: FileReplacement[]; fileReplacements?: FileReplacement[];
/** /**
* Run the TypeScript type checker in a forked process. * Run the TypeScript type checker in a forked process.
*/ */
forkTypeChecker?: boolean; forkTypeChecker?: boolean;
/** /**
* Localization file to use for i18n. * Localization file to use for i18n.
* @deprecated Use 'locales' object in the project metadata instead. * @deprecated Use 'locales' object in the project metadata instead.
*/ */
i18nFile?: string; i18nFile?: string;
/** /**
* Format of the localization file specified with --i18n-file. * Format of the localization file specified with --i18n-file.
* @deprecated No longer needed as the format will be determined automatically. * @deprecated No longer needed as the format will be determined automatically.
*/ */
i18nFormat?: string; i18nFormat?: string;
/** /**
* Locale to use for i18n. * Locale to use for i18n.
* @deprecated Use 'localize' instead. * @deprecated Use 'localize' instead.
*/ */
i18nLocale?: string; i18nLocale?: string;
/** /**
* How to handle missing translations for i18n. * How to handle missing translations for i18n.
*/ */
i18nMissingTranslation?: I18NMissingTranslation; i18nMissingTranslation?: I18NMissingTranslation;
/** /**
* Configures the generation of the application's HTML index. * Configures the generation of the application's HTML index.
*/ */
index: IndexUnion; index: IndexUnion;
/** /**
* List of additional NgModule files that will be lazy loaded. Lazy router modules will be * List of additional NgModule files that will be lazy loaded. Lazy router modules will be
* discovered automatically. * discovered automatically.
* @deprecated 'SystemJsNgModuleLoader' is deprecated, and this is part of its usage. Use * @deprecated 'SystemJsNgModuleLoader' is deprecated, and this is part of its usage. Use
* 'import()' syntax instead. * 'import()' syntax instead.
*/ */
lazyModules?: string[]; lazyModules?: string[];
localize?: Localize; localize?: Localize;
/** /**
* The full path for the main entry point to the app, relative to the current workspace. * The full path for the main entry point to the app, relative to the current workspace.
*/ */
main: string; main: string;
/** /**
* Use file name for lazy loaded chunks. * Use file name for lazy loaded chunks.
*/ */
namedChunks?: boolean; namedChunks?: boolean;
/** /**
* Path to ngsw-config.json. * Path to ngsw-config.json.
*/ */
ngswConfigPath?: string; ngswConfigPath?: string;
/** /**
* Enables optimization of the build output. * Enables optimization of the build output.
*/ */
optimization?: OptimizationUnion; optimization?: OptimizationUnion;
/** /**
* Define the output filename cache-busting hashing mode. * Define the output filename cache-busting hashing mode.
*/ */
outputHashing?: OutputHashing; outputHashing?: OutputHashing;
/** /**
* The full path for the new output directory, relative to the current workspace. * The full path for the new output directory, relative to the current workspace.
* *
* By default, writes output to a folder named dist/ in the current project. * By default, writes output to a folder named dist/ in the current project.
*/ */
outputPath: string; outputPath: string;
/** /**
* Enable and define the file watching poll time period in milliseconds. * Enable and define the file watching poll time period in milliseconds.
*/ */
poll?: number; poll?: number;
/** /**
* The full path for the polyfills file, relative to the current workspace. * The full path for the polyfills file, relative to the current workspace.
*/ */
polyfills?: string; polyfills?: string;
/** /**
* Do not use the real path when resolving modules. * Do not use the real path when resolving modules.
*/ */
preserveSymlinks?: boolean; preserveSymlinks?: boolean;
/** /**
* Output profile events for Chrome profiler. * Output profile events for Chrome profiler.
* @deprecated Use "NG_BUILD_PROFILING" environment variable instead. * @deprecated Use "NG_BUILD_PROFILING" environment variable instead.
*/ */
profile?: boolean; profile?: boolean;
/** /**
* Log progress to the console while building. * Log progress to the console while building.
*/ */
progress?: boolean; progress?: boolean;
/** /**
* Change root relative URLs in stylesheets to include base HREF and deploy URL. Use only * Change root relative URLs in stylesheets to include base HREF and deploy URL. Use only
* for compatibility and transition. The behavior of this option is non-standard and will be * for compatibility and transition. The behavior of this option is non-standard and will be
* removed in the next major release. * removed in the next major release.
* @deprecated * @deprecated
*/ */
rebaseRootRelativeCssUrls?: boolean; rebaseRootRelativeCssUrls?: boolean;
/** /**
* The path where style resources will be placed, relative to outputPath. * The path where style resources will be placed, relative to outputPath.
*/ */
resourcesOutputPath?: string; resourcesOutputPath?: string;
/** /**
* Global scripts to be included in the build. * Global scripts to be included in the build.
*/ */
scripts?: ExtraEntryPoint[]; scripts?: ExtraEntryPoint[];
/** /**
* Generates a service worker config for production builds. * Generates a service worker config for production builds.
*/ */
serviceWorker?: boolean; serviceWorker?: boolean;
/** /**
* Show circular dependency warnings on builds. * Show circular dependency warnings on builds.
*/ */
showCircularDependencies?: boolean; showCircularDependencies?: boolean;
/** /**
* Flag to prevent building an app shell. * Flag to prevent building an app shell.
* @deprecated * @deprecated
*/ */
skipAppShell?: boolean; skipAppShell?: boolean;
/** /**
* Output sourcemaps. * Output sourcemaps.
*/ */
sourceMap?: SourceMapUnion; sourceMap?: SourceMapUnion;
/** /**
* Generates a 'stats.json' file which can be analyzed using tools such as * Generates a 'stats.json' file which can be analyzed using tools such as
* 'webpack-bundle-analyzer'. * 'webpack-bundle-analyzer'.
*/ */
statsJson?: boolean; statsJson?: boolean;
/** /**
* Options to pass to style preprocessors. * Options to pass to style preprocessors.
*/ */
stylePreprocessorOptions?: StylePreprocessorOptions; stylePreprocessorOptions?: StylePreprocessorOptions;
/** /**
* Global styles to be included in the build. * Global styles to be included in the build.
*/ */
styles?: ExtraEntryPoint[]; styles?: ExtraEntryPoint[];
/** /**
* Enables the use of subresource integrity validation. * Enables the use of subresource integrity validation.
*/ */
subresourceIntegrity?: boolean; subresourceIntegrity?: boolean;
/** /**
* The full path for the TypeScript configuration file, relative to the current workspace. * The full path for the TypeScript configuration file, relative to the current workspace.
*/ */
tsConfig: string; tsConfig: string;
/** /**
* Use a separate bundle containing only vendor libraries. * Use a separate bundle containing only vendor libraries.
*/ */
vendorChunk?: boolean; vendorChunk?: boolean;
/** /**
* Resolve vendor packages sourcemaps. * Resolve vendor packages sourcemaps.
* @deprecated * @deprecated
*/ */
vendorSourceMap?: boolean; vendorSourceMap?: boolean;
/** /**
* Adds more details to output logging. * Adds more details to output logging.
*/ */
verbose?: boolean; verbose?: boolean;
/** /**
* Run build when files change. * Run build when files change.
*/ */
watch?: boolean; watch?: boolean;
/** /**
* TypeScript configuration for Web Worker modules. * TypeScript configuration for Web Worker modules.
*/ */
webWorkerTsConfig?: string; webWorkerTsConfig?: string;
} }
export type AssetPattern = AssetPatternClass | string; export type AssetPattern = AssetPatternClass | string;
export interface AssetPatternClass { export interface AssetPatternClass {
/** /**
* The pattern to match. * The pattern to match.
*/ */
glob: string; glob: string;
/** /**
* An array of globs to ignore. * An array of globs to ignore.
*/ */
ignore?: string[]; ignore?: string[];
/** /**
* The input directory path in which to apply 'glob'. Defaults to the project root. * The input directory path in which to apply 'glob'. Defaults to the project root.
*/ */
input: string; input: string;
/** /**
* Absolute path within the output. * Absolute path within the output.
*/ */
output: string; output: string;
} }
export interface Budget { export interface Budget {
/** /**
* The baseline size for comparison. * The baseline size for comparison.
*/ */
baseline?: string; baseline?: string;
/** /**
* The threshold for error relative to the baseline (min & max). * The threshold for error relative to the baseline (min & max).
*/ */
error?: string; error?: string;
/** /**
* The maximum threshold for error relative to the baseline. * The maximum threshold for error relative to the baseline.
*/ */
maximumError?: string; maximumError?: string;
/** /**
* The maximum threshold for warning relative to the baseline. * The maximum threshold for warning relative to the baseline.
*/ */
maximumWarning?: string; maximumWarning?: string;
/** /**
* The minimum threshold for error relative to the baseline. * The minimum threshold for error relative to the baseline.
*/ */
minimumError?: string; minimumError?: string;
/** /**
* The minimum threshold for warning relative to the baseline. * The minimum threshold for warning relative to the baseline.
*/ */
minimumWarning?: string; minimumWarning?: string;
/** /**
* The name of the bundle. * The name of the bundle.
*/ */
name?: string; name?: string;
/** /**
* The type of budget. * The type of budget.
*/ */
type: Type; type: Type;
/** /**
* The threshold for warning relative to the baseline (min & max). * The threshold for warning relative to the baseline (min & max).
*/ */
warning?: string; warning?: string;
} }
/** /**
* The type of budget. * The type of budget.
*/ */
export enum Type { export enum Type {
All = "all", All = 'all',
AllScript = "allScript", AllScript = 'allScript',
Any = "any", Any = 'any',
AnyComponentStyle = "anyComponentStyle", AnyComponentStyle = 'anyComponentStyle',
AnyScript = "anyScript", AnyScript = 'anyScript',
Bundle = "bundle", Bundle = 'bundle',
Initial = "initial", Initial = 'initial'
} }
/** /**
* Define the crossorigin attribute setting of elements that provide CORS support. * Define the crossorigin attribute setting of elements that provide CORS support.
*/ */
export enum CrossOrigin { export enum CrossOrigin {
Anonymous = "anonymous", Anonymous = 'anonymous',
None = "none", None = 'none',
UseCredentials = "use-credentials", UseCredentials = 'use-credentials'
} }
export interface FileReplacement { export interface FileReplacement {
replace?: string; replace?: string;
replaceWith?: string; replaceWith?: string;
src?: string; src?: string;
with?: string; with?: string;
} }
/** /**
* How to handle missing translations for i18n. * How to handle missing translations for i18n.
*/ */
export enum I18NMissingTranslation { export enum I18NMissingTranslation {
Error = "error", Error = 'error',
Ignore = "ignore", Ignore = 'ignore',
Warning = "warning", Warning = 'warning'
} }
/** /**
@ -335,15 +334,15 @@ export enum I18NMissingTranslation {
export type IndexUnion = IndexObject | string; export type IndexUnion = IndexObject | string;
export interface IndexObject { export interface IndexObject {
/** /**
* The path of a file to use for the application's generated HTML index. * The path of a file to use for the application's generated HTML index.
*/ */
input: string; input: string;
/** /**
* The output path of the application's generated HTML index file. The full provided path * The output path of the application's generated HTML index file. The full provided path
* will be used and will be considered relative to the application's configured output path. * will be used and will be considered relative to the application's configured output path.
*/ */
output?: string; output?: string;
} }
export type Localize = string[] | boolean; export type Localize = string[] | boolean;
@ -354,45 +353,45 @@ export type Localize = string[] | boolean;
export type OptimizationUnion = boolean | OptimizationClass; export type OptimizationUnion = boolean | OptimizationClass;
export interface OptimizationClass { export interface OptimizationClass {
/** /**
* Enables optimization of the scripts output. * Enables optimization of the scripts output.
*/ */
scripts?: boolean; scripts?: boolean;
/** /**
* Enables optimization of the styles output. * Enables optimization of the styles output.
*/ */
styles?: boolean; styles?: boolean;
} }
/** /**
* Define the output filename cache-busting hashing mode. * Define the output filename cache-busting hashing mode.
*/ */
export enum OutputHashing { export enum OutputHashing {
All = "all", All = 'all',
Bundles = "bundles", Bundles = 'bundles',
Media = "media", Media = 'media',
None = "none", None = 'none'
} }
export type ExtraEntryPoint = ExtraEntryPointClass | string; export type ExtraEntryPoint = ExtraEntryPointClass | string;
export interface ExtraEntryPointClass { export interface ExtraEntryPointClass {
/** /**
* The bundle name for this extra entry point. * The bundle name for this extra entry point.
*/ */
bundleName?: string; bundleName?: string;
/** /**
* If the bundle will be referenced in the HTML file. * If the bundle will be referenced in the HTML file.
*/ */
inject?: boolean; inject?: boolean;
/** /**
* The file to include. * The file to include.
*/ */
input: string; input: string;
/** /**
* If the bundle will be lazy loaded. * If the bundle will be lazy loaded.
*/ */
lazy?: boolean; lazy?: boolean;
} }
/** /**
@ -401,30 +400,30 @@ export interface ExtraEntryPointClass {
export type SourceMapUnion = boolean | SourceMapClass; export type SourceMapUnion = boolean | SourceMapClass;
export interface SourceMapClass { export interface SourceMapClass {
/** /**
* Output sourcemaps used for error reporting tools. * Output sourcemaps used for error reporting tools.
*/ */
hidden?: boolean; hidden?: boolean;
/** /**
* Output sourcemaps for all scripts. * Output sourcemaps for all scripts.
*/ */
scripts?: boolean; scripts?: boolean;
/** /**
* Output sourcemaps for all styles. * Output sourcemaps for all styles.
*/ */
styles?: boolean; styles?: boolean;
/** /**
* Resolve vendor packages sourcemaps. * Resolve vendor packages sourcemaps.
*/ */
vendor?: boolean; vendor?: boolean;
} }
/** /**
* Options to pass to style preprocessors. * Options to pass to style preprocessors.
*/ */
export interface StylePreprocessorOptions { export interface StylePreprocessorOptions {
/** /**
* Paths to include. Paths will be resolved to project root. * Paths to include. Paths will be resolved to project root.
*/ */
includePaths?: string[]; includePaths?: string[];
} }

View File

@ -22,9 +22,11 @@ export class BuildBrowserFeatures {
constructor( constructor(
private projectRoot: string, private projectRoot: string,
private scriptTarget: ts.ScriptTarget, private scriptTarget: ts.ScriptTarget
) { ) {
this._supportedBrowsers = browserslist(undefined, { path: this.projectRoot }); this._supportedBrowsers = browserslist(undefined, {
path: this.projectRoot
});
this._es6TargetOrLater = this.scriptTarget > ts.ScriptTarget.ES5; this._es6TargetOrLater = this.scriptTarget > ts.ScriptTarget.ES5;
} }
@ -54,12 +56,11 @@ export class BuildBrowserFeatures {
return false; return false;
} }
const safariBrowsers = [ const safariBrowsers = ['safari 10.1', 'ios_saf 10.3'];
'safari 10.1',
'ios_saf 10.3',
];
return this._supportedBrowsers.some(browser => safariBrowsers.includes(browser)); return this._supportedBrowsers.some(browser =>
safariBrowsers.includes(browser)
);
} }
/** /**
@ -70,24 +71,22 @@ export class BuildBrowserFeatures {
// n: feature is unavailable // n: feature is unavailable
// a: feature is partially supported // a: feature is partially supported
// x: feature is prefixed // x: feature is prefixed
const criteria = [ const criteria = ['y', 'a'];
'y',
'a',
];
const data = feature(features[featureId]); const data = feature(features[featureId]);
return !this._supportedBrowsers return !this._supportedBrowsers.some(browser => {
.some(browser => { const [agentId, version] = browser.split(' ');
const [agentId, version] = browser.split(' ');
const browserData = data.stats[agentId]; const browserData = data.stats[agentId];
const featureStatus = (browserData && browserData[version]) as string | undefined; const featureStatus = (browserData && browserData[version]) as
| string
| undefined;
// We are only interested in the first character // We are only interested in the first character
// Ex: when 'a #4 #5', we only need to check for 'a' // Ex: when 'a #4 #5', we only need to check for 'a'
// as for such cases we should polyfill these features as needed // as for such cases we should polyfill these features as needed
return !featureStatus || !criteria.includes(featureStatus.charAt(0)); return !featureStatus || !criteria.includes(featureStatus.charAt(0));
}); });
} }
} }

View File

@ -6,7 +6,6 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import { TestProjectHost } from '@angular-devkit/architect/testing'; import { TestProjectHost } from '@angular-devkit/architect/testing';
import { getSystemPath, join } from '@angular-devkit/core'; import { getSystemPath, join } from '@angular-devkit/core';
import { ScriptTarget } from 'typescript'; import { ScriptTarget } from 'typescript';
@ -15,7 +14,8 @@ import { BuildBrowserFeatures } from './build-browser-features';
const devkitRoot = (global as any)._DevKitRoot; // tslint:disable-line:no-any const devkitRoot = (global as any)._DevKitRoot; // tslint:disable-line:no-any
const workspaceRoot = join( const workspaceRoot = join(
devkitRoot, devkitRoot,
'tests/angular_devkit/build_angular/hello-world-app/'); 'tests/angular_devkit/build_angular/hello-world-app/'
);
const host = new TestProjectHost(workspaceRoot); const host = new TestProjectHost(workspaceRoot);
@ -31,48 +31,48 @@ describe('BuildBrowserFeatures', () => {
describe('isDifferentialLoadingNeeded', () => { describe('isDifferentialLoadingNeeded', () => {
it('should be true for for IE 9-11 and ES2015', () => { it('should be true for for IE 9-11 and ES2015', () => {
host.writeMultipleFiles({ host.writeMultipleFiles({
'browserslist': 'IE 9-11', browserslist: 'IE 9-11'
}); });
const buildBrowserFeatures = new BuildBrowserFeatures( const buildBrowserFeatures = new BuildBrowserFeatures(
workspaceRootSysPath, workspaceRootSysPath,
ScriptTarget.ES2015, ScriptTarget.ES2015
); );
expect(buildBrowserFeatures.isDifferentialLoadingNeeded()).toBe(true); expect(buildBrowserFeatures.isDifferentialLoadingNeeded()).toBe(true);
}); });
it('should be false for Chrome and ES2015', () => { it('should be false for Chrome and ES2015', () => {
host.writeMultipleFiles({ host.writeMultipleFiles({
'browserslist': 'last 1 chrome version', browserslist: 'last 1 chrome version'
}); });
const buildBrowserFeatures = new BuildBrowserFeatures( const buildBrowserFeatures = new BuildBrowserFeatures(
workspaceRootSysPath, workspaceRootSysPath,
ScriptTarget.ES2015, ScriptTarget.ES2015
); );
expect(buildBrowserFeatures.isDifferentialLoadingNeeded()).toBe(false); expect(buildBrowserFeatures.isDifferentialLoadingNeeded()).toBe(false);
}); });
it('detects no need for differential loading for target is ES5', () => { it('detects no need for differential loading for target is ES5', () => {
host.writeMultipleFiles({ host.writeMultipleFiles({
'browserslist': 'last 1 chrome version', browserslist: 'last 1 chrome version'
}); });
const buildBrowserFeatures = new BuildBrowserFeatures( const buildBrowserFeatures = new BuildBrowserFeatures(
workspaceRootSysPath, workspaceRootSysPath,
ScriptTarget.ES5, ScriptTarget.ES5
); );
expect(buildBrowserFeatures.isDifferentialLoadingNeeded()).toBe(false); expect(buildBrowserFeatures.isDifferentialLoadingNeeded()).toBe(false);
}); });
it('should be false for Safari 10.1 when target is ES2015', () => { it('should be false for Safari 10.1 when target is ES2015', () => {
host.writeMultipleFiles({ host.writeMultipleFiles({
'browserslist': 'Safari 10.1', browserslist: 'Safari 10.1'
}); });
const buildBrowserFeatures = new BuildBrowserFeatures( const buildBrowserFeatures = new BuildBrowserFeatures(
workspaceRootSysPath, workspaceRootSysPath,
ScriptTarget.ES2015, ScriptTarget.ES2015
); );
expect(buildBrowserFeatures.isDifferentialLoadingNeeded()).toBe(false); expect(buildBrowserFeatures.isDifferentialLoadingNeeded()).toBe(false);
}); });
@ -81,48 +81,48 @@ describe('BuildBrowserFeatures', () => {
describe('isFeatureSupported', () => { describe('isFeatureSupported', () => {
it('should be true for es6-module and Safari 10.1', () => { it('should be true for es6-module and Safari 10.1', () => {
host.writeMultipleFiles({ host.writeMultipleFiles({
'browserslist': 'Safari 10.1', browserslist: 'Safari 10.1'
}); });
const buildBrowserFeatures = new BuildBrowserFeatures( const buildBrowserFeatures = new BuildBrowserFeatures(
workspaceRootSysPath, workspaceRootSysPath,
ScriptTarget.ES2015, ScriptTarget.ES2015
); );
expect(buildBrowserFeatures.isFeatureSupported('es6-module')).toBe(true); expect(buildBrowserFeatures.isFeatureSupported('es6-module')).toBe(true);
}); });
it('should be false for es6-module and IE9', () => { it('should be false for es6-module and IE9', () => {
host.writeMultipleFiles({ host.writeMultipleFiles({
'browserslist': 'IE 9', browserslist: 'IE 9'
}); });
const buildBrowserFeatures = new BuildBrowserFeatures( const buildBrowserFeatures = new BuildBrowserFeatures(
workspaceRootSysPath, workspaceRootSysPath,
ScriptTarget.ES2015, ScriptTarget.ES2015
); );
expect(buildBrowserFeatures.isFeatureSupported('es6-module')).toBe(false); expect(buildBrowserFeatures.isFeatureSupported('es6-module')).toBe(false);
}); });
it('should be true for es6-module and last 1 chrome version', () => { it('should be true for es6-module and last 1 chrome version', () => {
host.writeMultipleFiles({ host.writeMultipleFiles({
'browserslist': 'last 1 chrome version', browserslist: 'last 1 chrome version'
}); });
const buildBrowserFeatures = new BuildBrowserFeatures( const buildBrowserFeatures = new BuildBrowserFeatures(
workspaceRootSysPath, workspaceRootSysPath,
ScriptTarget.ES2015, ScriptTarget.ES2015
); );
expect(buildBrowserFeatures.isFeatureSupported('es6-module')).toBe(true); expect(buildBrowserFeatures.isFeatureSupported('es6-module')).toBe(true);
}); });
it('should be true for es6-module and Edge 18', () => { it('should be true for es6-module and Edge 18', () => {
host.writeMultipleFiles({ host.writeMultipleFiles({
'browserslist': 'Edge 18', browserslist: 'Edge 18'
}); });
const buildBrowserFeatures = new BuildBrowserFeatures( const buildBrowserFeatures = new BuildBrowserFeatures(
workspaceRootSysPath, workspaceRootSysPath,
ScriptTarget.ES2015, ScriptTarget.ES2015
); );
expect(buildBrowserFeatures.isFeatureSupported('es6-module')).toBe(true); expect(buildBrowserFeatures.isFeatureSupported('es6-module')).toBe(true);
}); });
@ -131,63 +131,63 @@ describe('BuildBrowserFeatures', () => {
describe('isNoModulePolyfillNeeded', () => { describe('isNoModulePolyfillNeeded', () => {
it('should be false for Safari 10.1 when target is ES5', () => { it('should be false for Safari 10.1 when target is ES5', () => {
host.writeMultipleFiles({ host.writeMultipleFiles({
'browserslist': 'Safari 10.1', browserslist: 'Safari 10.1'
}); });
const buildBrowserFeatures = new BuildBrowserFeatures( const buildBrowserFeatures = new BuildBrowserFeatures(
workspaceRootSysPath, workspaceRootSysPath,
ScriptTarget.ES5, ScriptTarget.ES5
); );
expect(buildBrowserFeatures.isNoModulePolyfillNeeded()).toBe(false); expect(buildBrowserFeatures.isNoModulePolyfillNeeded()).toBe(false);
}); });
it('should be false for Safari 10.1 when target is ES2015', () => { it('should be false for Safari 10.1 when target is ES2015', () => {
host.writeMultipleFiles({ host.writeMultipleFiles({
'browserslist': 'Safari 10.1', browserslist: 'Safari 10.1'
}); });
const buildBrowserFeatures = new BuildBrowserFeatures( const buildBrowserFeatures = new BuildBrowserFeatures(
workspaceRootSysPath, workspaceRootSysPath,
ScriptTarget.ES2015, ScriptTarget.ES2015
); );
expect(buildBrowserFeatures.isNoModulePolyfillNeeded()).toBe(false); expect(buildBrowserFeatures.isNoModulePolyfillNeeded()).toBe(false);
}); });
it('should be true for Safari 9+ when target is ES2015', () => { it('should be true for Safari 9+ when target is ES2015', () => {
host.writeMultipleFiles({ host.writeMultipleFiles({
'browserslist': 'Safari >= 9', browserslist: 'Safari >= 9'
}); });
const buildBrowserFeatures = new BuildBrowserFeatures( const buildBrowserFeatures = new BuildBrowserFeatures(
workspaceRootSysPath, workspaceRootSysPath,
ScriptTarget.ES2015, ScriptTarget.ES2015
); );
expect(buildBrowserFeatures.isNoModulePolyfillNeeded()).toBe(true); expect(buildBrowserFeatures.isNoModulePolyfillNeeded()).toBe(true);
}); });
it('should be false for Safari 9+ when target is ES5', () => { it('should be false for Safari 9+ when target is ES5', () => {
host.writeMultipleFiles({ host.writeMultipleFiles({
'browserslist': 'Safari >= 9', browserslist: 'Safari >= 9'
}); });
const buildBrowserFeatures = new BuildBrowserFeatures( const buildBrowserFeatures = new BuildBrowserFeatures(
workspaceRootSysPath, workspaceRootSysPath,
ScriptTarget.ES5, ScriptTarget.ES5
); );
expect(buildBrowserFeatures.isNoModulePolyfillNeeded()).toBe(false); expect(buildBrowserFeatures.isNoModulePolyfillNeeded()).toBe(false);
}); });
it('should be false when not supporting Safari 10.1 target is ES2015', () => { it('should be false when not supporting Safari 10.1 target is ES2015', () => {
host.writeMultipleFiles({ host.writeMultipleFiles({
'browserslist': ` browserslist: `
Edge 18 Edge 18
IE 9 IE 9
`, `
}); });
const buildBrowserFeatures = new BuildBrowserFeatures( const buildBrowserFeatures = new BuildBrowserFeatures(
workspaceRootSysPath, workspaceRootSysPath,
ScriptTarget.ES2015, ScriptTarget.ES2015
); );
expect(buildBrowserFeatures.isNoModulePolyfillNeeded()).toBe(false); expect(buildBrowserFeatures.isNoModulePolyfillNeeded()).toBe(false);
}); });

View File

@ -12,14 +12,18 @@ import { concatMap, last } from 'rxjs/operators';
/** /**
* Delete an output directory, but error out if it's the root of the project. * Delete an output directory, but error out if it's the root of the project.
*/ */
export function deleteOutputDir(root: Path, outputPath: Path, host: virtualFs.Host) { export function deleteOutputDir(
root: Path,
outputPath: Path,
host: virtualFs.Host
) {
const resolvedOutputPath = resolve(root, outputPath); const resolvedOutputPath = resolve(root, outputPath);
if (resolvedOutputPath === root) { if (resolvedOutputPath === root) {
throw new Error('Output path MUST not be project root directory!'); throw new Error('Output path MUST not be project root directory!');
} }
return host.exists(resolvedOutputPath).pipe( return host.exists(resolvedOutputPath).pipe(
concatMap(exists => exists ? host.delete(resolvedOutputPath) : EMPTY), concatMap(exists => (exists ? host.delete(resolvedOutputPath) : EMPTY)),
last(null, null), last(null, null)
); );
} }

View File

@ -7,4 +7,5 @@
*/ */
const mangleVariable = process.env['NG_BUILD_MANGLE']; const mangleVariable = process.env['NG_BUILD_MANGLE'];
export const manglingDisabled = export const manglingDisabled =
!!mangleVariable && (mangleVariable === '0' || mangleVariable.toLowerCase() === 'false'); !!mangleVariable &&
(mangleVariable === '0' || mangleVariable.toLowerCase() === 'false');

View File

@ -14,11 +14,10 @@ import {
normalize, normalize,
relative, relative,
resolve, resolve,
virtualFs, virtualFs
} from '@angular-devkit/core'; } from '@angular-devkit/core';
import { AssetPattern, AssetPatternClass } from '../browser/schema'; import { AssetPattern, AssetPatternClass } from '../browser/schema';
export class MissingAssetSourceRootException extends BaseException { export class MissingAssetSourceRootException extends BaseException {
constructor(path: String) { constructor(path: String) {
super(`The ${path} asset path must start with the project source root.`); super(`The ${path} asset path must start with the project source root.`);
@ -30,7 +29,7 @@ export function normalizeAssetPatterns(
host: virtualFs.SyncDelegateHost, host: virtualFs.SyncDelegateHost,
root: Path, root: Path,
projectRoot: Path, projectRoot: Path,
maybeSourceRoot: Path | undefined, maybeSourceRoot: Path | undefined
): AssetPatternClass[] { ): AssetPatternClass[] {
// When sourceRoot is not available, we default to ${projectRoot}/src. // When sourceRoot is not available, we default to ${projectRoot}/src.
const sourceRoot = maybeSourceRoot || join(projectRoot, 'src'); const sourceRoot = maybeSourceRoot || join(projectRoot, 'src');
@ -40,47 +39,46 @@ export function normalizeAssetPatterns(
return []; return [];
} }
return assetPatterns return assetPatterns.map(assetPattern => {
.map(assetPattern => { // Normalize string asset patterns to objects.
// Normalize string asset patterns to objects. if (typeof assetPattern === 'string') {
if (typeof assetPattern === 'string') { const assetPath = normalize(assetPattern);
const assetPath = normalize(assetPattern); const resolvedAssetPath = resolve(root, assetPath);
const resolvedAssetPath = resolve(root, assetPath);
// Check if the string asset is within sourceRoot. // Check if the string asset is within sourceRoot.
if (!resolvedAssetPath.startsWith(resolvedSourceRoot)) { if (!resolvedAssetPath.startsWith(resolvedSourceRoot)) {
throw new MissingAssetSourceRootException(assetPattern); throw new MissingAssetSourceRootException(assetPattern);
}
let glob: string, input: Path, output: Path;
let isDirectory = false;
try {
isDirectory = host.isDirectory(resolvedAssetPath);
} catch {
isDirectory = true;
}
if (isDirectory) {
// Folders get a recursive star glob.
glob = '**/*';
// Input directory is their original path.
input = assetPath;
} else {
// Files are their own glob.
glob = basename(assetPath);
// Input directory is their original dirname.
input = dirname(assetPath);
}
// Output directory for both is the relative path from source root to input.
output = relative(resolvedSourceRoot, resolve(root, input));
// Return the asset pattern in object format.
return { glob, input, output };
} else {
// It's already an AssetPatternObject, no need to convert.
return assetPattern;
} }
});
let glob: string, input: Path, output: Path;
let isDirectory = false;
try {
isDirectory = host.isDirectory(resolvedAssetPath);
} catch {
isDirectory = true;
}
if (isDirectory) {
// Folders get a recursive star glob.
glob = '**/*';
// Input directory is their original path.
input = assetPath;
} else {
// Files are their own glob.
glob = basename(assetPath);
// Input directory is their original dirname.
input = dirname(assetPath);
}
// Output directory for both is the relative path from source root to input.
output = relative(resolvedSourceRoot, resolve(root, input));
// Return the asset pattern in object format.
return { glob, input, output };
} else {
// It's already an AssetPatternObject, no need to convert.
return assetPattern;
}
});
} }

View File

@ -1,4 +1,3 @@
/** /**
* @license * @license
* Copyright Google Inc. All Rights Reserved. * Copyright Google Inc. All Rights Reserved.
@ -7,50 +6,62 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import { Path, virtualFs } from '@angular-devkit/core'; import { Path, virtualFs } from '@angular-devkit/core';
import { BuildOptions } from '../angular-cli-files/models/build-options'; import { BuildOptions } from '../angular-cli-files/models/build-options';
import { import {
AssetPatternClass, AssetPatternClass,
OptimizationClass, OptimizationClass,
Schema as BrowserBuilderSchema, Schema as BrowserBuilderSchema,
SourceMapClass, SourceMapClass
} from '../browser/schema'; } from '../browser/schema';
import { normalizeAssetPatterns } from './normalize-asset-patterns'; import { normalizeAssetPatterns } from './normalize-asset-patterns';
import { import {
NormalizedFileReplacement, NormalizedFileReplacement,
normalizeFileReplacements, normalizeFileReplacements
} from './normalize-file-replacements'; } from './normalize-file-replacements';
import { normalizeOptimization } from './normalize-optimization'; import { normalizeOptimization } from './normalize-optimization';
import { normalizeSourceMaps } from './normalize-source-maps'; import { normalizeSourceMaps } from './normalize-source-maps';
/** /**
* A normalized browser builder schema. * A normalized browser builder schema.
*/ */
export type NormalizedBrowserBuilderSchema = BrowserBuilderSchema & BuildOptions & { export type NormalizedBrowserBuilderSchema = BrowserBuilderSchema &
sourceMap: SourceMapClass; BuildOptions & {
assets: AssetPatternClass[]; sourceMap: SourceMapClass;
fileReplacements: NormalizedFileReplacement[]; assets: AssetPatternClass[];
optimization: OptimizationClass; fileReplacements: NormalizedFileReplacement[];
}; optimization: OptimizationClass;
};
export function normalizeBrowserSchema( export function normalizeBrowserSchema(
host: virtualFs.Host<{}>, host: virtualFs.Host<{}>,
root: Path, root: Path,
projectRoot: Path, projectRoot: Path,
sourceRoot: Path | undefined, sourceRoot: Path | undefined,
options: BrowserBuilderSchema, options: BrowserBuilderSchema
): NormalizedBrowserBuilderSchema { ): NormalizedBrowserBuilderSchema {
const syncHost = new virtualFs.SyncDelegateHost(host); const syncHost = new virtualFs.SyncDelegateHost(host);
const normalizedSourceMapOptions = normalizeSourceMaps(options.sourceMap || false); const normalizedSourceMapOptions = normalizeSourceMaps(
normalizedSourceMapOptions.vendor = normalizedSourceMapOptions.vendor || options.vendorSourceMap; options.sourceMap || false
);
normalizedSourceMapOptions.vendor =
normalizedSourceMapOptions.vendor || options.vendorSourceMap;
return { return {
...options, ...options,
assets: normalizeAssetPatterns(options.assets || [], syncHost, root, projectRoot, sourceRoot), assets: normalizeAssetPatterns(
fileReplacements: normalizeFileReplacements(options.fileReplacements || [], syncHost, root), options.assets || [],
syncHost,
root,
projectRoot,
sourceRoot
),
fileReplacements: normalizeFileReplacements(
options.fileReplacements || [],
syncHost,
root
),
optimization: normalizeOptimization(options.optimization), optimization: normalizeOptimization(options.optimization),
sourceMap: normalizedSourceMapOptions, sourceMap: normalizedSourceMapOptions,
@ -60,10 +71,11 @@ export function normalizeBrowserSchema(
scripts: options.scripts || [], scripts: options.scripts || [],
styles: options.styles || [], styles: options.styles || [],
stylePreprocessorOptions: { stylePreprocessorOptions: {
includePaths: options.stylePreprocessorOptions includePaths:
&& options.stylePreprocessorOptions.includePaths (options.stylePreprocessorOptions &&
|| [], options.stylePreprocessorOptions.includePaths) ||
[]
}, },
lazyModules: options.lazyModules || [], lazyModules: options.lazyModules || []
}; };
} }

View File

@ -12,11 +12,10 @@ import {
getSystemPath, getSystemPath,
join, join,
normalize, normalize,
virtualFs, virtualFs
} from '@angular-devkit/core'; } from '@angular-devkit/core';
import { FileReplacement } from '../browser/schema'; import { FileReplacement } from '../browser/schema';
export class MissingFileReplacementException extends BaseException { export class MissingFileReplacementException extends BaseException {
constructor(path: String) { constructor(path: String) {
super(`The ${path} path in file replacements does not exist.`); super(`The ${path} path in file replacements does not exist.`);
@ -31,14 +30,15 @@ export interface NormalizedFileReplacement {
export function normalizeFileReplacements( export function normalizeFileReplacements(
fileReplacements: FileReplacement[], fileReplacements: FileReplacement[],
host: virtualFs.SyncDelegateHost, host: virtualFs.SyncDelegateHost,
root: Path, root: Path
): NormalizedFileReplacement[] { ): NormalizedFileReplacement[] {
if (fileReplacements.length === 0) { if (fileReplacements.length === 0) {
return []; return [];
} }
const normalizedReplacement = fileReplacements const normalizedReplacement = fileReplacements.map(replacement =>
.map(replacement => normalizeFileReplacement(replacement, root)); normalizeFileReplacement(replacement, root)
);
for (const { replace, with: replacementWith } of normalizedReplacement) { for (const { replace, with: replacementWith } of normalizedReplacement) {
if (!host.exists(replacementWith)) { if (!host.exists(replacementWith)) {
@ -55,7 +55,7 @@ export function normalizeFileReplacements(
function normalizeFileReplacement( function normalizeFileReplacement(
fileReplacement: FileReplacement, fileReplacement: FileReplacement,
root?: Path, root?: Path
): NormalizedFileReplacement { ): NormalizedFileReplacement {
let replacePath: Path; let replacePath: Path;
let withPath: Path; let withPath: Path;
@ -66,7 +66,9 @@ function normalizeFileReplacement(
replacePath = normalize(fileReplacement.replace); replacePath = normalize(fileReplacement.replace);
withPath = normalize(fileReplacement.with); withPath = normalize(fileReplacement.with);
} else { } else {
throw new Error(`Invalid file replacement: ${JSON.stringify(fileReplacement)}`); throw new Error(
`Invalid file replacement: ${JSON.stringify(fileReplacement)}`
);
} }
// TODO: For 7.x should this only happen if not absolute? // TODO: For 7.x should this only happen if not absolute?

View File

@ -9,10 +9,12 @@
import { OptimizationClass, OptimizationUnion } from '../browser/schema'; import { OptimizationClass, OptimizationUnion } from '../browser/schema';
export function normalizeOptimization( export function normalizeOptimization(
optimization: OptimizationUnion = false, optimization: OptimizationUnion = false
): Required<OptimizationClass> { ): Required<OptimizationClass> {
return { return {
scripts: typeof optimization === 'object' ? !!optimization.scripts : optimization, scripts:
styles: typeof optimization === 'object' ? !!optimization.styles : optimization, typeof optimization === 'object' ? !!optimization.scripts : optimization,
styles:
typeof optimization === 'object' ? !!optimization.styles : optimization
}; };
} }

View File

@ -11,13 +11,13 @@ import { SourceMapClass, SourceMapUnion } from '../browser/schema';
export function normalizeSourceMaps(sourceMap: SourceMapUnion): SourceMapClass { export function normalizeSourceMaps(sourceMap: SourceMapUnion): SourceMapClass {
const scripts = typeof sourceMap === 'object' ? sourceMap.scripts : sourceMap; const scripts = typeof sourceMap === 'object' ? sourceMap.scripts : sourceMap;
const styles = typeof sourceMap === 'object' ? sourceMap.styles : sourceMap; const styles = typeof sourceMap === 'object' ? sourceMap.styles : sourceMap;
const hidden = typeof sourceMap === 'object' && sourceMap.hidden || false; const hidden = (typeof sourceMap === 'object' && sourceMap.hidden) || false;
const vendor = typeof sourceMap === 'object' && sourceMap.vendor || false; const vendor = (typeof sourceMap === 'object' && sourceMap.vendor) || false;
return { return {
vendor, vendor,
hidden, hidden,
scripts, scripts,
styles, styles
}; };
} }

View File

@ -6,4 +6,4 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
require('../../../../../lib/bootstrap-local'); require('../../../../../lib/bootstrap-local');
module.exports = require('./process-bundle.ts'); module.exports = require('./process-bundle.ts');

View File

@ -8,7 +8,11 @@
import { createHash } from 'crypto'; import { createHash } from 'crypto';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { RawSourceMap, SourceMapConsumer, SourceMapGenerator } from 'source-map'; import {
RawSourceMap,
SourceMapConsumer,
SourceMapGenerator
} from 'source-map';
import { minify } from 'terser'; import { minify } from 'terser';
import { ScriptTarget, transpileModule } from 'typescript'; import { ScriptTarget, transpileModule } from 'typescript';
import { SourceMapSource } from 'webpack-sources'; import { SourceMapSource } from 'webpack-sources';
@ -54,7 +58,7 @@ export const enum CacheKey {
OriginalCode = 0, OriginalCode = 0,
OriginalMap = 1, OriginalMap = 1,
DownlevelCode = 2, DownlevelCode = 2,
DownlevelMap = 3, DownlevelMap = 3
} }
let cachePath: string | undefined; let cachePath: string | undefined;
@ -63,15 +67,21 @@ export function setup(options: { cachePath: string }): void {
cachePath = options.cachePath; cachePath = options.cachePath;
} }
async function cachePut(content: string, key: string | null, integrity?: string): Promise<void> { async function cachePut(
content: string,
key: string | null,
integrity?: string
): Promise<void> {
if (cachePath && key) { if (cachePath && key) {
await cacache.put(cachePath, key, content, { await cacache.put(cachePath, key, content, {
metadata: { integrity }, metadata: { integrity }
}); });
} }
} }
export async function process(options: ProcessBundleOptions): Promise<ProcessBundleResult> { export async function process(
options: ProcessBundleOptions
): Promise<ProcessBundleResult> {
if (!options.cacheKeys) { if (!options.cacheKeys) {
options.cacheKeys = []; options.cacheKeys = [];
} }
@ -79,7 +89,10 @@ export async function process(options: ProcessBundleOptions): Promise<ProcessBun
const result: ProcessBundleResult = { name: options.name }; const result: ProcessBundleResult = { name: options.name };
if (options.integrityAlgorithm) { if (options.integrityAlgorithm) {
// Store unmodified code integrity value -- used for SRI value replacement // Store unmodified code integrity value -- used for SRI value replacement
result.integrity = generateIntegrityValue(options.integrityAlgorithm, options.code); result.integrity = generateIntegrityValue(
options.integrityAlgorithm,
options.code
);
} }
// Runtime chunk requires specialized handling // Runtime chunk requires specialized handling
@ -108,15 +121,18 @@ export async function process(options: ProcessBundleOptions): Promise<ProcessBun
fileName: downlevelFilename, fileName: downlevelFilename,
compilerOptions: { compilerOptions: {
sourceMap: !!sourceMap, sourceMap: !!sourceMap,
target: ScriptTarget.ES5, target: ScriptTarget.ES5
}, }
}); });
downlevelCode = transformResult.outputText; downlevelCode = transformResult.outputText;
if (sourceMap && transformResult.sourceMapText) { if (sourceMap && transformResult.sourceMapText) {
if (manualSourceMaps) { if (manualSourceMaps) {
downlevelMap = await mergeSourcemaps(sourceMap, JSON.parse(transformResult.sourceMapText)); downlevelMap = await mergeSourcemaps(
sourceMap,
JSON.parse(transformResult.sourceMapText)
);
} else { } else {
// More accurate but significantly more costly // More accurate but significantly more costly
const tempSource = new SourceMapSource( const tempSource = new SourceMapSource(
@ -124,7 +140,7 @@ export async function process(options: ProcessBundleOptions): Promise<ProcessBun
downlevelFilename, downlevelFilename,
JSON.parse(transformResult.sourceMapText), JSON.parse(transformResult.sourceMapText),
sourceCode, sourceCode,
sourceMap, sourceMap
); );
downlevelMap = tempSource.map(); downlevelMap = tempSource.map();
@ -137,7 +153,7 @@ export async function process(options: ProcessBundleOptions): Promise<ProcessBun
const minifyResult = terserMangle(downlevelCode, { const minifyResult = terserMangle(downlevelCode, {
filename: downlevelFilename, filename: downlevelFilename,
map: downlevelMap, map: downlevelMap,
compress: true, compress: true
}); });
downlevelCode = minifyResult.code; downlevelCode = minifyResult.code;
downlevelMap = minifyResult.map; downlevelMap = minifyResult.map;
@ -166,13 +182,13 @@ export async function process(options: ProcessBundleOptions): Promise<ProcessBun
downlevelFilename, downlevelFilename,
downlevelCode, downlevelCode,
mapContent, mapContent,
options.integrityAlgorithm, options.integrityAlgorithm
); );
await cachePut( await cachePut(
downlevelCode, downlevelCode,
options.cacheKeys[CacheKey.DownlevelCode], options.cacheKeys[CacheKey.DownlevelCode],
result.downlevel.integrity, result.downlevel.integrity
); );
fs.writeFileSync(downlevelPath, downlevelCode); fs.writeFileSync(downlevelPath, downlevelCode);
} }
@ -183,7 +199,7 @@ export async function process(options: ProcessBundleOptions): Promise<ProcessBun
options.filename, options.filename,
options.code, options.code,
options.map, options.map,
options.integrityAlgorithm, options.integrityAlgorithm
); );
} }
@ -205,7 +221,7 @@ async function mergeSourcemaps(first: RawSourceMap, second: RawSourceMap) {
} }
const originalPosition = originalConsumer.originalPositionFor({ const originalPosition = originalConsumer.originalPositionFor({
line: mapping.originalLine, line: mapping.originalLine,
column: mapping.originalColumn, column: mapping.originalColumn
}); });
if ( if (
originalPosition.line === null || originalPosition.line === null ||
@ -217,14 +233,14 @@ async function mergeSourcemaps(first: RawSourceMap, second: RawSourceMap) {
generator.addMapping({ generator.addMapping({
generated: { generated: {
line: mapping.generatedLine, line: mapping.generatedLine,
column: mapping.generatedColumn, column: mapping.generatedColumn
}, },
name: originalPosition.name || undefined, name: originalPosition.name || undefined,
original: { original: {
line: originalPosition.line, line: originalPosition.line,
column: originalPosition.column, column: originalPosition.column
}, },
source: originalPosition.source, source: originalPosition.source
}); });
}); });
}); });
@ -242,24 +258,28 @@ async function mergeSourcemaps(first: RawSourceMap, second: RawSourceMap) {
return map; return map;
} }
async function mangleOriginal(options: ProcessBundleOptions): Promise<ProcessBundleFile> { async function mangleOriginal(
options: ProcessBundleOptions
): Promise<ProcessBundleFile> {
const result = terserMangle(options.code, { const result = terserMangle(options.code, {
filename: path.basename(options.filename), filename: path.basename(options.filename),
map: options.map ? JSON.parse(options.map) : undefined, map: options.map ? JSON.parse(options.map) : undefined,
ecma: 6, ecma: 6
}); });
let mapContent; let mapContent;
if (result.map) { if (result.map) {
if (!options.hiddenSourceMaps) { if (!options.hiddenSourceMaps) {
result.code += `\n//# sourceMappingURL=${path.basename(options.filename)}.map`; result.code += `\n//# sourceMappingURL=${path.basename(
options.filename
)}.map`;
} }
mapContent = JSON.stringify(result.map); mapContent = JSON.stringify(result.map);
await cachePut( await cachePut(
mapContent, mapContent,
(options.cacheKeys && options.cacheKeys[CacheKey.OriginalMap]) || null, (options.cacheKeys && options.cacheKeys[CacheKey.OriginalMap]) || null
); );
fs.writeFileSync(options.filename + '.map', mapContent); fs.writeFileSync(options.filename + '.map', mapContent);
} }
@ -268,13 +288,13 @@ async function mangleOriginal(options: ProcessBundleOptions): Promise<ProcessBun
options.filename, options.filename,
result.code, result.code,
mapContent, mapContent,
options.integrityAlgorithm, options.integrityAlgorithm
); );
await cachePut( await cachePut(
result.code, result.code,
(options.cacheKeys && options.cacheKeys[CacheKey.OriginalCode]) || null, (options.cacheKeys && options.cacheKeys[CacheKey.OriginalCode]) || null,
fileResult.integrity, fileResult.integrity
); );
fs.writeFileSync(options.filename, result.code); fs.writeFileSync(options.filename, result.code);
@ -283,7 +303,12 @@ async function mangleOriginal(options: ProcessBundleOptions): Promise<ProcessBun
function terserMangle( function terserMangle(
code: string, code: string,
options: { filename?: string; map?: RawSourceMap; compress?: boolean; ecma?: 5 | 6 } = {}, options: {
filename?: string;
map?: RawSourceMap;
compress?: boolean;
ecma?: 5 | 6;
} = {}
) { ) {
// Note: Investigate converting the AST instead of re-parsing // Note: Investigate converting the AST instead of re-parsing
// estree -> terser is already supported; need babel -> estree/terser // estree -> terser is already supported; need babel -> estree/terser
@ -296,7 +321,7 @@ function terserMangle(
safari10: true, safari10: true,
output: { output: {
ascii_only: true, ascii_only: true,
webkit: true, webkit: true
}, },
sourceMap: sourceMap:
!!options.map && !!options.map &&
@ -305,10 +330,10 @@ function terserMangle(
// terser uses an old version of the sourcemap typings // terser uses an old version of the sourcemap typings
// tslint:disable-next-line: no-any // tslint:disable-next-line: no-any
content: options.map as any, content: options.map as any,
asObject: true, asObject: true
// typings don't include asObject option // typings don't include asObject option
// tslint:disable-next-line: no-any // tslint:disable-next-line: no-any
} as any), } as any)
}); });
if (minifyOutput.error) { if (minifyOutput.error) {
@ -316,25 +341,29 @@ function terserMangle(
} }
// tslint:disable-next-line: no-non-null-assertion // tslint:disable-next-line: no-non-null-assertion
return { code: minifyOutput.code!, map: minifyOutput.map as unknown | RawSourceMap | undefined }; return {
code: minifyOutput.code!,
map: minifyOutput.map as unknown | RawSourceMap | undefined
};
} }
function createFileEntry( function createFileEntry(
filename: string, filename: string,
code: string, code: string,
map: string | undefined, map: string | undefined,
integrityAlgorithm?: string, integrityAlgorithm?: string
): ProcessBundleFile { ): ProcessBundleFile {
return { return {
filename: filename, filename: filename,
size: Buffer.byteLength(code), size: Buffer.byteLength(code),
integrity: integrityAlgorithm && generateIntegrityValue(integrityAlgorithm, code), integrity:
integrityAlgorithm && generateIntegrityValue(integrityAlgorithm, code),
map: !map map: !map
? undefined ? undefined
: { : {
filename: filename + '.map', filename: filename + '.map',
size: Buffer.byteLength(map), size: Buffer.byteLength(map)
}, }
}; };
} }
@ -352,7 +381,7 @@ function generateIntegrityValue(hashAlgorithm: string, code: string) {
// However, two variants are still needed due to lazy routing and SRI differences // However, two variants are still needed due to lazy routing and SRI differences
// NOTE: This should eventually be a babel plugin // NOTE: This should eventually be a babel plugin
async function processRuntime( async function processRuntime(
options: ProcessBundleOptions, options: ProcessBundleOptions
): Promise<Partial<ProcessBundleResult>> { ): Promise<Partial<ProcessBundleResult>> {
let originalCode = options.code; let originalCode = options.code;
let downlevelCode = options.code; let downlevelCode = options.code;
@ -365,10 +394,16 @@ async function processRuntime(
} }
if (data.original && data.original.integrity) { if (data.original && data.original.integrity) {
originalCode = originalCode.replace(data.integrity, data.original.integrity); originalCode = originalCode.replace(
data.integrity,
data.original.integrity
);
} }
if (data.downlevel && data.downlevel.integrity) { if (data.downlevel && data.downlevel.integrity) {
downlevelCode = downlevelCode.replace(data.integrity, data.downlevel.integrity); downlevelCode = downlevelCode.replace(
data.integrity,
data.downlevel.integrity
);
} }
} }
} }
@ -383,7 +418,7 @@ async function processRuntime(
if (options.optimize) { if (options.optimize) {
const minifiyResults = terserMangle(downlevelCode, { const minifiyResults = terserMangle(downlevelCode, {
filename: path.basename(downlevelFilePath), filename: path.basename(downlevelFilePath),
map: options.map === undefined ? undefined : JSON.parse(options.map), map: options.map === undefined ? undefined : JSON.parse(options.map)
}); });
downlevelCode = minifiyResults.code; downlevelCode = minifiyResults.code;
downlevelMap = JSON.stringify(minifiyResults.map); downlevelMap = JSON.stringify(minifiyResults.map);
@ -394,8 +429,8 @@ async function processRuntime(
downlevelFilePath, downlevelFilePath,
downlevelCode, downlevelCode,
downlevelMap, downlevelMap,
options.integrityAlgorithm, options.integrityAlgorithm
), )
}; };
} else { } else {
if (options.map) { if (options.map) {
@ -409,28 +444,30 @@ async function processRuntime(
options.filename, options.filename,
originalCode, originalCode,
options.map, options.map,
options.integrityAlgorithm, options.integrityAlgorithm
), ),
downlevel: createFileEntry( downlevel: createFileEntry(
downlevelFilePath, downlevelFilePath,
downlevelCode, downlevelCode,
downlevelMap, downlevelMap,
options.integrityAlgorithm, options.integrityAlgorithm
), )
}; };
} }
if (downlevelMap) { if (downlevelMap) {
await cachePut( await cachePut(
downlevelMap, downlevelMap,
(options.cacheKeys && options.cacheKeys[CacheKey.DownlevelMap]) || null, (options.cacheKeys && options.cacheKeys[CacheKey.DownlevelMap]) || null
); );
fs.writeFileSync(downlevelFilePath + '.map', downlevelMap); fs.writeFileSync(downlevelFilePath + '.map', downlevelMap);
downlevelCode += `\n//# sourceMappingURL=${path.basename(downlevelFilePath)}.map`; downlevelCode += `\n//# sourceMappingURL=${path.basename(
downlevelFilePath
)}.map`;
} }
await cachePut( await cachePut(
downlevelCode, downlevelCode,
(options.cacheKeys && options.cacheKeys[CacheKey.DownlevelCode]) || null, (options.cacheKeys && options.cacheKeys[CacheKey.DownlevelCode]) || null
); );
fs.writeFileSync(downlevelFilePath, downlevelCode); fs.writeFileSync(downlevelFilePath, downlevelCode);

View File

@ -11,27 +11,26 @@ import { resolve } from 'path';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
const treeKill = require('tree-kill'); const treeKill = require('tree-kill');
export function runModuleAsObservableFork( export function runModuleAsObservableFork(
cwd: string, cwd: string,
modulePath: string, modulePath: string,
exportName: string | undefined, exportName: string | undefined,
// tslint:disable-next-line:no-any // tslint:disable-next-line:no-any
args: any[], args: any[]
): Observable<BuilderOutput> { ): Observable<BuilderOutput> {
return new Observable(obs => { return new Observable(obs => {
const workerPath: string = resolve(__dirname, './run-module-worker.js'); const workerPath: string = resolve(__dirname, './run-module-worker.js');
const debugArgRegex = /--inspect(?:-brk|-port)?|--debug(?:-brk|-port)/; const debugArgRegex = /--inspect(?:-brk|-port)?|--debug(?:-brk|-port)/;
const execArgv = process.execArgv.filter((arg) => { const execArgv = process.execArgv.filter(arg => {
// Remove debug args. // Remove debug args.
// Workaround for https://github.com/nodejs/node/issues/9435 // Workaround for https://github.com/nodejs/node/issues/9435
return !debugArgRegex.test(arg); return !debugArgRegex.test(arg);
}); });
const forkOptions: ForkOptions = { const forkOptions: ForkOptions = ({
cwd, cwd,
execArgv, execArgv
} as {} as ForkOptions; } as {}) as ForkOptions;
// TODO: support passing in a logger to use as stdio streams // TODO: support passing in a logger to use as stdio streams
// if (logger) { // if (logger) {
@ -77,7 +76,7 @@ export function runModuleAsObservableFork(
hash: '5d4b9a5c0a4e0f9977598437b0e85bcc', hash: '5d4b9a5c0a4e0f9977598437b0e85bcc',
modulePath, modulePath,
exportName, exportName,
args, args
}); });
// Teardown logic. When unsubscribing, kill the forked process. // Teardown logic. When unsubscribing, kill the forked process.

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
process.on('message', (message) => { process.on('message', message => {
// Only process messages with the hash in 'run-module-as-observable-fork.ts'. // Only process messages with the hash in 'run-module-as-observable-fork.ts'.
if (message.hash === '5d4b9a5c0a4e0f9977598437b0e85bcc') { if (message.hash === '5d4b9a5c0a4e0f9977598437b0e85bcc') {
const requiredModule = require(message.modulePath); const requiredModule = require(message.modulePath);
@ -17,4 +17,3 @@ process.on('message', (message) => {
} }
} }
}); });

View File

@ -8,15 +8,24 @@
import { logging, tags } from '@angular-devkit/core'; import { logging, tags } from '@angular-devkit/core';
import { SemVer, gte, satisfies } from 'semver'; import { SemVer, gte, satisfies } from 'semver';
export function assertCompatibleAngularVersion(projectRoot: string, logger: logging.LoggerApi) { export function assertCompatibleAngularVersion(
projectRoot: string,
logger: logging.LoggerApi
) {
let angularCliPkgJson; let angularCliPkgJson;
let angularPkgJson; let angularPkgJson;
let rxjsPkgJson; let rxjsPkgJson;
const resolveOptions = { paths: [projectRoot] }; const resolveOptions = { paths: [projectRoot] };
try { try {
const angularPackagePath = require.resolve('@angular/core/package.json', resolveOptions); const angularPackagePath = require.resolve(
const rxjsPackagePath = require.resolve('rxjs/package.json', resolveOptions); '@angular/core/package.json',
resolveOptions
);
const rxjsPackagePath = require.resolve(
'rxjs/package.json',
resolveOptions
);
angularPkgJson = require(angularPackagePath); angularPkgJson = require(angularPackagePath);
rxjsPkgJson = require(rxjsPackagePath); rxjsPkgJson = require(rxjsPackagePath);
@ -28,7 +37,14 @@ export function assertCompatibleAngularVersion(projectRoot: string, logger: logg
process.exit(2); process.exit(2);
} }
if (!(angularPkgJson && angularPkgJson['version'] && rxjsPkgJson && rxjsPkgJson['version'])) { if (
!(
angularPkgJson &&
angularPkgJson['version'] &&
rxjsPkgJson &&
rxjsPkgJson['version']
)
) {
logger.error(tags.stripIndents` logger.error(tags.stripIndents`
Cannot determine versions of "@angular/core" and/or "rxjs". Cannot determine versions of "@angular/core" and/or "rxjs".
This likely means your local installation is broken. Please reinstall your packages. This likely means your local installation is broken. Please reinstall your packages.
@ -38,7 +54,10 @@ export function assertCompatibleAngularVersion(projectRoot: string, logger: logg
} }
try { try {
const angularCliPkgPath = require.resolve('@angular/cli/package.json', resolveOptions); const angularCliPkgPath = require.resolve(
'@angular/cli/package.json',
resolveOptions
);
angularCliPkgJson = require(angularCliPkgPath); angularCliPkgJson = require(angularCliPkgPath);
if (!(angularCliPkgJson && angularCliPkgJson['version'])) { if (!(angularCliPkgJson && angularCliPkgJson['version'])) {
throw new Error(); throw new Error();
@ -66,7 +85,11 @@ export function assertCompatibleAngularVersion(projectRoot: string, logger: logg
const angularVersion = new SemVer(angularPkgJson['version']); const angularVersion = new SemVer(angularPkgJson['version']);
const rxjsVersion = new SemVer(rxjsPkgJson['version']); const rxjsVersion = new SemVer(rxjsPkgJson['version']);
if (!satisfies(angularVersion, supportedAngularSemver, { includePrerelease: true })) { if (
!satisfies(angularVersion, supportedAngularSemver, {
includePrerelease: true
})
) {
logger.error( logger.error(
tags.stripIndents` tags.stripIndents`
This version of CLI is only compatible with Angular versions ${supportedAngularSemver}, This version of CLI is only compatible with Angular versions ${supportedAngularSemver},
@ -74,7 +97,7 @@ export function assertCompatibleAngularVersion(projectRoot: string, logger: logg
Please visit the link below to find instructions on how to update Angular. Please visit the link below to find instructions on how to update Angular.
https://angular-update-guide.firebaseapp.com/ https://angular-update-guide.firebaseapp.com/
` + '\n', ` + '\n'
); );
process.exit(3); process.exit(3);
@ -90,18 +113,21 @@ export function assertCompatibleAngularVersion(projectRoot: string, logger: logg
Please visit the link below to find instructions on how to update RxJs. Please visit the link below to find instructions on how to update RxJs.
https://docs.google.com/document/d/12nlLt71VLKb-z3YaSGzUfx6mJbc34nsMXtByPUN35cg/edit# https://docs.google.com/document/d/12nlLt71VLKb-z3YaSGzUfx6mJbc34nsMXtByPUN35cg/edit#
` + '\n', ` + '\n'
); );
process.exit(3); process.exit(3);
} else if (gte(angularVersion, '6.0.0-rc.0') && !gte(rxjsVersion, '6.0.0-beta.0')) { } else if (
gte(angularVersion, '6.0.0-rc.0') &&
!gte(rxjsVersion, '6.0.0-beta.0')
) {
logger.warn( logger.warn(
tags.stripIndents` tags.stripIndents`
This project uses a temporary compatibility version of RxJs (${rxjsVersion}). This project uses a temporary compatibility version of RxJs (${rxjsVersion}).
Please visit the link below to find instructions on how to update RxJs. Please visit the link below to find instructions on how to update RxJs.
https://docs.google.com/document/d/12nlLt71VLKb-z3YaSGzUfx6mJbc34nsMXtByPUN35cg/edit# https://docs.google.com/document/d/12nlLt71VLKb-z3YaSGzUfx6mJbc34nsMXtByPUN35cg/edit#
` + '\n', ` + '\n'
); );
} }
} }

View File

@ -13,7 +13,7 @@ import {
normalize, normalize,
resolve, resolve,
schema, schema,
virtualFs, virtualFs
} from '@angular-devkit/core'; } from '@angular-devkit/core';
import { NodeJsSyncHost } from '@angular-devkit/core/node'; import { NodeJsSyncHost } from '@angular-devkit/core/node';
import * as fs from 'fs'; import * as fs from 'fs';
@ -27,14 +27,16 @@ import {
NormalizedBrowserBuilderSchema, NormalizedBrowserBuilderSchema,
defaultProgress, defaultProgress,
fullDifferential, fullDifferential,
normalizeBrowserSchema, normalizeBrowserSchema
} from '../utils'; } from '../utils';
import { BuildBrowserFeatures } from './build-browser-features'; import { BuildBrowserFeatures } from './build-browser-features';
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin'); const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const webpackMerge = require('webpack-merge'); const webpackMerge = require('webpack-merge');
type BrowserWebpackConfigOptions = WebpackConfigOptions<NormalizedBrowserBuilderSchema>; type BrowserWebpackConfigOptions = WebpackConfigOptions<
NormalizedBrowserBuilderSchema
>;
export async function generateWebpackConfig( export async function generateWebpackConfig(
context: BuilderContext, context: BuilderContext,
@ -42,12 +44,16 @@ export async function generateWebpackConfig(
projectRoot: string, projectRoot: string,
sourceRoot: string | undefined, sourceRoot: string | undefined,
options: NormalizedBrowserBuilderSchema, options: NormalizedBrowserBuilderSchema,
webpackPartialGenerator: (wco: BrowserWebpackConfigOptions) => webpack.Configuration[], webpackPartialGenerator: (
logger: logging.LoggerApi, wco: BrowserWebpackConfigOptions
) => webpack.Configuration[],
logger: logging.LoggerApi
): Promise<webpack.Configuration[]> { ): Promise<webpack.Configuration[]> {
// Ensure Build Optimizer is only used with AOT. // Ensure Build Optimizer is only used with AOT.
if (options.buildOptimizer && !options.aot) { if (options.buildOptimizer && !options.aot) {
throw new Error(`The 'buildOptimizer' option cannot be used without 'aot'.`); throw new Error(
`The 'buildOptimizer' option cannot be used without 'aot'.`
);
} }
const tsConfigPath = path.resolve(workspaceRoot, options.tsConfig); const tsConfigPath = path.resolve(workspaceRoot, options.tsConfig);
@ -59,10 +65,14 @@ export async function generateWebpackConfig(
// At the moment, only the browser builder supports differential loading // At the moment, only the browser builder supports differential loading
// However this config generation is used by multiple builders such as dev-server // However this config generation is used by multiple builders such as dev-server
const scriptTarget = tsConfig.options.target || ts.ScriptTarget.ES5; const scriptTarget = tsConfig.options.target || ts.ScriptTarget.ES5;
const buildBrowserFeatures = new BuildBrowserFeatures(projectRoot, scriptTarget); const buildBrowserFeatures = new BuildBrowserFeatures(
const differentialLoading = context.builder.builderName === 'browser' projectRoot,
&& !options.watch scriptTarget
&& buildBrowserFeatures.isDifferentialLoadingNeeded(); );
const differentialLoading =
context.builder.builderName === 'browser' &&
!options.watch &&
buildBrowserFeatures.isDifferentialLoadingNeeded();
const scriptTargets = [scriptTarget]; const scriptTargets = [scriptTarget];
@ -73,29 +83,33 @@ export async function generateWebpackConfig(
// For differential loading, we can have several targets // For differential loading, we can have several targets
return scriptTargets.map(scriptTarget => { return scriptTargets.map(scriptTarget => {
let buildOptions: NormalizedBrowserBuilderSchema = { ...options }; let buildOptions: NormalizedBrowserBuilderSchema = { ...options };
const supportES2015 const supportES2015 =
= scriptTarget !== ts.ScriptTarget.ES3 && scriptTarget !== ts.ScriptTarget.ES5; scriptTarget !== ts.ScriptTarget.ES3 &&
scriptTarget !== ts.ScriptTarget.ES5;
if (differentialLoading && fullDifferential) { if (differentialLoading && fullDifferential) {
buildOptions = { buildOptions = {
...options, ...options,
...( ...// FIXME: we do create better webpack config composition to achieve the below
// FIXME: we do create better webpack config composition to achieve the below // When DL is enabled and supportES2015 is true it means that we are on the second build
// When DL is enabled and supportES2015 is true it means that we are on the second build // This also means that we don't need to include styles and assets multiple times
// This also means that we don't need to include styles and assets multiple times (supportES2015
supportES2015 ? {}
? {} : {
: {
styles: options.extractCss ? [] : options.styles, styles: options.extractCss ? [] : options.styles,
assets: [], assets: []
} }),
),
es5BrowserSupport: undefined, es5BrowserSupport: undefined,
esVersionInFileName: true, esVersionInFileName: true,
scriptTargetOverride: scriptTarget, scriptTargetOverride: scriptTarget
}; };
} else if (differentialLoading && !fullDifferential) { } else if (differentialLoading && !fullDifferential) {
buildOptions = { ...options, esVersionInFileName: true, scriptTargetOverride: ts.ScriptTarget.ES5, es5BrowserSupport: undefined }; buildOptions = {
...options,
esVersionInFileName: true,
scriptTargetOverride: ts.ScriptTarget.ES5,
es5BrowserSupport: undefined
};
} }
const wco: BrowserWebpackConfigOptions = { const wco: BrowserWebpackConfigOptions = {
@ -106,7 +120,7 @@ export async function generateWebpackConfig(
buildOptions, buildOptions,
tsConfig, tsConfig,
tsConfigPath, tsConfigPath,
supportES2015, supportES2015
}; };
wco.buildOptions.progress = defaultProgress(wco.buildOptions.progress); wco.buildOptions.progress = defaultProgress(wco.buildOptions.progress);
@ -121,21 +135,24 @@ export async function generateWebpackConfig(
if (!webpackConfig.resolve.alias) { if (!webpackConfig.resolve.alias) {
webpackConfig.resolve.alias = {}; webpackConfig.resolve.alias = {};
} }
webpackConfig.resolve.alias['zone.js/dist/zone'] = 'zone.js/dist/zone-evergreen'; webpackConfig.resolve.alias['zone.js/dist/zone'] =
'zone.js/dist/zone-evergreen';
} }
if (options.profile || process.env['NG_BUILD_PROFILING']) { if (options.profile || process.env['NG_BUILD_PROFILING']) {
const esVersionInFileName = getEsVersionForFileName( const esVersionInFileName = getEsVersionForFileName(
fullDifferential ? buildOptions.scriptTargetOverride : tsConfig.options.target, fullDifferential
wco.buildOptions.esVersionInFileName, ? buildOptions.scriptTargetOverride
: tsConfig.options.target,
wco.buildOptions.esVersionInFileName
); );
const smp = new SpeedMeasurePlugin({ const smp = new SpeedMeasurePlugin({
outputFormat: 'json', outputFormat: 'json',
outputTarget: path.resolve( outputTarget: path.resolve(
workspaceRoot, workspaceRoot,
`speed-measure-plugin${esVersionInFileName}.json`, `speed-measure-plugin${esVersionInFileName}.json`
), )
}); });
return smp.wrap(webpackConfig); return smp.wrap(webpackConfig);
@ -145,18 +162,22 @@ export async function generateWebpackConfig(
}); });
} }
export async function generateBrowserWebpackConfigFromWorkspace( export async function generateBrowserWebpackConfigFromWorkspace(
options: BrowserBuilderSchema, options: BrowserBuilderSchema,
context: BuilderContext, context: BuilderContext,
projectName: string, projectName: string,
workspace: experimental.workspace.Workspace, workspace: experimental.workspace.Workspace,
host: virtualFs.Host<fs.Stats>, host: virtualFs.Host<fs.Stats>,
webpackPartialGenerator: (wco: BrowserWebpackConfigOptions) => webpack.Configuration[], webpackPartialGenerator: (
logger: logging.LoggerApi, wco: BrowserWebpackConfigOptions
) => webpack.Configuration[],
logger: logging.LoggerApi
): Promise<webpack.Configuration[]> { ): Promise<webpack.Configuration[]> {
// TODO: Use a better interface for workspace access. // TODO: Use a better interface for workspace access.
const projectRoot = resolve(workspace.root, normalize(workspace.getProject(projectName).root)); const projectRoot = resolve(
workspace.root,
normalize(workspace.getProject(projectName).root)
);
const projectSourceRoot = workspace.getProject(projectName).sourceRoot; const projectSourceRoot = workspace.getProject(projectName).sourceRoot;
const sourceRoot = projectSourceRoot const sourceRoot = projectSourceRoot
? resolve(workspace.root, normalize(projectSourceRoot)) ? resolve(workspace.root, normalize(projectSourceRoot))
@ -167,7 +188,7 @@ export async function generateBrowserWebpackConfigFromWorkspace(
workspace.root, workspace.root,
projectRoot, projectRoot,
sourceRoot, sourceRoot,
options, options
); );
return generateWebpackConfig( return generateWebpackConfig(
@ -177,30 +198,38 @@ export async function generateBrowserWebpackConfigFromWorkspace(
sourceRoot && getSystemPath(sourceRoot), sourceRoot && getSystemPath(sourceRoot),
normalizedOptions, normalizedOptions,
webpackPartialGenerator, webpackPartialGenerator,
logger, logger
); );
} }
export async function generateBrowserWebpackConfigFromContext( export async function generateBrowserWebpackConfigFromContext(
options: BrowserBuilderSchema, options: BrowserBuilderSchema,
context: BuilderContext, context: BuilderContext,
webpackPartialGenerator: (wco: BrowserWebpackConfigOptions) => webpack.Configuration[], webpackPartialGenerator: (
host: virtualFs.Host<fs.Stats> = new NodeJsSyncHost(), wco: BrowserWebpackConfigOptions
): Promise<{ workspace: experimental.workspace.Workspace, config: webpack.Configuration[] }> { ) => webpack.Configuration[],
host: virtualFs.Host<fs.Stats> = new NodeJsSyncHost()
): Promise<{
workspace: experimental.workspace.Workspace;
config: webpack.Configuration[];
}> {
const registry = new schema.CoreSchemaRegistry(); const registry = new schema.CoreSchemaRegistry();
registry.addPostTransform(schema.transforms.addUndefinedDefaults); registry.addPostTransform(schema.transforms.addUndefinedDefaults);
const workspace = await experimental.workspace.Workspace.fromPath( const workspace = await experimental.workspace.Workspace.fromPath(
host, host,
normalize(context.workspaceRoot), normalize(context.workspaceRoot),
registry, registry
); );
const projectName = context.target ? context.target.project : workspace.getDefaultProjectName(); const projectName = context.target
? context.target.project
: workspace.getDefaultProjectName();
if (!projectName) { if (!projectName) {
throw new Error('Must either have a target from the context or a default project.'); throw new Error(
'Must either have a target from the context or a default project.'
);
} }
const config = await generateBrowserWebpackConfigFromWorkspace( const config = await generateBrowserWebpackConfigFromWorkspace(
@ -210,7 +239,7 @@ export async function generateBrowserWebpackConfigFromContext(
workspace, workspace,
host, host,
webpackPartialGenerator, webpackPartialGenerator,
context.logger, context.logger
); );
return { workspace, config }; return { workspace, config };

View File

@ -95,16 +95,14 @@ export function getBaseWebpackPartial(
} }
if (options.extractLicenses) { if (options.extractLicenses) {
extraPlugins.push( extraPlugins.push(new LicenseWebpackPlugin({
new LicenseWebpackPlugin({ stats: {
stats: { warnings: false,
warnings: false, errors: false
errors: false, },
}, perChunkOutput: false,
perChunkOutput: false, outputFilename: `3rdpartylicenses.txt`
outputFilename: `3rdpartylicenses.txt`, }) as any);
}) as any
);
} }
// process asset entries // process asset entries