From b1f0e2227ddaa2562bd4240b36cdc59452139820 Mon Sep 17 00:00:00 2001 From: christophercr Date: Wed, 23 Jan 2019 17:13:13 +0100 Subject: [PATCH] feat(stark-build): adapt build utils and webpack config to read global styles from angular.json to align with Angular CLI ISSUES CLOSED: #1070 BREAKING CHANGE: global styles must be included in the angular.json (standard Angular CLI behavior) instead of importing them directly in the app --- packages/stark-build/config/build-utils.js | 76 ++++++++- packages/stark-build/config/ng-cli-utils.js | 149 ++++++++++++++++++ packages/stark-build/config/webpack.common.js | 20 ++- packages/stark-build/config/webpack.dev.js | 8 +- packages/stark-build/config/webpack.prod.js | 7 +- showcase/angular.json | 2 +- showcase/src/app/app.module.ts | 7 - starter/angular.json | 2 +- starter/src/app/app.module.ts | 4 - 9 files changed, 241 insertions(+), 34 deletions(-) diff --git a/packages/stark-build/config/build-utils.js b/packages/stark-build/config/build-utils.js index d9bdcff81a..42fb67e18d 100644 --- a/packages/stark-build/config/build-utils.js +++ b/packages/stark-build/config/build-utils.js @@ -13,7 +13,8 @@ const ANGULAR_APP_CONFIG = { deployUrl: angularCliAppConfig.architect.build.options.deployUrl || "", baseHref: angularCliAppConfig.architect.build.options.baseHref || "", sourceRoot: angularCliAppConfig.sourceRoot, - outputPath: angularCliAppConfig.architect.build.options.outputPath + outputPath: angularCliAppConfig.architect.build.options.outputPath, + buildOptions: angularCliAppConfig.architect.build.options || {} }; const DEFAULT_METADATA = { @@ -41,6 +42,65 @@ function readTsConfig(tsConfigPath) { return ts.parseJsonConfigFileContent(configResult.config, ts.sys, path.dirname(tsConfigPath), undefined, tsConfigPath); } +/** + * Logic extracted from @angular-devkit/build-angular/src/angular-cli-files/models/webpack-configs/styles.js + * + * @returns {{entryPoints: {}, globalStylePaths: Array}} + */ +function getApplicationGlobalStylesConfig() { + const stylesConfig = { entryPoints: {}, globalStylePaths: [] }; + + if (ANGULAR_APP_CONFIG.buildOptions.styles.length > 0) { + // const chunkNames = []; + ngCliUtils.normalizeExtraEntryPoints(ANGULAR_APP_CONFIG.buildOptions.styles, "styles").forEach(style => { + const resolvedPath = path.resolve(ANGULAR_APP_CONFIG.config.root, style.input); + // Add style entry points. + if (stylesConfig.entryPoints[style.bundleName]) { + stylesConfig.entryPoints[style.bundleName].push(resolvedPath); + } else { + stylesConfig.entryPoints[style.bundleName] = [resolvedPath]; + } + /// Add lazy styles to the list. + /// TODO not used for the moment + /// if (style.lazy) { + /// chunkNames.push(style.bundleName); + /// } + /// Add global css paths. + stylesConfig.globalStylePaths.push(resolvedPath); + }); + /// TODO not used for the moment + /// if (chunkNames.length > 0) { + /// // Add plugin to remove hashes from lazy styles. + /// extraPlugins.push(new webpack_1.RemoveHashPlugin({ chunkNames, hashFormat })); + /// } + } + + return stylesConfig; +} + +/** + * Method from @angular-devkit/build-angular/src/angular-cli-files/utilities/package-chunk-sort.js + * @returns {string[]} + */ +function generateEntryPoints() { + let entryPoints = ["polyfills", "sw-register"]; + // Add all styles/scripts, except lazy-loaded ones. + [ + ...Object.keys(getApplicationGlobalStylesConfig().entryPoints) + /// TODO not used for the moment + /// ...ngCliUtils.normalizeExtraEntryPoints(appConfig.scripts, 'scripts') + /// .filter(entry => !entry.lazy) + /// .map(entry => entry.bundleName), + ].forEach(bundleName => { + if (entryPoints.indexOf(bundleName) === -1) { + entryPoints.push(bundleName); + } + }); + + entryPoints.push("main"); + return entryPoints; +} + /** * Read the content of angular.json to get the path of the environment file. * It returns the path of the replacement file defined in "fileReplacements" of the environment or the default file @@ -136,10 +196,10 @@ function getNbbAssetsConfig() { * See: https://github.com/angular/angular-cli/wiki/angular-workspace */ function getApplicationAssetsConfig() { - const appConfig = ANGULAR_APP_CONFIG.config; + const buildOptions = ANGULAR_APP_CONFIG.buildOptions; - if (appConfig.architect && appConfig.architect.build && appConfig.architect.build.options && appConfig.architect.build.options.assets) { - return getCopyWebpackPluginConfig(appConfig.architect.build.options.assets); + if (buildOptions.assets && buildOptions.assets instanceof Array) { + return getCopyWebpackPluginConfig(buildOptions.assets); } return []; @@ -212,8 +272,10 @@ function getCopyWebpackPluginConfig(assets) { exports.ANGULAR_APP_CONFIG = ANGULAR_APP_CONFIG; exports.DEFAULT_METADATA = DEFAULT_METADATA; -exports.supportES2015 = supportES2015; -exports.readTsConfig = readTsConfig; +exports.generateEntryPoints = generateEntryPoints; +exports.getApplicationAssetsConfig = getApplicationAssetsConfig; +exports.getApplicationGlobalStylesConfig = getApplicationGlobalStylesConfig; exports.getEnvironmentFile = getEnvironmentFile; exports.getNbbAssetsConfig = getNbbAssetsConfig; -exports.getApplicationAssetsConfig = getApplicationAssetsConfig; +exports.readTsConfig = readTsConfig; +exports.supportES2015 = supportES2015; diff --git a/packages/stark-build/config/ng-cli-utils.js b/packages/stark-build/config/ng-cli-utils.js index aee3385e1a..9eb4f610e0 100644 --- a/packages/stark-build/config/ng-cli-utils.js +++ b/packages/stark-build/config/ng-cli-utils.js @@ -3,6 +3,24 @@ const fs = require("fs"); const cliUtilConfig = require("@angular/cli/utilities/config"); const { formatDiagnostics } = require("@angular/compiler-cli/ngtools2"); +/** + * The Separator for normalized path. + * @type {string} + */ +const normalizedSep = "/"; + +/** + * The root of a normalized path. + * @type {string} + */ +const normalizedRoot = normalizedSep; + +/** + * normalize() cache to reduce computation. For now this grows and we never flush it, but in the + * future we might want to add a few cache flush to prevent this from growing too large. + */ +let normalizedCache = new Map(); + function isDirectory(pathToCheck) { try { return fs.statSync(pathToCheck).isDirectory(); @@ -63,8 +81,139 @@ function getWorkspace() { return cliUtilConfig.getWorkspace(); } +/** + * Code taken from @angular-devkit/build-angular/src/angular-cli-files/models/webpack-configs/utils.js + * + * @param extraEntryPoints + * @param defaultBundleName + * @returns {*} + */ +function normalizeExtraEntryPoints(extraEntryPoints, defaultBundleName) { + return extraEntryPoints.map(entry => { + let normalizedEntry; + if (typeof entry === "string") { + normalizedEntry = { input: entry, lazy: false, bundleName: defaultBundleName }; + } else { + let bundleName; + if (entry.bundleName) { + bundleName = entry.bundleName; + } else if (entry.lazy) { + // Lazy entry points use the file name as bundle name. + bundleName = basename(normalize(entry.input.replace(/\.(js|css|scss|sass|less|styl)$/i, ""))); + } else { + bundleName = defaultBundleName; + } + normalizedEntry = Object.assign({}, entry, { bundleName }); + } + return normalizedEntry; + }); +} + +/** + * Code taken from @angular-devkit/core/src/virtual-fs/path.js + * + * Return the basename of the path, as a Path. See path.basename + */ +function basename(path) { + const i = path.lastIndexOf(normalizedSep); + if (i === -1) { + return fragment(path); + } else { + return fragment(path.substr(path.lastIndexOf(normalizedSep) + 1)); + } +} + +/** + * Code taken from @angular-devkit/core/src/virtual-fs/path.js + */ +function fragment(path) { + if (path.indexOf("/") !== -1) { + throw new Exception(path); + } + return path; +} + +/** + * Code taken from @angular-devkit/core/src/virtual-fs/path.js + * + * Normalize a string into a Path. This is the only mean to get a Path type from a string that + * represents a system path. This method cache the results as real world paths tend to be + * duplicated often. + * Normalization includes: + * - Windows backslashes `\\` are replaced with `/`. + * - Windows drivers are replaced with `/X/`, where X is the drive letter. + * - Absolute paths starts with `/`. + * - Multiple `/` are replaced by a single one. + * - Path segments `.` are removed. + * - Path segments `..` are resolved. + * - If a path is absolute, having a `..` at the start is invalid (and will throw). + * @param path The path to be normalized. + */ +function normalize(path) { + let maybePath = normalizedCache.get(path); + if (!maybePath) { + maybePath = noCacheNormalize(path); + normalizedCache.set(path, maybePath); + } + return maybePath; +} + +/** + * Code taken from @angular-devkit/core/src/virtual-fs/path.js + * + * The no cache version of the normalize() function. Used for benchmarking and testing. + */ +function noCacheNormalize(path) { + if (path == "" || path == ".") { + return ""; + } else if (path == normalizedRoot) { + return normalizedRoot; + } + // Match absolute windows path. + const original = path; + if (path.match(/^[A-Z]:[\/\\]/i)) { + path = "\\" + path[0] + "\\" + path.substr(3); + } + // We convert Windows paths as well here. + const p = path.split(/[\/\\]/g); + let relative = false; + let i = 1; + // Special case the first one. + if (p[0] != "") { + p.unshift("."); + relative = true; + } + while (i < p.length) { + if (p[i] == ".") { + p.splice(i, 1); + } else if (p[i] == "..") { + if (i < 2 && !relative) { + throw new Error(`Path ${JSON.stringify(original)} is invalid.`); + } else if (i >= 2 && p[i - 1] != "..") { + p.splice(i - 1, 2); + i--; + } else { + i++; + } + } else if (p[i] == "") { + p.splice(i, 1); + } else { + i++; + } + } + if (p.length == 1) { + return p[0] == "" ? normalizedSep : ""; + } else { + if (p[0] == ".") { + p.shift(); + } + return p.join(normalizedSep); + } +} + exports.getAngularCliAppConfig = getAngularCliAppConfig; exports.getDirectoriesNames = getDirectoriesNames; exports.getWorkspace = getWorkspace; exports.isDirectory = isDirectory; +exports.normalizeExtraEntryPoints = normalizeExtraEntryPoints; exports.validateAngularCLIConfig = validateAngularCLIConfig; diff --git a/packages/stark-build/config/webpack.common.js b/packages/stark-build/config/webpack.common.js index ad20846041..695ae1bf79 100644 --- a/packages/stark-build/config/webpack.common.js +++ b/packages/stark-build/config/webpack.common.js @@ -33,10 +33,12 @@ module.exports = options => { const METADATA = Object.assign({}, buildUtils.DEFAULT_METADATA, options.metadata || {}); const supportES2015 = buildUtils.supportES2015(METADATA.TS_CONFIG_PATH); - const entry = { - polyfills: "./src/polyfills.browser.ts", - main: "./src/main.browser.ts" - }; + const globalStylePaths = buildUtils.getApplicationGlobalStylesConfig().globalStylePaths; + + const entry = Object.assign({}, buildUtils.getApplicationGlobalStylesConfig().entryPoints, { + polyfills: helpers.root(buildUtils.ANGULAR_APP_CONFIG.buildOptions.polyfills), + main: helpers.root(buildUtils.ANGULAR_APP_CONFIG.buildOptions.main) + }); const tsConfigApp = buildUtils.readTsConfig(helpers.root(METADATA.TS_CONFIG_PATH)); @@ -225,7 +227,7 @@ module.exports = options => { } } ], - exclude: [helpers.root(buildUtils.ANGULAR_APP_CONFIG.sourceRoot, "styles")] + exclude: globalStylePaths }, /** @@ -257,7 +259,7 @@ module.exports = options => { }, "sass-loader" ], - exclude: [helpers.root(buildUtils.ANGULAR_APP_CONFIG.sourceRoot, "styles")] + exclude: globalStylePaths }, /** @@ -288,7 +290,7 @@ module.exports = options => { } } ], - exclude: [helpers.root(buildUtils.ANGULAR_APP_CONFIG.sourceRoot, "styles")] + exclude: globalStylePaths }, /** @@ -428,7 +430,9 @@ module.exports = options => { template: "src/index.html", title: METADATA.TITLE, chunksSortMode: function(a, b) { - const entryPoints = ["inline", "polyfills", "sw-register", "styles", "vendor", "main"]; + // generated entry points will include those from styles config + // logic extracted from getBrowserConfig function in @angular-devkit/build-angular/src/angular-cli-files/models/webpack-configs/browser.js + const entryPoints = buildUtils.generateEntryPoints(); return entryPoints.indexOf(a.names[0]) - entryPoints.indexOf(b.names[0]); }, metadata: METADATA, diff --git a/packages/stark-build/config/webpack.dev.js b/packages/stark-build/config/webpack.dev.js index 879bec80f5..9c855fa53c 100644 --- a/packages/stark-build/config/webpack.dev.js +++ b/packages/stark-build/config/webpack.dev.js @@ -41,6 +41,8 @@ module.exports = function(env) { // PUBLIC: process.env.PUBLIC_DEV || HOST + ':' + PORT // TODO check if needed/useful in our case? }); + const globalStylePaths = buildUtils.getApplicationGlobalStylesConfig().globalStylePaths; + // Directives to be used in CSP header const cspDirectives = [ "base-uri 'self'", @@ -180,7 +182,7 @@ module.exports = function(env) { } } ], - include: [helpers.root(buildUtils.ANGULAR_APP_CONFIG.sourceRoot, "styles")] + include: globalStylePaths }, /** @@ -215,7 +217,7 @@ module.exports = function(env) { }, "sass-loader" ], - include: [helpers.root(buildUtils.ANGULAR_APP_CONFIG.sourceRoot, "styles")] + include: globalStylePaths }, /** @@ -246,7 +248,7 @@ module.exports = function(env) { } } ], - include: [helpers.root(buildUtils.ANGULAR_APP_CONFIG.sourceRoot, "styles")] + include: globalStylePaths } ] }, diff --git a/packages/stark-build/config/webpack.prod.js b/packages/stark-build/config/webpack.prod.js index c82860e474..7a0d295142 100644 --- a/packages/stark-build/config/webpack.prod.js +++ b/packages/stark-build/config/webpack.prod.js @@ -55,6 +55,7 @@ module.exports = function() { }); const isCITestEnv = helpers.hasProcessFlag("ci-test-env"); const supportES2015 = buildUtils.supportES2015(METADATA.TS_CONFIG_PATH); + const globalStylePaths = buildUtils.getApplicationGlobalStylesConfig().globalStylePaths; const webpackConfig = webpackMerge(commonConfig({ ENV: ENV, metadata: METADATA }), { /** @@ -220,7 +221,7 @@ module.exports = function() { } } ], - include: [helpers.root(buildUtils.ANGULAR_APP_CONFIG.sourceRoot, "styles")] + include: globalStylePaths }, /** @@ -250,7 +251,7 @@ module.exports = function() { }, "sass-loader" ], - include: [helpers.root(buildUtils.ANGULAR_APP_CONFIG.sourceRoot, "styles")] + include: globalStylePaths }, /** @@ -279,7 +280,7 @@ module.exports = function() { } } ], - include: [helpers.root(buildUtils.ANGULAR_APP_CONFIG.sourceRoot, "styles")] + include: globalStylePaths } ] }, diff --git a/showcase/angular.json b/showcase/angular.json index 71f4f28395..28093da3c6 100644 --- a/showcase/angular.json +++ b/showcase/angular.json @@ -33,7 +33,7 @@ "output": "./" } ], - "styles": ["src/styles.css"], + "styles": ["src/styles/styles.scss", "src/styles/styles.pcss"], "scripts": [], "deployUrl": "/", "baseHref": "/" diff --git a/showcase/src/app/app.module.ts b/showcase/src/app/app.module.ts index 5fdbd37ff8..8c03283e32 100644 --- a/showcase/src/app/app.module.ts +++ b/showcase/src/app/app.module.ts @@ -84,13 +84,6 @@ import { APP_STATES } from "./app.routes"; // App is our top level component import { AppComponent } from "./app.component"; -/* tslint:disable:no-import-side-effect */ -// load PostCSS styles -import "../styles/styles.pcss"; -// load SASS styles -import "../styles/styles.scss"; -/* tslint:enable */ - // TODO: where to put this factory function? export function starkAppConfigFactory(): StarkApplicationConfig { const config: any = require("../stark-app-config.json"); diff --git a/starter/angular.json b/starter/angular.json index 842bff499b..a871405787 100644 --- a/starter/angular.json +++ b/starter/angular.json @@ -33,7 +33,7 @@ "output": "./" } ], - "styles": ["src/styles.css"], + "styles": ["src/styles/styles.scss"], "scripts": [], "deployUrl": "", "baseHref": "/" diff --git a/starter/src/app/app.module.ts b/starter/src/app/app.module.ts index 8c0f6ef9c4..67e8403d3b 100644 --- a/starter/src/app/app.module.ts +++ b/starter/src/app/app.module.ts @@ -74,10 +74,6 @@ import { environment } from "../environments/environment"; import { APP_STATES } from "./app.routes"; // App is our top level component import { AppComponent } from "./app.component"; -/* tslint:disable:no-import-side-effect */ -// load SASS styles -import "../styles/styles.scss"; -/* tslint:enable */ // TODO: where to put this factory function? export function starkAppConfigFactory(): StarkApplicationConfig {