mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-17 11:03:53 +08:00
This commit updates several schematics to make the new `@angular/ssr` feature opt-in. Users can opt in by using the `--server-routing` option or by responding with `yes` to the prompt.
359 lines
10 KiB
TypeScript
359 lines
10 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,
|
|
serverRouting: options.serverRouting,
|
|
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: options.experimentalZoneless ? [] : ['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: options.experimentalZoneless ? [] : ['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;
|
|
}
|