Alan Agius 9e13a8b0c8 fix(@angular/build): format sizes using decimal byte units consistently
Ensure that file sizes are consistently formatted using decimal byte units, adhering to the International System of Units (SI) convention. This aligns with clarity and standardization across the project.

- Kilobyte (kB): 10^3 bytes (1000 bytes)
- Megabyte (MB): 10^6 bytes (1,000,000 bytes)
- Gigabyte (GB): 10^9 bytes (1,000,000,000 bytes)

Closes: #27580
2024-05-06 16:03:25 +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.io/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: '2kB',
maximumError: '4kB',
},
];
} 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': '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': '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;
}