Charles Lyding d223d1eda3 refactor(@schematics/angular): increase new application anyComponentStyle budget thresholds
The 2kB/4kB warning/error thresholds for any component style within an application
has now been increased to 4kB/8kB for warnings/errors respectively. This allows for
more complex styles within a component while also reducing the likelihood of the budget
rule being disabled or removed. The new limits still provide diagnostics for extreme
size cases such as accidentally importing all Bootstrap or Material styles into an
individual component. Such instances are a primary use case for the budget.
2024-08-21 20:47:31 +02:00

358 lines
9.9 KiB
TypeScript

/**
* @license
* Copyright Google LLC 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.dev/license
*/
import { JsonObject, join, normalize } from '@angular-devkit/core';
import {
MergeStrategy,
Rule,
SchematicContext,
Tree,
apply,
applyTemplates,
chain,
filter,
mergeWith,
move,
noop,
schematic,
strings,
url,
} from '@angular-devkit/schematics';
import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks';
import { Schema as ComponentOptions } from '../component/schema';
import { NodeDependencyType, addPackageJsonDependency } from '../utility/dependencies';
import { latestVersions } from '../utility/latest-versions';
import { relativePathToWorkspaceRoot } from '../utility/paths';
import { getWorkspace, updateWorkspace } from '../utility/workspace';
import { Builders, ProjectType } from '../utility/workspace-models';
import { Schema as ApplicationOptions, Style } from './schema';
export default function (options: ApplicationOptions): Rule {
return async (host: Tree, context: SchematicContext) => {
const { appDir, appRootSelector, componentOptions, folderName, sourceDir } =
await getAppOptions(host, options);
return chain([
addAppToWorkspaceFile(options, appDir, folderName),
options.standalone
? noop()
: schematic('module', {
name: 'app',
commonModule: false,
flat: true,
routing: options.routing,
routingScope: 'Root',
path: sourceDir,
project: options.name,
}),
schematic('component', {
name: 'app',
selector: appRootSelector,
flat: true,
path: sourceDir,
skipImport: true,
project: options.name,
...componentOptions,
}),
mergeWith(
apply(url(options.standalone ? './files/standalone-files' : './files/module-files'), [
options.routing ? noop() : filter((path) => !path.endsWith('app.routes.ts.template')),
componentOptions.skipTests
? filter((path) => !path.endsWith('.spec.ts.template'))
: noop(),
applyTemplates({
utils: strings,
...options,
...componentOptions,
selector: appRootSelector,
relativePathToWorkspaceRoot: relativePathToWorkspaceRoot(appDir),
appName: options.name,
folderName,
}),
move(appDir),
]),
MergeStrategy.Overwrite,
),
mergeWith(
apply(url('./files/common-files'), [
options.minimal
? filter((path) => !path.endsWith('tsconfig.spec.json.template'))
: noop(),
componentOptions.inlineTemplate
? filter((path) => !path.endsWith('component.html.template'))
: noop(),
applyTemplates({
utils: strings,
...options,
selector: appRootSelector,
relativePathToWorkspaceRoot: relativePathToWorkspaceRoot(appDir),
appName: options.name,
folderName,
}),
move(appDir),
]),
MergeStrategy.Overwrite,
),
options.ssr
? schematic('ssr', {
project: options.name,
skipInstall: true,
})
: noop(),
options.skipPackageJson ? noop() : addDependenciesToPackageJson(options),
]);
};
}
function addDependenciesToPackageJson(options: ApplicationOptions) {
return (host: Tree, context: SchematicContext) => {
[
{
type: NodeDependencyType.Dev,
name: '@angular/compiler-cli',
version: latestVersions.Angular,
},
{
type: NodeDependencyType.Dev,
name: '@angular-devkit/build-angular',
version: latestVersions.DevkitBuildAngular,
},
{
type: NodeDependencyType.Dev,
name: 'typescript',
version: latestVersions['typescript'],
},
].forEach((dependency) => addPackageJsonDependency(host, dependency));
if (!options.skipInstall) {
context.addTask(new NodePackageInstallTask());
}
return host;
};
}
function addAppToWorkspaceFile(
options: ApplicationOptions,
appDir: string,
folderName: string,
): Rule {
let projectRoot = appDir;
if (projectRoot) {
projectRoot += '/';
}
const schematics: JsonObject = {};
if (
options.inlineTemplate ||
options.inlineStyle ||
options.minimal ||
options.style !== Style.Css
) {
const componentSchematicsOptions: JsonObject = {};
if (options.inlineTemplate ?? options.minimal) {
componentSchematicsOptions.inlineTemplate = true;
}
if (options.inlineStyle ?? options.minimal) {
componentSchematicsOptions.inlineStyle = true;
}
if (options.style && options.style !== Style.Css) {
componentSchematicsOptions.style = options.style;
}
schematics['@schematics/angular:component'] = componentSchematicsOptions;
}
if (options.skipTests || options.minimal) {
const schematicsWithTests = [
'class',
'component',
'directive',
'guard',
'interceptor',
'pipe',
'resolver',
'service',
];
schematicsWithTests.forEach((type) => {
((schematics[`@schematics/angular:${type}`] ??= {}) as JsonObject).skipTests = true;
});
}
if (!options.standalone) {
const schematicsWithStandalone = ['component', 'directive', 'pipe'];
schematicsWithStandalone.forEach((type) => {
((schematics[`@schematics/angular:${type}`] ??= {}) as JsonObject).standalone = false;
});
}
const sourceRoot = join(normalize(projectRoot), 'src');
let budgets: { type: string; maximumWarning: string; maximumError: string }[] = [];
if (options.strict) {
budgets = [
{
type: 'initial',
maximumWarning: '500kB',
maximumError: '1MB',
},
{
type: 'anyComponentStyle',
maximumWarning: '4kB',
maximumError: '8kB',
},
];
} else {
budgets = [
{
type: 'initial',
maximumWarning: '2MB',
maximumError: '5MB',
},
{
type: 'anyComponentStyle',
maximumWarning: '6kB',
maximumError: '10kB',
},
];
}
const inlineStyleLanguage = options?.style !== Style.Css ? options.style : undefined;
const project = {
root: normalize(projectRoot),
sourceRoot,
projectType: ProjectType.Application,
prefix: options.prefix || 'app',
schematics,
targets: {
build: {
builder: Builders.Application,
defaultConfiguration: 'production',
options: {
outputPath: `dist/${folderName}`,
index: `${sourceRoot}/index.html`,
browser: `${sourceRoot}/main.ts`,
polyfills: ['zone.js'],
tsConfig: `${projectRoot}tsconfig.app.json`,
inlineStyleLanguage,
assets: [{ 'glob': '**/*', 'input': `${projectRoot}public` }],
styles: [`${sourceRoot}/styles.${options.style}`],
scripts: [],
},
configurations: {
production: {
budgets,
outputHashing: 'all',
},
development: {
optimization: false,
extractLicenses: false,
sourceMap: true,
},
},
},
serve: {
builder: Builders.DevServer,
defaultConfiguration: 'development',
options: {},
configurations: {
production: {
buildTarget: `${options.name}:build:production`,
},
development: {
buildTarget: `${options.name}:build:development`,
},
},
},
'extract-i18n': {
builder: Builders.ExtractI18n,
},
test: options.minimal
? undefined
: {
builder: Builders.Karma,
options: {
polyfills: ['zone.js', 'zone.js/testing'],
tsConfig: `${projectRoot}tsconfig.spec.json`,
inlineStyleLanguage,
assets: [{ 'glob': '**/*', 'input': `${projectRoot}public` }],
styles: [`${sourceRoot}/styles.${options.style}`],
scripts: [],
},
},
},
};
return updateWorkspace((workspace) => {
workspace.projects.add({
name: options.name,
...project,
});
});
}
async function getAppOptions(
host: Tree,
options: ApplicationOptions,
): Promise<{
appDir: string;
appRootSelector: string;
componentOptions: Partial<ComponentOptions>;
folderName: string;
sourceDir: string;
}> {
const appRootSelector = `${options.prefix}-root`;
const componentOptions = getComponentOptions(options);
const workspace = await getWorkspace(host);
const newProjectRoot = (workspace.extensions.newProjectRoot as string | undefined) || '';
// If scoped project (i.e. "@foo/bar"), convert dir to "foo/bar".
let folderName = options.name.startsWith('@') ? options.name.slice(1) : options.name;
if (/[A-Z]/.test(folderName)) {
folderName = strings.dasherize(folderName);
}
const appDir =
options.projectRoot === undefined
? join(normalize(newProjectRoot), folderName)
: normalize(options.projectRoot);
const sourceDir = `${appDir}/src/app`;
return {
appDir,
appRootSelector,
componentOptions,
folderName,
sourceDir,
};
}
function getComponentOptions(options: ApplicationOptions): Partial<ComponentOptions> {
const componentOptions: Partial<ComponentOptions> = !options.minimal
? {
inlineStyle: options.inlineStyle,
inlineTemplate: options.inlineTemplate,
skipTests: options.skipTests,
style: options.style,
viewEncapsulation: options.viewEncapsulation,
}
: {
inlineStyle: options.inlineStyle ?? true,
inlineTemplate: options.inlineTemplate ?? true,
skipTests: true,
style: options.style,
viewEncapsulation: options.viewEncapsulation,
};
return componentOptions;
}