diff --git a/packages/angular/cli/lib/config/schema.json b/packages/angular/cli/lib/config/schema.json index 99bc50ac4895..c4530ec95799 100644 --- a/packages/angular/cli/lib/config/schema.json +++ b/packages/angular/cli/lib/config/schema.json @@ -633,9 +633,39 @@ "default": false }, "sourceMap": { - "type": "boolean", "description": "Output sourcemaps.", - "default": true + "default": true, + "oneOf": [ + { + "type": "object", + "properties": { + "scripts": { + "type": "boolean", + "description": "Output sourcemaps for all scripts.", + "default": true + }, + "styles": { + "type": "boolean", + "description": "Output sourcemaps for all styles.", + "default": true + }, + "hidden": { + "type": "boolean", + "description": "Output sourcemaps used for error reporting tools.", + "default": false + }, + "vendor": { + "type": "boolean", + "description": "Resolve vendor packages sourcemaps.", + "default": false + } + }, + "additionalProperties": false + }, + { + "type": "boolean" + } + ] }, "vendorSourceMap": { "type": "boolean", @@ -1045,8 +1075,34 @@ "description": "Build using Ahead of Time compilation." }, "sourceMap": { - "type": "boolean", - "description": "Output sourcemaps." + "description": "Output sourcemaps.", + "default": true, + "oneOf": [ + { + "type": "object", + "properties": { + "scripts": { + "type": "boolean", + "description": "Output sourcemaps for all scripts.", + "default": true + }, + "styles": { + "type": "boolean", + "description": "Output sourcemaps for all styles.", + "default": true + }, + "vendor": { + "type": "boolean", + "description": "Resolve vendor packages sourcemaps.", + "default": false + } + }, + "additionalProperties": false + }, + { + "type": "boolean" + } + ] }, "vendorSourceMap": { "type": "boolean", @@ -1189,9 +1245,34 @@ "description": "Defines the build environment." }, "sourceMap": { - "type": "boolean", "description": "Output sourcemaps.", - "default": true + "default": true, + "oneOf": [ + { + "type": "object", + "properties": { + "scripts": { + "type": "boolean", + "description": "Output sourcemaps for all scripts.", + "default": true + }, + "styles": { + "type": "boolean", + "description": "Output sourcemaps for all styles.", + "default": true + }, + "vendor": { + "type": "boolean", + "description": "Resolve vendor packages sourcemaps.", + "default": false + } + }, + "additionalProperties": false + }, + { + "type": "boolean" + } + ] }, "progress": { "type": "boolean", @@ -1455,9 +1536,39 @@ "description": "The path where style resources will be placed, relative to outputPath." }, "sourceMap": { - "type": "boolean", "description": "Output sourcemaps.", - "default": true + "default": true, + "oneOf": [ + { + "type": "object", + "properties": { + "scripts": { + "type": "boolean", + "description": "Output sourcemaps for all scripts.", + "default": true + }, + "styles": { + "type": "boolean", + "description": "Output sourcemaps for all styles.", + "default": true + }, + "hidden": { + "type": "boolean", + "description": "Output sourcemaps used for error reporting tools.", + "default": false + }, + "vendor": { + "type": "boolean", + "description": "Resolve vendor packages sourcemaps.", + "default": false + } + }, + "additionalProperties": false + }, + { + "type": "boolean" + } + ] }, "vendorSourceMap": { "type": "boolean", diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/models/build-options.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/models/build-options.ts index 8e8e6df74dde..649d287ad144 100644 --- a/packages/angular_devkit/build_angular/src/angular-cli-files/models/build-options.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/models/build-options.ts @@ -24,6 +24,9 @@ export interface BuildOptions { resourcesOutputPath?: string; aot?: boolean; sourceMap?: boolean; + scriptsSourceMap?: boolean; + stylesSourceMap?: boolean; + hiddenSourceMap?: boolean; vendorSourceMap?: boolean; evalSourceMap?: boolean; vendorChunk?: boolean; diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/browser.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/browser.ts index 27c22ce0309b..0fc80f090891 100644 --- a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/browser.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/browser.ts @@ -10,7 +10,7 @@ import * as path from 'path'; import { IndexHtmlWebpackPlugin } from '../../plugins/index-html-webpack-plugin'; import { generateEntryPoints } from '../../utilities/package-chunk-sort'; import { WebpackConfigOptions } from '../build-options'; -import { normalizeExtraEntryPoints } from './utils'; +import { getSourceMapDevTool, normalizeExtraEntryPoints } from './utils'; const SubresourceIntegrityPlugin = require('webpack-subresource-integrity'); @@ -19,16 +19,11 @@ export function getBrowserConfig(wco: WebpackConfigOptions) { const { root, buildOptions } = wco; const extraPlugins = []; - let sourcemaps: string | false = false; - if (buildOptions.sourceMap) { - // See https://webpack.js.org/configuration/devtool/ for sourcemap types. - if (buildOptions.evalSourceMap && !buildOptions.optimization) { - // Produce eval sourcemaps for development with serve, which are faster. - sourcemaps = 'eval'; - } else { - // Produce full separate sourcemaps for production. - sourcemaps = 'source-map'; - } + let isEval = false; + // See https://webpack.js.org/configuration/devtool/ for sourcemap types. + if (buildOptions.sourceMap && buildOptions.evalSourceMap && !buildOptions.optimization) { + // Produce eval sourcemaps for development with serve, which are faster. + isEval = true; } if (buildOptions.index) { @@ -59,11 +54,25 @@ export function getBrowserConfig(wco: WebpackConfigOptions) { })); } + if (!isEval && buildOptions.sourceMap) { + const { + scriptsSourceMap = false, + stylesSourceMap = false, + hiddenSourceMap = false, + } = buildOptions; + + extraPlugins.push(getSourceMapDevTool( + scriptsSourceMap, + stylesSourceMap, + hiddenSourceMap, + )); + } + const globalStylesBundleNames = normalizeExtraEntryPoints(buildOptions.styles, 'styles') .map(style => style.bundleName); return { - devtool: sourcemaps, + devtool: isEval ? 'eval' : false, resolve: { mainFields: [ ...(wco.supportES2015 ? ['es2015'] : []), diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/common.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/common.ts index 699dafdbf688..147f770912ea 100644 --- a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/common.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/common.ts @@ -15,7 +15,7 @@ import { ScriptsWebpackPlugin } from '../../plugins/scripts-webpack-plugin'; import { findUp } from '../../utilities/find-up'; import { isDirectory } from '../../utilities/is-directory'; import { requireProjectModule } from '../../utilities/require-project-module'; -import { WebpackConfigOptions } from '../build-options'; +import { BuildOptions, WebpackConfigOptions } from '../build-options'; import { getOutputHashFormat, normalizeExtraEntryPoints } from './utils'; const ProgressPlugin = require('webpack/lib/ProgressPlugin'); @@ -102,7 +102,7 @@ export function getCommonConfig(wco: WebpackConfigOptions) { extraPlugins.push(new ScriptsWebpackPlugin({ name: bundleName, - sourceMap: buildOptions.sourceMap, + sourceMap: buildOptions.scriptsSourceMap, filename: `${path.basename(bundleName)}${hash}.js`, scripts: script.paths, basePath: projectRoot, @@ -174,7 +174,7 @@ export function getCommonConfig(wco: WebpackConfigOptions) { use: [ { loader: buildOptimizerLoader, - options: { sourceMap: buildOptions.sourceMap }, + options: { sourceMap: buildOptions.scriptsSourceMap }, }, ], }; @@ -297,12 +297,12 @@ export function getCommonConfig(wco: WebpackConfigOptions) { // TODO: check with Mike what this feature needs. new BundleBudgetPlugin({ budgets: buildOptions.budgets }), new CleanCssWebpackPlugin({ - sourceMap: buildOptions.sourceMap, + sourceMap: buildOptions.stylesSourceMap, // component styles retain their original file name test: (file) => /\.(?:css|scss|sass|less|styl)$/.test(file), }), new TerserPlugin({ - sourceMap: buildOptions.sourceMap, + sourceMap: buildOptions.scriptsSourceMap, parallel: true, cache: true, terserOptions, diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/server.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/server.ts index ee12c70ed2b4..df1027e33354 100644 --- a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/server.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/server.ts @@ -7,6 +7,7 @@ */ import { Configuration } from 'webpack'; import { WebpackConfigOptions } from '../build-options'; +import { getSourceMapDevTool } from './utils'; /** @@ -15,8 +16,22 @@ import { WebpackConfigOptions } from '../build-options'; */ export function getServerConfig(wco: WebpackConfigOptions) { + const extraPlugins = []; + if (wco.buildOptions.sourceMap) { + const { + scriptsSourceMap = false, + stylesSourceMap = false, + hiddenSourceMap = false, + } = wco.buildOptions; + + extraPlugins.push(getSourceMapDevTool( + scriptsSourceMap, + stylesSourceMap, + hiddenSourceMap, + )); + } + const config: Configuration = { - devtool: wco.buildOptions.sourceMap ? 'source-map' : false, resolve: { mainFields: [ ...(wco.supportES2015 ? ['es2015'] : []), @@ -27,6 +42,7 @@ export function getServerConfig(wco: WebpackConfigOptions) { output: { libraryTarget: 'commonjs', }, + plugins: extraPlugins, node: false, }; diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/styles.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/styles.ts index 451febeaca3b..972bda036dc7 100644 --- a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/styles.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/styles.ts @@ -40,7 +40,8 @@ export function getStylesConfig(wco: WebpackConfigOptions) { const entryPoints: { [key: string]: string[] } = {}; const globalStylePaths: string[] = []; const extraPlugins = []; - const cssSourceMap = buildOptions.sourceMap; + + const cssSourceMap = buildOptions.stylesSourceMap; // Determine hashing format. const hashFormat = getOutputHashFormat(buildOptions.outputHashing as string); @@ -187,7 +188,7 @@ export function getStylesConfig(wco: WebpackConfigOptions) { options: { ident: 'embedded', plugins: postcssPluginCreator, - sourceMap: cssSourceMap ? 'inline' : false, + sourceMap: cssSourceMap && !buildOptions.hiddenSourceMap ? 'inline' : false, }, }, ...(use as webpack.Loader[]), @@ -208,7 +209,10 @@ export function getStylesConfig(wco: WebpackConfigOptions) { options: { ident: buildOptions.extractCss ? 'extracted' : 'embedded', plugins: postcssPluginCreator, - sourceMap: cssSourceMap && !buildOptions.extractCss ? 'inline' : cssSourceMap, + sourceMap: cssSourceMap + && !buildOptions.extractCss + && !buildOptions.hiddenSourceMap + ? 'inline' : cssSourceMap, }, }, ...(use as webpack.Loader[]), diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/test.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/test.ts index 4c8b05aa6008..f54aca248c54 100644 --- a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/test.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/test.ts @@ -10,6 +10,7 @@ import * as glob from 'glob'; import * as path from 'path'; import * as webpack from 'webpack'; import { WebpackConfigOptions, WebpackTestOptions } from '../build-options'; +import { getSourceMapDevTool } from './utils'; /** @@ -55,6 +56,20 @@ export function getTestConfig( }); } + if (wco.buildOptions.sourceMap) { + const { + scriptsSourceMap = false, + stylesSourceMap = false, + } = wco.buildOptions; + + extraPlugins.push(getSourceMapDevTool( + scriptsSourceMap, + stylesSourceMap, + false, + true, + )); + } + return { mode: 'development', resolve: { @@ -63,7 +78,7 @@ export function getTestConfig( 'browser', 'module', 'main', ], }, - devtool: buildOptions.sourceMap ? 'inline-source-map' : 'eval', + devtool: buildOptions.sourceMap ? false : 'eval', entry: { main: path.resolve(root, buildOptions.main), }, diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/typescript.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/typescript.ts index f03f522f8417..b6303a853e13 100644 --- a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/typescript.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/typescript.ts @@ -75,7 +75,7 @@ function _createAotPlugin( locale: buildOptions.i18nLocale, platform: buildOptions.platform === 'server' ? PLATFORM.Server : PLATFORM.Browser, missingTranslation: buildOptions.i18nMissingTranslation, - sourceMap: buildOptions.sourceMap, + sourceMap: buildOptions.scriptsSourceMap, additionalLazyModules, hostReplacementPaths, nameLazyFiles: buildOptions.namedChunks, @@ -108,7 +108,7 @@ export function getAotConfig( if (buildOptions.buildOptimizer) { loaders.unshift({ loader: buildOptimizerLoader, - options: { sourceMap: buildOptions.sourceMap } + options: { sourceMap: buildOptions.scriptsSourceMap } }); } diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/utils.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/utils.ts index f32f3f8afabb..2fe43e7ea98a 100644 --- a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/utils.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/utils.ts @@ -11,6 +11,7 @@ import * as path from 'path'; import { basename, normalize } from '@angular-devkit/core'; import { ExtraEntryPoint, ExtraEntryPointObject } from '../../../browser/schema'; +import { SourceMapDevToolPlugin } from 'webpack'; export const ngAppResolve = (resolvePath: string): string => { return path.resolve(process.cwd(), resolvePath); @@ -66,3 +67,25 @@ export function normalizeExtraEntryPoints( return normalizedEntry; }) } + +export function getSourceMapDevTool( + scriptsSourceMap: boolean, + stylesSourceMap: boolean, + hiddenSourceMap = false, + inlineSourceMap = false, +): SourceMapDevToolPlugin { + const include = []; + if (scriptsSourceMap) { + include.push(/js$/); + } + + if (stylesSourceMap) { + include.push(/css$/); + } + + return new SourceMapDevToolPlugin({ + filename: inlineSourceMap ? undefined : '[file].map', + include, + append: hiddenSourceMap ? false : undefined, + }); +} diff --git a/packages/angular_devkit/build_angular/src/browser/index.ts b/packages/angular_devkit/build_angular/src/browser/index.ts index ab49981a9f0c..1f3b3fa468f9 100644 --- a/packages/angular_devkit/build_angular/src/browser/index.ts +++ b/packages/angular_devkit/build_angular/src/browser/index.ts @@ -34,8 +34,18 @@ import { statsToString, statsWarningsToString, } from '../angular-cli-files/utilities/stats'; -import { defaultProgress, normalizeAssetPatterns, normalizeFileReplacements } from '../utils'; -import { AssetPatternObject, BrowserBuilderSchema, CurrentFileReplacement } from './schema'; +import { + NormalizedSourceMaps, + defaultProgress, + normalizeAssetPatterns, + normalizeFileReplacements, + normalizeSourceMaps, +} from '../utils'; +import { + AssetPatternObject, + BrowserBuilderSchema, + CurrentFileReplacement, +} from './schema'; const SpeedMeasurePlugin = require('speed-measure-webpack-plugin'); const webpackMerge = require('webpack-merge'); @@ -45,7 +55,9 @@ const webpackMerge = require('webpack-merge'); // Right now this normalization has to be done in all other builders that make use of the // BrowserBuildSchema and BrowserBuilder.buildWebpackConfig. // It would really help if it happens during architect.validateBuilderOptions, or similar. -export interface NormalizedBrowserBuilderSchema extends BrowserBuilderSchema { +export interface NormalizedBrowserBuilderSchema extends + Pick>, + NormalizedSourceMaps { assets: AssetPatternObject[]; fileReplacements: CurrentFileReplacement[]; } @@ -55,7 +67,7 @@ export class BrowserBuilder implements Builder { constructor(public context: BuilderContext) { } run(builderConfig: BuilderConfiguration): Observable { - const options = builderConfig.options; + let options = builderConfig.options; const root = this.context.workspace.root; const projectRoot = resolve(root, builderConfig.root); const host = new virtualFs.AliasHost(this.context.host as virtualFs.Host); @@ -71,6 +83,17 @@ export class BrowserBuilder implements Builder { options.assets, host, root, projectRoot, builderConfig.sourceRoot)), // Replace the assets in options with the normalized version. tap((assetPatternObjects => options.assets = assetPatternObjects)), + tap(() => { + const normalizedOptions = normalizeSourceMaps(options.sourceMap); + // todo: remove when removing the deprecations + normalizedOptions.vendorSourceMap + = normalizedOptions.vendorSourceMap || !!options.vendorSourceMap; + + options = { + ...options, + ...normalizedOptions, + }; + }), concatMap(() => { let webpackConfig; try { diff --git a/packages/angular_devkit/build_angular/src/browser/schema.d.ts b/packages/angular_devkit/build_angular/src/browser/schema.d.ts index 33ed974acadd..ba27d8ab0418 100644 --- a/packages/angular_devkit/build_angular/src/browser/schema.d.ts +++ b/packages/angular_devkit/build_angular/src/browser/schema.d.ts @@ -69,10 +69,11 @@ export interface BrowserBuilderSchema { /** * Output sourcemaps. */ - sourceMap: boolean; + sourceMap: SourceMapOptions; /** * Resolve vendor packages sourcemaps. + * @deprecated - use `sourceMap.vendor` instead */ vendorSourceMap?: boolean; @@ -236,6 +237,19 @@ export interface BrowserBuilderSchema { profile: boolean; } +export type SourceMapOptions = boolean | SourceMapObject; + +export interface SourceMapObject { + /** Output sourcemaps used for error reports. */ + hidden?: boolean; + /** Resolve vendor packages sourcemaps */ + vendor?: boolean; + /** Output sourcemaps for all scripts */ + scripts?: boolean; + /** Output sourcemaps for all styles. */ + styles?: boolean; +} + export type AssetPattern = string | AssetPatternObject; export interface AssetPatternObject { diff --git a/packages/angular_devkit/build_angular/src/browser/schema.json b/packages/angular_devkit/build_angular/src/browser/schema.json index 2f0259214116..38c7e541dc43 100644 --- a/packages/angular_devkit/build_angular/src/browser/schema.json +++ b/packages/angular_devkit/build_angular/src/browser/schema.json @@ -82,13 +82,44 @@ "default": false }, "sourceMap": { - "type": "boolean", "description": "Output sourcemaps.", - "default": true + "default": true, + "oneOf": [ + { + "type": "object", + "properties": { + "scripts": { + "type": "boolean", + "description": "Output sourcemaps for all scripts.", + "default": true + }, + "styles": { + "type": "boolean", + "description": "Output sourcemaps for all styles.", + "default": true + }, + "hidden": { + "type": "boolean", + "description": "Output sourcemaps used for error reporting tools.", + "default": false + }, + "vendor": { + "type": "boolean", + "description": "Resolve vendor packages sourcemaps.", + "default": false + } + }, + "additionalProperties": false + }, + { + "type": "boolean" + } + ] }, "vendorSourceMap": { "type": "boolean", "description": "Resolve vendor packages sourcemaps.", + "x-deprecated": true, "default": false }, "evalSourceMap": { @@ -415,4 +446,4 @@ ] } } -} +} \ No newline at end of file diff --git a/packages/angular_devkit/build_angular/src/dev-server/index.ts b/packages/angular_devkit/build_angular/src/dev-server/index.ts index 409115dacb54..f72fe3caa3be 100644 --- a/packages/angular_devkit/build_angular/src/dev-server/index.ts +++ b/packages/angular_devkit/build_angular/src/dev-server/index.ts @@ -24,11 +24,15 @@ import * as WebpackDevServer from 'webpack-dev-server'; import { checkPort } from '../angular-cli-files/utilities/check-port'; import { BrowserBuilder, NormalizedBrowserBuilderSchema, getBrowserLoggingCb } from '../browser/'; import { BrowserBuilderSchema } from '../browser/schema'; -import { normalizeAssetPatterns, normalizeFileReplacements } from '../utils'; +import { normalizeAssetPatterns, normalizeFileReplacements, normalizeSourceMaps } from '../utils'; const opn = require('opn'); -export interface DevServerBuilderOptions { +export interface DevServerBuilderOptions extends Pick { browserTarget: string; port: number; host: string; @@ -45,21 +49,6 @@ export interface DevServerBuilderOptions { watch: boolean; hmrWarning: boolean; servePathDefaultWarning: boolean; - - // These options come from the browser builder and are provided here for convenience. - optimization?: boolean; - aot?: boolean; - sourceMap?: boolean; - vendorSourceMap?: boolean; - /**@deprecated */ - evalSourceMap?: boolean; - vendorChunk?: boolean; - commonChunk?: boolean; - baseHref?: string; - deployUrl?: string; - progress?: boolean; - poll?: number; - verbose?: boolean; } type DevServerBuilderOptionsKeys = Extract; @@ -88,14 +77,24 @@ export class DevServerBuilder implements Builder { browserOptions.assets, host, root, projectRoot, builderConfig.sourceRoot)), // Replace the assets in options with the normalized version. tap((assetPatternObjects => browserOptions.assets = assetPatternObjects)), + tap(() => { + const normalizedOptions = normalizeSourceMaps(browserOptions.sourceMap); + // todo: remove when removing the deprecations + normalizedOptions.vendorSourceMap + = normalizedOptions.vendorSourceMap || !!browserOptions.vendorSourceMap; + + browserOptions = { + ...browserOptions, + ...normalizedOptions, + }; + }), concatMap(() => { const webpackConfig = this.buildWebpackConfig( root, projectRoot, host, browserOptions as NormalizedBrowserBuilderSchema); let webpackDevServerConfig: WebpackDevServer.Configuration; try { - webpackDevServerConfig = this._buildServerConfig( - root, projectRoot, options, browserOptions); + webpackDevServerConfig = this._buildServerConfig(root, options, browserOptions); } catch (err) { return throwError(err); } @@ -172,7 +171,8 @@ export class DevServerBuilder implements Builder { return buildEvent; }), - ); + // using more than 10 operators will cause rxjs to loose the types + ) as Observable; } buildWebpackConfig( @@ -190,7 +190,6 @@ export class DevServerBuilder implements Builder { private _buildServerConfig( root: Path, - projectRoot: Path, options: DevServerBuilderOptions, browserOptions: BrowserBuilderSchema, ) { diff --git a/packages/angular_devkit/build_angular/src/dev-server/schema.json b/packages/angular_devkit/build_angular/src/dev-server/schema.json index e2f5099fe589..c69da96418a2 100644 --- a/packages/angular_devkit/build_angular/src/dev-server/schema.json +++ b/packages/angular_devkit/build_angular/src/dev-server/schema.json @@ -92,12 +92,44 @@ "description": "Build using Ahead of Time compilation." }, "sourceMap": { - "type": "boolean", - "description": "Output sourcemaps." + "description": "Output sourcemaps.", + "default": true, + "oneOf": [ + { + "type": "object", + "properties": { + "scripts": { + "type": "boolean", + "description": "Output sourcemaps for all scripts.", + "default": true + }, + "styles": { + "type": "boolean", + "description": "Output sourcemaps for all styles.", + "default": true + }, + "hidden": { + "type": "boolean", + "description": "Output sourcemaps used for error reporting tools.", + "default": false + }, + "vendor": { + "type": "boolean", + "description": "Resolve vendor packages sourcemaps.", + "default": false + } + }, + "additionalProperties": false + }, + { + "type": "boolean" + } + ] }, "vendorSourceMap": { "type": "boolean", "description": "Resolve vendor packages sourcemaps.", + "x-deprecated": true, "default": false }, "evalSourceMap": { @@ -134,4 +166,4 @@ "required": [ "browserTarget" ] -} +} \ No newline at end of file diff --git a/packages/angular_devkit/build_angular/src/karma/index.ts b/packages/angular_devkit/build_angular/src/karma/index.ts index 1bd80e91196e..431506ed59ec 100644 --- a/packages/angular_devkit/build_angular/src/karma/index.ts +++ b/packages/angular_devkit/build_angular/src/karma/index.ts @@ -27,7 +27,12 @@ import { import { readTsconfig } from '../angular-cli-files/utilities/read-tsconfig'; import { requireProjectModule } from '../angular-cli-files/utilities/require-project-module'; import { AssetPatternObject, CurrentFileReplacement } from '../browser/schema'; -import { defaultProgress, normalizeAssetPatterns, normalizeFileReplacements } from '../utils'; +import { + defaultProgress, + normalizeAssetPatterns, + normalizeFileReplacements, + normalizeSourceMaps, +} from '../utils'; import { KarmaBuilderSchema } from './schema'; const webpackMerge = require('webpack-merge'); @@ -41,7 +46,7 @@ export class KarmaBuilder implements Builder { constructor(public context: BuilderContext) { } run(builderConfig: BuilderConfiguration): Observable { - const options = builderConfig.options; + let options = builderConfig.options; const root = this.context.workspace.root; const projectRoot = resolve(root, builderConfig.root); const host = new virtualFs.AliasHost(this.context.host as virtualFs.Host); @@ -53,6 +58,17 @@ export class KarmaBuilder implements Builder { options.assets, host, root, projectRoot, builderConfig.sourceRoot)), // Replace the assets in options with the normalized version. tap((assetPatternObjects => options.assets = assetPatternObjects)), + tap(() => { + const normalizedOptions = normalizeSourceMaps(options.sourceMap); + // todo: remove when removing the deprecations + normalizedOptions.vendorSourceMap + = normalizedOptions.vendorSourceMap || !!options.vendorSourceMap; + + options = { + ...options, + ...normalizedOptions, + }; + }), concatMap(() => new Observable(obs => { const karma = requireProjectModule(getSystemPath(projectRoot), 'karma'); const karmaConfig = getSystemPath(resolve(root, normalize(options.karmaConfig))); diff --git a/packages/angular_devkit/build_angular/src/karma/schema.d.ts b/packages/angular_devkit/build_angular/src/karma/schema.d.ts index 4ea24b6afe19..632e6a55a5c4 100644 --- a/packages/angular_devkit/build_angular/src/karma/schema.d.ts +++ b/packages/angular_devkit/build_angular/src/karma/schema.d.ts @@ -7,17 +7,20 @@ */ import { BrowserBuilderSchema } from '../browser/schema'; - -// TODO: in TS 2.8 use Extract instead of Pick to make a subset of another type. export interface KarmaBuilderSchema extends Pick { /** * The name of the Karma configuration file.. */ karmaConfig: string; + /** + * Output sourcemaps. + */ + sourceMap: KarmaSourceMapOptions; + /** * Override which browsers tests are run against. */ @@ -38,3 +41,14 @@ export interface KarmaBuilderSchema extends Pick { constructor(public context: BuilderContext) { } run(builderConfig: BuilderConfiguration): Observable { - const options = builderConfig.options; + let options = builderConfig.options; const root = this.context.workspace.root; const projectRoot = resolve(root, builderConfig.root); const host = new virtualFs.AliasHost(this.context.host as virtualFs.Host); @@ -51,8 +51,19 @@ export class ServerBuilder implements Builder { concatMap(() => options.deleteOutputPath ? this._deleteOutputDir(root, normalize(options.outputPath), this.context.host) : of(null)), - concatMap(() => normalizeFileReplacements(options.fileReplacements, host, root)), - tap(fileReplacements => options.fileReplacements = fileReplacements), + concatMap(() => normalizeFileReplacements(options.fileReplacements, host, root)), + tap(fileReplacements => options.fileReplacements = fileReplacements), + tap(() => { + const normalizedOptions = normalizeSourceMaps(options.sourceMap); + // todo: remove when removing the deprecations + normalizedOptions.vendorSourceMap + = normalizedOptions.vendorSourceMap || !!options.vendorSourceMap; + + options = { + ...options, + ...normalizedOptions, + }; + }), concatMap(() => { const webpackConfig = this.buildWebpackConfig(root, projectRoot, host, options); diff --git a/packages/angular_devkit/build_angular/src/server/schema.d.ts b/packages/angular_devkit/build_angular/src/server/schema.d.ts index 0addbe5222e0..35acca4b0ae7 100644 --- a/packages/angular_devkit/build_angular/src/server/schema.d.ts +++ b/packages/angular_devkit/build_angular/src/server/schema.d.ts @@ -6,17 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ -import { FileReplacement } from '../browser/schema'; +import { BrowserBuilderSchema, FileReplacement, SourceMapOptions } from '../browser/schema'; export interface BuildWebpackServerSchema { /** * The name of the TypeScript configuration file. */ tsConfig: string; - /** - * Output sourcemaps. - */ - sourceMap?: boolean; /** * Adds more details to output logging. */ @@ -37,8 +33,13 @@ export interface BuildWebpackServerSchema { * Use a separate bundle containing only vendor libraries. */ vendorChunk?: boolean; + /** + * Output sourcemaps. + */ + sourceMap: SourceMapOptions; /** * Resolve vendor packages sourcemaps. + * @deprecated use sourceMap.vendor */ vendorSourceMap?: boolean; /** diff --git a/packages/angular_devkit/build_angular/src/server/schema.json b/packages/angular_devkit/build_angular/src/server/schema.json index de93450c3297..3cd83727fd61 100644 --- a/packages/angular_devkit/build_angular/src/server/schema.json +++ b/packages/angular_devkit/build_angular/src/server/schema.json @@ -50,13 +50,44 @@ "default": "" }, "sourceMap": { - "type": "boolean", "description": "Output sourcemaps.", - "default": true + "default": true, + "oneOf": [ + { + "type": "object", + "properties": { + "scripts": { + "type": "boolean", + "description": "Output sourcemaps for all scripts.", + "default": true + }, + "styles": { + "type": "boolean", + "description": "Output sourcemaps for all styles.", + "default": true + }, + "hidden": { + "type": "boolean", + "description": "Output sourcemaps used for error reporting tools.", + "default": false + }, + "vendor": { + "type": "boolean", + "description": "Resolve vendor packages sourcemaps.", + "default": false + } + }, + "additionalProperties": false + }, + { + "type": "boolean" + } + ] }, "vendorSourceMap": { "type": "boolean", "description": "Resolve vendor packages sourcemaps.", + "x-deprecated": true, "default": false }, "evalSourceMap": { @@ -220,6 +251,5 @@ } ] } - } } \ No newline at end of file diff --git a/packages/angular_devkit/build_angular/src/utils/index.ts b/packages/angular_devkit/build_angular/src/utils/index.ts index be8b36d7091a..e4b77f57dcbe 100644 --- a/packages/angular_devkit/build_angular/src/utils/index.ts +++ b/packages/angular_devkit/build_angular/src/utils/index.ts @@ -10,3 +10,4 @@ export * from './default-progress'; export * from './run-module-as-observable-fork'; export * from './normalize-file-replacements'; export * from './normalize-asset-patterns'; +export * from './normalize-source-maps'; diff --git a/packages/angular_devkit/build_angular/src/utils/normalize-source-maps.ts b/packages/angular_devkit/build_angular/src/utils/normalize-source-maps.ts new file mode 100644 index 000000000000..41392886b8b8 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/utils/normalize-source-maps.ts @@ -0,0 +1,32 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * 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 + */ + +import { SourceMapOptions } from '../browser/schema'; + +export interface NormalizedSourceMaps { + sourceMap: boolean; + scriptsSourceMap: boolean; + stylesSourceMap: boolean; + hiddenSourceMap: boolean; + vendorSourceMap: boolean; +} + +export function normalizeSourceMaps(sourceMap: SourceMapOptions): NormalizedSourceMaps { + const scriptsSourceMap = !!(typeof sourceMap === 'object' ? sourceMap.scripts : sourceMap); + const stylesSourceMap = !!(typeof sourceMap === 'object' ? sourceMap.styles : sourceMap); + const hiddenSourceMap = typeof sourceMap === 'object' && !!sourceMap.hidden; + const vendorSourceMap = typeof sourceMap === 'object' && !!sourceMap.vendor; + + return { + sourceMap: stylesSourceMap || scriptsSourceMap, + vendorSourceMap, + hiddenSourceMap, + scriptsSourceMap, + stylesSourceMap, + }; +} diff --git a/packages/angular_devkit/build_angular/test/browser/source-map_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/source-map_spec_large.ts index 4708376f1f7f..64d172c6c921 100644 --- a/packages/angular_devkit/build_angular/test/browser/source-map_spec_large.ts +++ b/packages/angular_devkit/build_angular/test/browser/source-map_spec_large.ts @@ -9,9 +9,10 @@ import { runTargetSpec } from '@angular-devkit/architect/testing'; import { join, normalize, virtualFs } from '@angular-devkit/core'; import { tap } from 'rxjs/operators'; +import { BrowserBuilderSchema, OutputHashing } from '../../src'; import { browserTargetSpec, host } from '../utils'; - +// tslint:disable-next-line:no-big-function describe('Browser Builder source map', () => { const outputPath = normalize('dist'); @@ -19,13 +20,38 @@ describe('Browser Builder source map', () => { afterEach(done => host.restore().toPromise().then(done, done.fail)); it('works', (done) => { - const overrides = { sourceMap: true }; + const overrides: Partial = { + sourceMap: true, + extractCss: true, + styles: ['src/styles.css'], + }; + + host.writeMultipleFiles({ + 'src/styles.css': `div { flex: 1 }`, + }); runTargetSpec(host, browserTargetSpec, overrides).pipe( tap((buildEvent) => expect(buildEvent.success).toBe(true)), tap(() => { - const fileName = join(outputPath, 'main.js.map'); - expect(host.scopedSync().exists(fileName)).toBe(true); + const scriptsFileName = join(outputPath, 'main.js.map'); + expect(host.scopedSync().exists(scriptsFileName)).toBe(true); + + const stylesFileName = join(outputPath, 'styles.css.map'); + expect(host.scopedSync().exists(stylesFileName)).toBe(true); + }), + ).toPromise().then(done, done.fail); + }); + + it('works with outputHashing', (done) => { + const overrides: Partial = { + sourceMap: true, + outputHashing: OutputHashing.All, + }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + expect(host.fileMatchExists(outputPath, /main\.[0-9a-f]{20}\.js.map/)).toBeTruthy(); }), ).toPromise().then(done, done.fail); }); @@ -55,4 +81,143 @@ describe('Browser Builder source map', () => { }), ).toPromise().then(done, done.fail); }); + + it('supports hidden sourcemaps', (done) => { + const overrides: Partial = { + sourceMap: { + hidden: true, + styles: true, + scripts: true, + }, + extractCss: true, + styles: ['src/styles.scss'], + }; + + host.writeMultipleFiles({ + 'src/styles.scss': `div { flex: 1 }`, + }); + + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + expect(host.scopedSync().exists(join(outputPath, 'main.js.map'))).toBe(true); + expect(host.scopedSync().exists(join(outputPath, 'styles.css.map'))).toBe(true); + + const scriptContent = virtualFs.fileBufferToString( + host.scopedSync().read(join(outputPath, 'main.js')), + ); + expect(scriptContent).not.toContain('sourceMappingURL=main.js.map'); + + const styleContent = virtualFs.fileBufferToString( + host.scopedSync().read(join(outputPath, 'styles.css')), + ); + expect(styleContent).not.toContain('sourceMappingURL=styles.css.map'); + }), + ).toPromise().then(done, done.fail); + }); + + it('supports styles only sourcemaps', (done) => { + const overrides: Partial = { + sourceMap: { + styles: true, + scripts: false, + }, + extractCss: true, + styles: ['src/styles.scss'], + }; + + host.writeMultipleFiles({ + 'src/styles.scss': `div { flex: 1 }`, + }); + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + expect(host.scopedSync().exists(join(outputPath, 'main.js.map'))).toBe(false); + expect(host.scopedSync().exists(join(outputPath, 'styles.css.map'))).toBe(true); + + const scriptContent = virtualFs.fileBufferToString( + host.scopedSync().read(join(outputPath, 'main.js')), + ); + expect(scriptContent).not.toContain('sourceMappingURL=main.js.map'); + + const styleContent = virtualFs.fileBufferToString( + host.scopedSync().read(join(outputPath, 'styles.css')), + ); + + expect(styleContent).toContain('sourceMappingURL=styles.css.map'); + }), + ).toPromise().then(done, done.fail); + }); + + it('supports scripts only sourcemaps', (done) => { + const overrides: Partial = { + sourceMap: { + styles: false, + scripts: true, + }, + extractCss: true, + styles: ['src/styles.scss'], + }; + + host.writeMultipleFiles({ + 'src/styles.scss': `div { flex: 1 }`, + }); + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + expect(host.scopedSync().exists(join(outputPath, 'main.js.map'))).toBe(true); + expect(host.scopedSync().exists(join(outputPath, 'styles.css.map'))).toBe(false); + + const scriptContent = virtualFs.fileBufferToString( + host.scopedSync().read(join(outputPath, 'main.js')), + ); + expect(scriptContent).toContain('sourceMappingURL=main.js.map'); + + const styleContent = virtualFs.fileBufferToString( + host.scopedSync().read(join(outputPath, 'styles.css')), + ); + expect(styleContent).not.toContain('sourceMappingURL=styles.css.map'); + }), + ).toPromise().then(done, done.fail); + }); + + it('should not inline component styles sourcemaps when hidden', (done) => { + const overrides: Partial = { + sourceMap: { + hidden: true, + styles: true, + scripts: true, + }, + extractCss: true, + styles: ['src/styles.scss'], + }; + + host.writeMultipleFiles({ + 'src/styles.scss': `div { flex: 1 }`, + 'src/app/app.component.css': `p { color: red; }`, + }); + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + expect(host.scopedSync().exists(join(outputPath, 'main.js.map'))).toBe(true); + expect(host.scopedSync().exists(join(outputPath, 'styles.css.map'))).toBe(true); + + const scriptContent = virtualFs.fileBufferToString( + host.scopedSync().read(join(outputPath, 'main.js')), + ); + + expect(scriptContent).not.toContain('sourceMappingURL=main.js.map'); + expect(scriptContent).not.toContain('sourceMappingURL=data:application/json'); + + const styleContent = virtualFs.fileBufferToString( + host.scopedSync().read(join(outputPath, 'styles.css')), + ); + expect(styleContent).not.toContain('sourceMappingURL=styles.css.map'); + }), + ).toPromise().then(done, done.fail); + }); }); diff --git a/packages/angular_devkit/build_angular/test/browser/vendor-source-map_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/vendor-source-map_spec_large.ts index 45bc61914039..00f7f52002df 100644 --- a/packages/angular_devkit/build_angular/test/browser/vendor-source-map_spec_large.ts +++ b/packages/angular_devkit/build_angular/test/browser/vendor-source-map_spec_large.ts @@ -10,6 +10,7 @@ import { runTargetSpec } from '@angular-devkit/architect/testing'; import { join, normalize, virtualFs } from '@angular-devkit/core'; import * as path from 'path'; import { tap } from 'rxjs/operators'; +import { BrowserBuilderSchema } from '../../src/browser/schema'; import { browserTargetSpec, host } from '../utils'; describe('Browser Builder external source map', () => { @@ -19,7 +20,13 @@ describe('Browser Builder external source map', () => { afterEach(done => host.restore().toPromise().then(done, done.fail)); it('works', (done) => { - const overrides = { sourceMap: true, vendorSourceMap: true }; + const overrides: Partial = { + sourceMap: { + scripts: true, + styles: true, + vendor: true, + }, + }; runTargetSpec(host, browserTargetSpec, overrides).pipe( tap((buildEvent) => expect(buildEvent.success).toBe(true)), @@ -35,7 +42,13 @@ describe('Browser Builder external source map', () => { }); it('does not map sourcemaps from external library when disabled', (done) => { - const overrides = { sourceMap: true, vendorSourceMap: false }; + const overrides: Partial = { + sourceMap: { + scripts: true, + styles: true, + vendor: false, + }, + }; runTargetSpec(host, browserTargetSpec, overrides).pipe( tap((buildEvent) => expect(buildEvent.success).toBe(true)), diff --git a/packages/angular_devkit/build_angular/test/server/base_spec_large.ts b/packages/angular_devkit/build_angular/test/server/base_spec_large.ts index 845666bf5e05..67c941150413 100644 --- a/packages/angular_devkit/build_angular/test/server/base_spec_large.ts +++ b/packages/angular_devkit/build_angular/test/server/base_spec_large.ts @@ -9,6 +9,7 @@ import { runTargetSpec } from '@angular-devkit/architect/testing'; import { join, normalize, virtualFs } from '@angular-devkit/core'; import { take, tap } from 'rxjs/operators'; +import { BuildWebpackServerSchema } from '../../src/server/schema'; import { host } from '../utils'; @@ -47,6 +48,58 @@ describe('Server Builder', () => { ).toPromise().then(done, done.fail); }); + it('supports scripts only sourcemaps', (done) => { + const overrides: Partial = { + sourceMap: { + styles: false, + scripts: true, + }, + }; + + host.writeMultipleFiles({ + 'src/app/app.component.css': `p { color: red; }`, + }); + + runTargetSpec(host, { project: 'app', target: 'server' }, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + expect(host.scopedSync().exists(join(outputPath, 'main.js.map'))).toBe(true); + + const scriptContent = virtualFs.fileBufferToString( + host.scopedSync().read(join(outputPath, 'main.js')), + ); + expect(scriptContent).toContain('sourceMappingURL=main.js.map'); + expect(scriptContent).not.toContain('sourceMappingURL=data:application/json'); + }), + ).toPromise().then(done, done.fail); + }); + + it('supports component styles sourcemaps', (done) => { + const overrides: Partial = { + sourceMap: { + styles: true, + scripts: true, + }, + }; + + host.writeMultipleFiles({ + 'src/app/app.component.css': `p { color: red; }`, + }); + + runTargetSpec(host, { project: 'app', target: 'server' }, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + expect(host.scopedSync().exists(join(outputPath, 'main.js.map'))).toBe(true); + + const scriptContent = virtualFs.fileBufferToString( + host.scopedSync().read(join(outputPath, 'main.js')), + ); + expect(scriptContent).toContain('sourceMappingURL=main.js.map'); + expect(scriptContent).toContain('sourceMappingURL=data:application/json'); + }), + ).toPromise().then(done, done.fail); + }); + it('runs watch mode', (done) => { const overrides = { watch: true };