feat(@schematics/angular): production builds by default

With this change we do several changes to the `angular.json` configuration for `build` , `server` and `app-shell` targets so that these are `production` by default.

- build, server and app-shell targets are configured to run production by default.
- We add a new configuration named `development` to run the mentioned builder targets in development. Ex: `ng build --configuration development`.
- When adding `universal` or `app-shell`, we generate the full set of configurations as per the `buiid` target. Previously, we only generated the `production` configuration.
- We added a helper script in `package.json` to run build in watch mode. `npm run watch` which is a shortcut for `ng build --watch --configuration development`
This commit is contained in:
Alan Agius 2021-02-26 10:42:55 +01:00 committed by Filipe Silva
parent a5877bf917
commit 1de6d71edd
16 changed files with 128 additions and 108 deletions

View File

@ -52,7 +52,7 @@ describe('PWA Schematic', () => {
schematicRunner.runSchematicAsync('ng-add', defaultOptions, appTree).toPromise().then(tree => {
const configText = tree.readContent('/angular.json');
const config = JSON.parse(configText);
const swFlag = config.projects.bar.architect.build.configurations.production.serviceWorker;
const swFlag = config.projects.bar.architect.build.options.serviceWorker;
expect(swFlag).toEqual(true);
done();
}, done.fail);

View File

@ -155,7 +155,7 @@ function addUniversalTarget(options: AppShellOptions): Rule {
}
function addAppShellConfigToWorkspace(options: AppShellOptions): Rule {
return () => {
return (host, context) => {
if (!options.route) {
throw new SchematicsException(`Route is not defined`);
}
@ -166,20 +166,44 @@ function addAppShellConfigToWorkspace(options: AppShellOptions): Rule {
return;
}
// Validation of targets is handled already in the main function.
// Duplicate keys means that we have configurations in both server and build builders.
const serverConfigKeys = project.targets.get('server')?.configurations ?? {};
const buildConfigKeys = project.targets.get('build')?.configurations ?? {};
const configurationNames = Object.keys({
...serverConfigKeys,
...buildConfigKeys,
});
const configurations: Record<string, {}> = {};
for (const key of configurationNames) {
if (!serverConfigKeys[key]) {
context.logger.warn(`Skipped adding "${key}" configuration to "app-shell" target as it's missing from "server" target.`);
continue;
}
if (!buildConfigKeys[key]) {
context.logger.warn(`Skipped adding "${key}" configuration to "app-shell" target as it's missing from "build" target.`);
continue;
}
configurations[key] = {
browserTarget: `${options.clientProject}:build:${key}`,
serverTarget: `${options.clientProject}:server:${key}`,
};
}
project.targets.add({
name: 'app-shell',
builder: Builders.AppShell,
defaultConfiguration: configurations['production'] ? 'production' : undefined,
options: {
browserTarget: `${options.clientProject}:build`,
serverTarget: `${options.clientProject}:server`,
route: options.route,
},
configurations: {
production: {
browserTarget: `${options.clientProject}:build:production`,
serverTarget: `${options.clientProject}:server:production`,
},
},
configurations,
});
});
};
@ -242,9 +266,9 @@ function addServerRoutes(options: AppShellOptions): Rule {
if (!isImported(moduleSource, 'Routes', '@angular/router')) {
const recorder = host.beginUpdate(modulePath);
const routesChange = insertImport(moduleSource,
modulePath,
'Routes',
'@angular/router');
modulePath,
'Routes',
'@angular/router');
if (routesChange) {
applyToUpdateRecorder(recorder, [routesChange]);
}
@ -263,16 +287,16 @@ function addServerRoutes(options: AppShellOptions): Rule {
if (!isImported(moduleSource, 'RouterModule', '@angular/router')) {
const recorder = host.beginUpdate(modulePath);
const routerModuleChange = insertImport(moduleSource,
modulePath,
'RouterModule',
'@angular/router');
modulePath,
'RouterModule',
'@angular/router');
if (routerModuleChange) {
applyToUpdateRecorder(recorder, [routerModuleChange]);
}
const metadataChange = addSymbolToNgModuleMetadata(
moduleSource, modulePath, 'imports', 'RouterModule.forRoot(routes)');
moduleSource, modulePath, 'imports', 'RouterModule.forRoot(routes)');
if (metadataChange) {
applyToUpdateRecorder(recorder, metadataChange);
}

View File

@ -68,9 +68,9 @@ describe('App Shell Schematic', () => {
const content = tree.readContent(filePath);
const workspace = JSON.parse(content);
const target = workspace.projects.bar.architect['app-shell'];
expect(target.options.browserTarget).toEqual('bar:build');
expect(target.options.serverTarget).toEqual('bar:server');
expect(target.options.route).toEqual('shell');
expect(target.configurations.development.browserTarget).toEqual('bar:build:development');
expect(target.configurations.development.serverTarget).toEqual('bar:server:development');
expect(target.configurations.production.browserTarget).toEqual('bar:build:production');
expect(target.configurations.production.serverTarget).toEqual('bar:server:production');
});

View File

@ -172,6 +172,7 @@ function addAppToWorkspaceFile(options: ApplicationOptions, appDir: string): Rul
targets: {
build: {
builder: Builders.Browser,
defaultConfiguration: 'production',
options: {
outputPath: `dist/${options.name}`,
index: `${sourceRoot}/index.html`,
@ -190,30 +191,35 @@ function addAppToWorkspaceFile(options: ApplicationOptions, appDir: string): Rul
},
configurations: {
production: {
budgets,
fileReplacements: [{
replace: `${sourceRoot}/environments/environment.ts`,
with: `${sourceRoot}/environments/environment.prod.ts`,
}],
buildOptimizer: true,
optimization: true,
outputHashing: 'all',
sourceMap: false,
namedChunks: false,
extractLicenses: true,
vendorChunk: false,
buildOptimizer: true,
budgets,
},
development: {
vendorChunk: true,
},
},
},
serve: {
builder: Builders.DevServer,
options: {
browserTarget: `${options.name}:build`,
},
defaultConfiguration: 'development',
options: {},
configurations: {
production: {
browserTarget: `${options.name}:build:production`,
},
development: {
browserTarget: `${options.name}:build:development`,
},
},
},
'extract-i18n': {

View File

@ -48,14 +48,17 @@ export default function (options: E2eOptions): Rule {
project.targets.add({
name: 'e2e',
builder: Builders.Protractor,
defaultConfiguration: 'development',
options: {
protractorConfig: `${root}/protractor.conf.js`,
devServerTarget: `${options.relatedAppName}:serve`,
},
configurations: {
production: {
devServerTarget: `${options.relatedAppName}:serve:production`,
},
development: {
devServerTarget: `${options.relatedAppName}:serve:development`,
},
},
});

View File

@ -94,9 +94,9 @@ describe('Application Schematic', () => {
const tree = await schematicRunner.runSchematicAsync('e2e', defaultOptions, applicationTree)
.toPromise();
const workspace = JSON.parse(tree.readContent('/angular.json'));
const e2eOptions = workspace.projects.foo.architect.e2e.options;
expect(e2eOptions.protractorConfig).toEqual('projects/foo/e2e/protractor.conf.js');
expect(e2eOptions.devServerTarget).toEqual('foo:serve');
const { options, configurations } = workspace.projects.foo.architect.e2e;
expect(options.protractorConfig).toEqual('projects/foo/e2e/protractor.conf.js');
expect(configurations.development.devServerTarget).toEqual('foo:serve:development');
});
});

View File

@ -95,14 +95,17 @@ function addLibToWorkspaceFile(
targets: {
build: {
builder: Builders.NgPackagr,
defaultConfiguration: 'production',
options: {
tsConfig: `${projectRoot}/tsconfig.lib.json`,
project: `${projectRoot}/ng-package.json`,
},
configurations: {
production: {
tsConfig: `${projectRoot}/tsconfig.lib.prod.json`,
},
development: {
tsConfig: `${projectRoot}/tsconfig.lib.json`,
},
},
},
test: {

View File

@ -309,22 +309,22 @@ describe('Library Schematic', () => {
const config = getJsonFileContent(tree, '/angular.json');
const project = config.projects.foo;
expect(project.root).toEqual('foo');
const buildOpt = project.architect.build.options;
expect(buildOpt.project).toEqual('foo/ng-package.json');
expect(buildOpt.tsConfig).toEqual('foo/tsconfig.lib.json');
const { options, configurations } = project.architect.build;
expect(options.project).toEqual('foo/ng-package.json');
expect(configurations.production.tsConfig).toEqual('foo/tsconfig.lib.prod.json');
const appTsConfig = getJsonFileContent(tree, '/foo/tsconfig.lib.json');
expect(appTsConfig.extends).toEqual('../tsconfig.json');
const libTsConfig = getJsonFileContent(tree, '/foo/tsconfig.lib.json');
expect(libTsConfig.extends).toEqual('../tsconfig.json');
const specTsConfig = getJsonFileContent(tree, '/foo/tsconfig.spec.json');
expect(specTsConfig.extends).toEqual('../tsconfig.json');
});
it(`should add 'production' configuration`, async () => {
it(`should add 'development' configuration`, async () => {
const tree = await schematicRunner.runSchematicAsync('library', defaultOptions, workspaceTree)
.toPromise();
const workspace = JSON.parse(tree.readContent('/angular.json'));
expect(workspace.projects.foo.architect.build.configurations.production).toBeDefined();
expect(workspace.projects.foo.architect.build.configurations.development).toBeDefined();
});
it(`should add 'ng-packagr' builder`, async () => {

View File

@ -49,6 +49,8 @@ describe('Migration to version 9', () => {
tree,
)
.toPromise();
tree.overwrite('angular.json', tree.readContent('angular.json').replace(/development/g, 'production'));
});
describe('i18n configuration', () => {

View File

@ -93,6 +93,8 @@ describe('Migration to version 9', () => {
);
tree.overwrite('tsconfig.app.json', tsConfig);
tree.overwrite('angular.json', tree.readContent('angular.json').replace(/development/g, 'production'));
});
describe('scripts and style options', () => {
@ -277,6 +279,8 @@ describe('Migration to version 9', () => {
tree,
)
.toPromise();
tree.overwrite('angular.json', tree.readContent('angular.json').replace(/development/g, 'production'));
});
it('should add optimization option when not defined', async () => {

View File

@ -130,21 +130,12 @@ export default function (options: ServiceWorkerOptions): Rule {
if (!buildTarget) {
throw targetBuildNotFoundError();
}
const buildOptions =
(buildTarget.options || {}) as unknown as BrowserBuilderOptions;
let buildConfiguration;
if (options.configuration && buildTarget.configurations) {
buildConfiguration =
buildTarget.configurations[options.configuration] as unknown as BrowserBuilderOptions | undefined;
}
const config = buildConfiguration || buildOptions;
const buildOptions = (buildTarget.options || {}) as unknown as BrowserBuilderOptions;
const root = project.root;
buildOptions.serviceWorker = true;
buildOptions.ngswConfigPath = join(normalize(root), 'ngsw-config.json');
config.serviceWorker = true;
config.ngswConfigPath = join(normalize(root), 'ngsw-config.json');
let { resourcesOutputPath = '' } = config;
let { resourcesOutputPath = '' } = buildOptions;
if (resourcesOutputPath) {
resourcesOutputPath = normalize(`/${resourcesOutputPath}`);
}

View File

@ -19,7 +19,7 @@ describe('Service Worker Schematic', () => {
const defaultOptions: ServiceWorkerOptions = {
project: 'bar',
target: 'build',
configuration: 'production',
configuration: '',
};
let appTree: UnitTestTree;
@ -45,25 +45,13 @@ describe('Service Worker Schematic', () => {
.toPromise();
});
it('should update the production configuration', async () => {
it('should add `serviceWorker` option to build target', async () => {
const tree = await schematicRunner.runSchematicAsync('service-worker', defaultOptions, appTree)
.toPromise();
const configText = tree.readContent('/angular.json');
const config = JSON.parse(configText);
const swFlag = config.projects.bar.architect
.build.configurations.production.serviceWorker;
expect(swFlag).toEqual(true);
});
const buildConfig = JSON.parse(configText).projects.bar.architect.build;
it('should update the target options if no configuration is set', async () => {
const options = { ...defaultOptions, configuration: '' };
const tree = await schematicRunner.runSchematicAsync('service-worker', options, appTree)
.toPromise();
const configText = tree.readContent('/angular.json');
const config = JSON.parse(configText);
const swFlag = config.projects.bar.architect
.build.options.serviceWorker;
expect(swFlag).toEqual(true);
expect(buildConfig.options.serviceWorker).toBeTrue();
});
it('should add the necessary dependency', async () => {
@ -162,8 +150,7 @@ describe('Service Worker Schematic', () => {
expect(tree.exists(path)).toEqual(true);
const { projects } = JSON.parse(tree.readContent('/angular.json'));
expect(projects.bar.architect.build.configurations.production.ngswConfigPath)
.toBe('projects/bar/ngsw-config.json');
expect(projects.bar.architect.build.options.ngswConfigPath).toBe('projects/bar/ngsw-config.json');
});
it('should add $schema in ngsw-config.json with correct relative path', async () => {
@ -214,7 +201,7 @@ describe('Service Worker Schematic', () => {
it('should add resourcesOutputPath to root assets when specified', async () => {
const config = JSON.parse(appTree.readContent('/angular.json'));
config.projects.bar.architect.build.configurations.production.resourcesOutputPath = 'outDir';
config.projects.bar.architect.build.options.resourcesOutputPath = 'outDir';
appTree.overwrite('/angular.json', JSON.stringify(config));
const tree = await schematicRunner.runSchematicAsync('service-worker', defaultOptions, appTree)
.toPromise();
@ -243,7 +230,6 @@ describe('Service Worker Schematic', () => {
expect(tree.exists('/ngsw-config.json')).toBe(true);
const { projects } = JSON.parse(tree.readContent('/angular.json'));
expect(projects.foo.architect.build.configurations.production.ngswConfigPath)
.toBe('ngsw-config.json');
expect(projects.foo.architect.build.options.ngswConfigPath).toBe('ngsw-config.json');
});
});

View File

@ -20,7 +20,7 @@
"configuration": {
"type": "string",
"description": "The configuration to apply service worker to.",
"default": "production"
"x-deprecated": "No longer has an effect."
}
},
"required": [

View File

@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {
JsonValue,
Path,
basename,
join,
@ -35,62 +36,65 @@ import { findBootstrapModuleCall, findBootstrapModulePath } from '../utility/ng-
import { relativePathToWorkspaceRoot } from '../utility/paths';
import { targetBuildNotFoundError } from '../utility/project-targets';
import { getWorkspace, updateWorkspace } from '../utility/workspace';
import { BrowserBuilderOptions, Builders, OutputHashing } from '../utility/workspace-models';
import { BrowserBuilderOptions, Builders } from '../utility/workspace-models';
import { Schema as UniversalOptions } from './schema';
function updateConfigFile(options: UniversalOptions, tsConfigDirectory: Path): Rule {
return updateWorkspace(workspace => {
const clientProject = workspace.projects.get(options.clientProject);
if (clientProject) {
const buildTarget = clientProject.targets.get('build');
let fileReplacements;
if (buildTarget && buildTarget.configurations && buildTarget.configurations.production) {
fileReplacements = buildTarget.configurations.production.fileReplacements;
}
if (buildTarget && buildTarget.options) {
buildTarget.options.outputPath = `dist/${options.clientProject}/browser`;
}
// In case the browser builder hashes the assets
// we need to add this setting to the server builder
// as otherwise when assets it will be requested twice.
// One for the server which will be unhashed, and other on the client which will be hashed.
let outputHashing: OutputHashing | undefined;
if (buildTarget && buildTarget.configurations && buildTarget.configurations.production) {
switch (buildTarget.configurations.production.outputHashing as OutputHashing) {
case 'all':
case 'media':
outputHashing = 'media';
break;
const getServerOptions = (options: Record<string, JsonValue | undefined> = {}): {} => {
return {
outputHashing: options?.outputHashing === 'all' ? 'media' : options?.outputHashing,
fileReplacements: options?.fileReplacements,
optimization: options?.optimization === undefined ? undefined : !!options?.optimization,
sourceMap: options?.sourceMap,
localization: options?.localization,
stylePreprocessorOptions: options?.stylePreprocessorOptions,
resourcesOutputPath: options?.resourcesOutputPath,
deployUrl: options?.deployUrl,
i18nMissingTranslation: options?.i18nMissingTranslation,
preserveSymlinks: options?.preserveSymlinks,
extractLicenses: options?.extractLicenses,
};
};
const buildTarget = clientProject.targets.get('build');
if (buildTarget?.options) {
buildTarget.options.outputPath = `dist/${options.clientProject}/browser`;
}
const buildConfigurations = buildTarget?.configurations;
const configurations: Record<string, {}> = {};
if (buildConfigurations) {
for (const [key, options] of Object.entries(buildConfigurations)) {
configurations[key] = getServerOptions(options);
}
}
const mainPath = options.main as string;
const serverTsConfig = join(tsConfigDirectory, 'tsconfig.server.json');
clientProject.targets.add({
name: 'server',
builder: Builders.Server,
defaultConfiguration: 'production',
options: {
outputPath: `dist/${options.clientProject}/server`,
main: join(normalize(clientProject.root), 'src', mainPath.endsWith('.ts') ? mainPath : mainPath + '.ts'),
tsConfig: serverTsConfig,
...(buildTarget?.options ? getServerOptions(buildTarget?.options) : {}),
},
configurations: {
production: {
outputHashing,
fileReplacements,
sourceMap: false,
optimization: true,
},
},
configurations,
});
const lintTarget = clientProject.targets.get('lint');
if (lintTarget && lintTarget.options && Array.isArray(lintTarget.options.tsConfig)) {
lintTarget.options.tsConfig =
lintTarget.options.tsConfig.concat(serverTsConfig);
lintTarget.options.tsConfig = lintTarget.options.tsConfig.concat(serverTsConfig);
}
}
});

View File

@ -156,13 +156,9 @@ describe('Universal Schematic', () => {
expect(opts.main).toEqual('projects/bar/src/main.server.ts');
expect(opts.tsConfig).toEqual('projects/bar/tsconfig.server.json');
const configurations = targets.server.configurations;
expect(configurations.production).toBeDefined();
expect(configurations.production.fileReplacements).toBeDefined();
expect(configurations.production.outputHashing).toBe('media');
const fileReplacements = targets.server.configurations.production.fileReplacements;
expect(fileReplacements.length).toEqual(1);
expect(fileReplacements[0].replace).toEqual('projects/bar/src/environments/environment.ts');
expect(fileReplacements[0].with).toEqual('projects/bar/src/environments/environment.prod.ts');
expect(configurations.production.fileReplacements.length).toEqual(1);
expect(configurations.production.fileReplacements[0].replace).toEqual('projects/bar/src/environments/environment.ts');
expect(configurations.production.fileReplacements[0].with).toEqual('projects/bar/src/environments/environment.prod.ts');
});
it('should update workspace with a build target outputPath', async () => {

View File

@ -4,7 +4,8 @@
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build"<% if (!minimal) { %>,
"build": "ng build",
"watch": "ng build --watch --configuration development"<% if (!minimal) { %>,
"test": "ng test",
"lint": "ng lint"<% } %>
},