Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/design/analytics.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ PROJECT NAME TO BUILD OR A MODULE NAME.**
| SchematicCollectionName | `ep.ng_schematic_collection_name` | `string` |
| SchematicName | `ep.ng_schematic_name` | `string` |
| Standalone | `ep.ng_standalone` | `string` |
| SSR | `ep.ng_ssr` | `string` |
| Style | `ep.ng_style` | `string` |
| Routing | `ep.ng_routing` | `string` |
| InlineTemplate | `ep.ng_inline_template` | `string` |
Expand Down
1 change: 1 addition & 0 deletions packages/angular/cli/src/analytics/analytics-parameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export enum EventCustomDimension {
SchematicCollectionName = 'ep.ng_schematic_collection_name',
SchematicName = 'ep.ng_schematic_name',
Standalone = 'ep.ng_standalone',
SSR = 'ep.ng_ssr',
Style = 'ep.ng_style',
Routing = 'ep.ng_routing',
InlineTemplate = 'ep.ng_inline_template',
Expand Down
10 changes: 0 additions & 10 deletions packages/angular/ssr/schematics/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,8 @@ filegroup(
name = "schematics_assets",
srcs = glob(
[
"**/files/**/*",
"**/*.json",
],
exclude = [
# NB: we need to exclude the nested node_modules that is laid out by yarn workspaces
"node_modules/**",
],
),
)

Expand All @@ -66,11 +61,8 @@ ts_library(
],
data = [":schematics_assets"],
deps = [
"//packages/angular_devkit/core",
"//packages/angular_devkit/schematics",
"//packages/schematics/angular",
"@npm//@types/node",
"@npm//typescript",
],
)

Expand All @@ -89,8 +81,6 @@ ts_library(
# @external_begin
deps = [
":schematics",
"//packages/angular_devkit/core",
"//packages/angular_devkit/schematics",
"//packages/angular_devkit/schematics/testing",
],
# @external_end
Expand Down
2 changes: 1 addition & 1 deletion packages/angular/ssr/schematics/collection.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"schematics": {
"ng-add": {
"description": "Adds Angular SSR to the application without affecting any templates",
"factory": "./ng-add",
"factory": "./ng-add/index",
"schema": "./ng-add/schema.json"
}
}
Expand Down
250 changes: 3 additions & 247 deletions packages/angular/ssr/schematics/ng-add/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,251 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import { join, normalize, strings } from '@angular-devkit/core';
import {
Rule,
SchematicsException,
apply,
applyTemplates,
chain,
externalSchematic,
mergeWith,
move,
noop,
url,
} from '@angular-devkit/schematics';
import { Schema as ServerOptions } from '@schematics/angular/server/schema';
import { DependencyType, addDependency, updateWorkspace } from '@schematics/angular/utility';
import { JSONFile } from '@schematics/angular/utility/json-file';
import { isStandaloneApp } from '@schematics/angular/utility/ng-ast-utils';
import { targetBuildNotFoundError } from '@schematics/angular/utility/project-targets';
import { getMainFilePath } from '@schematics/angular/utility/standalone/util';
import { getWorkspace } from '@schematics/angular/utility/workspace';
import { Builders } from '@schematics/angular/utility/workspace-models';
import { externalSchematic } from '@angular-devkit/schematics';
import { Schema as SSROptions } from './schema';

import { latestVersions } from '../utility/latest-versions';
import { getOutputPath, getProject } from '../utility/utils';

import { Schema as AddServerOptions } from './schema';

const SERVE_SSR_TARGET_NAME = 'serve-ssr';
const PRERENDER_TARGET_NAME = 'prerender';

function addScriptsRule(options: AddServerOptions): Rule {
return async (host) => {
const pkgPath = '/package.json';
const buffer = host.read(pkgPath);
if (buffer === null) {
throw new SchematicsException('Could not find package.json');
}

const serverDist = await getOutputPath(host, options.project, 'server');
const pkg = JSON.parse(buffer.toString()) as { scripts?: Record<string, string> };
pkg.scripts = {
...pkg.scripts,
'dev:ssr': `ng run ${options.project}:${SERVE_SSR_TARGET_NAME}`,
'serve:ssr': `node ${serverDist}/main.js`,
'build:ssr': `ng build && ng run ${options.project}:server`,
'prerender': `ng run ${options.project}:${PRERENDER_TARGET_NAME}`,
};

host.overwrite(pkgPath, JSON.stringify(pkg, null, 2));
};
}

function updateApplicationBuilderTsConfigRule(options: AddServerOptions): Rule {
return async (host) => {
const project = await getProject(host, options.project);
const buildTarget = project.targets.get('build');
if (!buildTarget || !buildTarget.options) {
return;
}

const tsConfigPath = buildTarget.options.tsConfig;
if (!tsConfigPath || typeof tsConfigPath !== 'string') {
// No tsconfig path
return;
}

const tsConfig = new JSONFile(host, tsConfigPath);
const filesAstNode = tsConfig.get(['files']);
const serverFilePath = 'server.ts';
if (Array.isArray(filesAstNode) && !filesAstNode.some(({ text }) => text === serverFilePath)) {
tsConfig.modify(['files'], [...filesAstNode, serverFilePath]);
}
};
}

function updateApplicationBuilderWorkspaceConfigRule(
projectRoot: string,
options: AddServerOptions,
): Rule {
return updateWorkspace((workspace) => {
const buildTarget = workspace.projects.get(options.project)?.targets.get('build');
if (!buildTarget) {
return;
}

buildTarget.options = {
...buildTarget.options,
prerender: true,
ssr: join(normalize(projectRoot), 'server.ts'),
};
});
}

function updateWebpackBuilderWorkspaceConfigRule(options: AddServerOptions): Rule {
return updateWorkspace((workspace) => {
const projectName = options.project;
const project = workspace.projects.get(projectName);
if (!project) {
return;
}

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const serverTarget = project.targets.get('server')!;
(serverTarget.options ??= {}).main = join(normalize(project.root), 'server.ts');

const serveSSRTarget = project.targets.get(SERVE_SSR_TARGET_NAME);
if (serveSSRTarget) {
return;
}

project.targets.add({
name: SERVE_SSR_TARGET_NAME,
builder: '@angular-devkit/build-angular:ssr-dev-server',
defaultConfiguration: 'development',
options: {},
configurations: {
development: {
browserTarget: `${projectName}:build:development`,
serverTarget: `${projectName}:server:development`,
},
production: {
browserTarget: `${projectName}:build:production`,
serverTarget: `${projectName}:server:production`,
},
},
});

const prerenderTarget = project.targets.get(PRERENDER_TARGET_NAME);
if (prerenderTarget) {
return;
}

project.targets.add({
name: PRERENDER_TARGET_NAME,
builder: '@angular-devkit/build-angular:prerender',
defaultConfiguration: 'production',
options: {
routes: ['/'],
},
configurations: {
production: {
browserTarget: `${projectName}:build:production`,
serverTarget: `${projectName}:server:production`,
},
development: {
browserTarget: `${projectName}:build:development`,
serverTarget: `${projectName}:server:development`,
},
},
});
});
}

function updateWebpackBuilderServerTsConfigRule(options: AddServerOptions): Rule {
return async (host) => {
const project = await getProject(host, options.project);
const serverTarget = project.targets.get('server');
if (!serverTarget || !serverTarget.options) {
return;
}

const tsConfigPath = serverTarget.options.tsConfig;
if (!tsConfigPath || typeof tsConfigPath !== 'string') {
// No tsconfig path
return;
}

const tsConfig = new JSONFile(host, tsConfigPath);
const filesAstNode = tsConfig.get(['files']);
const serverFilePath = 'server.ts';
if (Array.isArray(filesAstNode) && !filesAstNode.some(({ text }) => text === serverFilePath)) {
tsConfig.modify(['files'], [...filesAstNode, serverFilePath]);
}
};
}

function addDependencies(): Rule {
return chain([
addDependency('express', latestVersions['express'], {
type: DependencyType.Default,
}),
addDependency('@types/express', latestVersions['@types/express'], {
type: DependencyType.Dev,
}),
]);
}

function addServerFile(options: ServerOptions, isStandalone: boolean): Rule {
return async (host) => {
const project = await getProject(host, options.project);
const browserDistDirectory = await getOutputPath(host, options.project, 'build');

return mergeWith(
apply(
url(
`./files/${
project?.targets?.get('build')?.builder === Builders.Application
? 'application-builder'
: 'server-builder'
}`,
),
[
applyTemplates({
...strings,
...options,
browserDistDirectory,
isStandalone,
}),
move(project.root),
],
),
);
};
}

export default function (options: AddServerOptions): Rule {
return async (host) => {
const browserEntryPoint = await getMainFilePath(host, options.project);
const isStandalone = isStandaloneApp(host, browserEntryPoint);

const workspace = await getWorkspace(host);
const clientProject = workspace.projects.get(options.project);
if (!clientProject) {
throw targetBuildNotFoundError();
}
const isUsingApplicationBuilder =
clientProject.targets.get('build')?.builder === Builders.Application;

return chain([
externalSchematic('@schematics/angular', 'server', {
...options,
skipInstall: true,
}),
...(isUsingApplicationBuilder
? [
updateApplicationBuilderWorkspaceConfigRule(clientProject.root, options),
updateApplicationBuilderTsConfigRule(options),
]
: [
addScriptsRule(options),
updateWebpackBuilderServerTsConfigRule(options),
updateWebpackBuilderWorkspaceConfigRule(options),
]),
addServerFile(options, isStandalone),
addDependencies(),
]);
};
}
export default (options: SSROptions) => externalSchematic('@schematics/angular', 'ssr', options);
Loading