Browse Source

Merge pull request #24026 from abpframework/feat/#23427

Schematic to Enable SSR in Existing ABP Angular Projects
pull/24103/head
sumeyye 3 months ago
committed by GitHub
parent
commit
fca19fded2
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 7
      npm/ng-packs/packages/oauth/src/lib/services/server-token-storage.service.ts
  2. 3
      npm/ng-packs/packages/oauth/src/lib/tokens/cookies.ts
  3. 1
      npm/ng-packs/packages/oauth/src/lib/tokens/index.ts
  4. 12
      npm/ng-packs/packages/schematics/src/collection.json
  5. 192
      npm/ng-packs/packages/schematics/src/commands/ssr-add/files/application-builder/server.ts.template
  6. 220
      npm/ng-packs/packages/schematics/src/commands/ssr-add/files/server-builder/server.ts.template
  7. 472
      npm/ng-packs/packages/schematics/src/commands/ssr-add/index.ts
  8. 23
      npm/ng-packs/packages/schematics/src/commands/ssr-add/schema.json
  9. 23
      npm/ng-packs/packages/schematics/src/commands/ssr-add/server/files/application-builder/ngmodule-src/app/app.module.server.ts.template
  10. 8
      npm/ng-packs/packages/schematics/src/commands/ssr-add/server/files/application-builder/ngmodule-src/app/app.routes.server.ts.template
  11. 1
      npm/ng-packs/packages/schematics/src/commands/ssr-add/server/files/application-builder/ngmodule-src/main.server.ts.template
  12. 29
      npm/ng-packs/packages/schematics/src/commands/ssr-add/server/files/application-builder/standalone-src/app/app.config.server.ts.template
  13. 8
      npm/ng-packs/packages/schematics/src/commands/ssr-add/server/files/application-builder/standalone-src/app/app.routes.server.ts.template
  14. 7
      npm/ng-packs/packages/schematics/src/commands/ssr-add/server/files/application-builder/standalone-src/main.server.ts.template
  15. 25
      npm/ng-packs/packages/schematics/src/commands/ssr-add/server/files/server-builder/ngmodule-src/app/app.module.server.ts.template
  16. 1
      npm/ng-packs/packages/schematics/src/commands/ssr-add/server/files/server-builder/ngmodule-src/main.server.ts.template
  17. 15
      npm/ng-packs/packages/schematics/src/commands/ssr-add/server/files/server-builder/root/tsconfig.server.json.template
  18. 29
      npm/ng-packs/packages/schematics/src/commands/ssr-add/server/files/server-builder/standalone-src/app/app.config.server.ts.template
  19. 7
      npm/ng-packs/packages/schematics/src/commands/ssr-add/server/files/server-builder/standalone-src/main.server.ts.template
  20. 268
      npm/ng-packs/packages/schematics/src/commands/ssr-add/server/index.ts
  21. 23
      npm/ng-packs/packages/schematics/src/commands/ssr-add/server/schema.json
  22. 1
      npm/ng-packs/packages/schematics/src/utils/angular/index.ts
  23. 28
      npm/ng-packs/packages/schematics/src/utils/angular/latest-versions/index.ts
  24. 2
      npm/ng-packs/packages/schematics/src/utils/angular/workspace.ts
  25. 3
      npm/ng-packs/scripts/build-schematics.ts

7
npm/ng-packs/packages/oauth/src/lib/services/server-token-storage.service.ts

@ -1,13 +1,16 @@
import { Inject, Injectable, Optional } from '@angular/core';
import { inject, Inject, Injectable, Optional } from '@angular/core';
import { OAuthStorage } from 'angular-oauth2-oidc';
import { REQUEST } from '@angular/core';
import { COOKIES } from '../tokens';
@Injectable({ providedIn: null })
export class ServerTokenStorageService implements OAuthStorage {
private cookies = new Map<string, string>();
// For server builders where REQUEST injection is not possible, cookies can be provided via COOKIES token
private cookiesStr = inject<string>(COOKIES, { optional: true });
constructor(@Optional() @Inject(REQUEST) private req: Request | null) {
const cookieHeader = this.req?.headers.get('cookie') ?? '';
const cookieHeader = this.req?.headers.get('cookie') ?? this.cookiesStr ?? '';
for (const part of cookieHeader.split(';')) {
const i = part.indexOf('=');
if (i > -1) {

3
npm/ng-packs/packages/oauth/src/lib/tokens/cookies.ts

@ -0,0 +1,3 @@
import { InjectionToken } from '@angular/core';
export const COOKIES = new InjectionToken<string>('COOKIES');

1
npm/ng-packs/packages/oauth/src/lib/tokens/index.ts

@ -1 +1,2 @@
export * from './auth-flow-strategy';
export * from './cookies';

12
npm/ng-packs/packages/schematics/src/collection.json

@ -34,7 +34,17 @@
"description": "ABP Change Styles of Theme Schematics",
"factory": "./commands/change-theme",
"schema": "./commands/change-theme/schema.json"
},
"server": {
"factory": "./commands/ssr-add/server",
"description": "Create an Angular server app.",
"schema": "./commands/ssr-add/server/schema.json",
"hidden": true
},
"ssr-add": {
"description": "ABP SSR Add Schematics",
"factory": "./commands/ssr-add",
"schema": "./commands/ssr-add/schema.json"
}
}
}

192
npm/ng-packs/packages/schematics/src/commands/ssr-add/files/application-builder/server.ts.template

@ -0,0 +1,192 @@
import {
AngularNodeAppEngine,
createNodeRequestHandler,
isMainModule,
writeResponseToNodeResponse,
} from '@angular/ssr/node';
import express from 'express';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import {environment} from './environments/environment';
import { ServerCookieParser } from '@abp/ng.core';
// ESM import
import * as oidc from 'openid-client';
if (environment.production === false) {
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0";
}
const serverDistFolder = dirname(fileURLToPath(import.meta.url));
const browserDistFolder = resolve(serverDistFolder, '../browser');
const app = express();
const angularApp = new AngularNodeAppEngine();
const ISSUER = new URL(environment.oAuthConfig.issuer);
const CLIENT_ID = environment.oAuthConfig.clientId;
const REDIRECT_URI = environment.oAuthConfig.redirectUri;
const SCOPE = environment.oAuthConfig.scope;
// @ts-ignore
const CLIENT_SECRET = environment.oAuthConfig.clientSecret || undefined;
const config = await oidc.discovery(ISSUER, CLIENT_ID, CLIENT_SECRET);
const secureCookie = { httpOnly: true, sameSite: 'lax' as const, secure: environment.production, path: '/' };
const tokenCookie = { ...secureCookie, httpOnly: false };
app.use(ServerCookieParser.middleware());
const sessions = new Map<string, { pkce?: string; state?: string; refresh?: string; at?: string, returnUrl?: string }>();
app.get('/authorize', async (_req, res) => {
const code_verifier = oidc.randomPKCECodeVerifier();
const code_challenge = await oidc.calculatePKCECodeChallenge(code_verifier);
const state = oidc.randomState();
if (_req.query.returnUrl) {
const returnUrl = String(_req.query.returnUrl || null);
res.cookie('returnUrl', returnUrl, { ...secureCookie, maxAge: 5 * 60 * 1000 });
}
const sid = crypto.randomUUID();
sessions.set(sid, { pkce: code_verifier, state });
res.cookie('sid', sid, secureCookie);
const url = oidc.buildAuthorizationUrl(config, {
redirect_uri: REDIRECT_URI,
scope: SCOPE,
code_challenge,
code_challenge_method: 'S256',
state,
});
res.redirect(url.toString());
});
app.get('/logout', async (req, res) => {
try {
const sid = req.cookies.sid;
if (sid && sessions.has(sid)) {
sessions.delete(sid);
}
res.clearCookie('sid', secureCookie);
res.clearCookie('access_token', tokenCookie);
res.clearCookie('refresh_token', secureCookie);
res.clearCookie('expires_at', tokenCookie);
res.clearCookie('returnUrl', secureCookie);
const endSessionEndpoint = config.serverMetadata().end_session_endpoint;
if (endSessionEndpoint) {
const logoutUrl = new URL(endSessionEndpoint);
logoutUrl.searchParams.set('post_logout_redirect_uri', REDIRECT_URI);
logoutUrl.searchParams.set('client_id', CLIENT_ID);
return res.redirect(logoutUrl.toString());
}
res.redirect('/');
} catch (error) {
console.error('Logout error:', error);
res.status(500).send('Logout error');
}
});
app.get('/', async (req, res, next) => {
try {
const { code, state } = req.query as any;
if (!code || !state) return next();
const sid = req.cookies.sid;
const sess = sid && sessions.get(sid);
if (!sess || state !== sess.state) return res.status(400).send('invalid state');
const tokenEndpoint = config.serverMetadata().token_endpoint!;
const body = new URLSearchParams({
grant_type: 'authorization_code',
code: String(code),
redirect_uri: environment.oAuthConfig.redirectUri,
code_verifier: sess.pkce!,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET || ''
});
const resp = await fetch(tokenEndpoint, {
method: 'POST',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
body,
});
if (!resp.ok) {
const errTxt = await resp.text();
console.error('token error:', resp.status, errTxt);
return res.status(500).send('token error');
}
const tokens = await resp.json();
const expiresInSec =
Number(tokens.expires_in ?? tokens.expiresIn ?? 3600);
const skewSec = 60;
const accessExpiresAt = new Date(
Date.now() + Math.max(0, expiresInSec - skewSec) * 1000
);
sessions.set(sid, { ...sess, at: tokens.access_token, refresh: tokens.refresh_token });
res.cookie('access_token', tokens.access_token, {...tokenCookie, maxAge: accessExpiresAt.getTime()});
res.cookie('refresh_token', tokens.refresh_token, secureCookie);
res.cookie('expires_at', String(accessExpiresAt.getTime()), tokenCookie);
const returnUrl = req.cookies?.returnUrl ?? '/';
res.clearCookie('returnUrl', secureCookie);
return res.redirect(returnUrl);
} catch (e) {
console.error('OIDC error:', e);
return res.status(500).send('oidc error');
}
});
/**
* Serve static files from /browser
*/
app.use(
express.static(browserDistFolder, {
maxAge: '1y',
index: false,
redirect: false,
}),
);
/**
* Handle all other requests by rendering the Angular application.
*/
app.use((req, res, next) => {
angularApp
.handle(req)
.then(response => {
if (response) {
res.cookie('ssr-init', 'true', {...secureCookie, httpOnly: false});
return writeResponseToNodeResponse(response, res);
} else {
return next()
}
})
.catch(next);
});
/**
* Start the server if this module is the main entry point.
* The server listens on the port defined by the `PORT` environment variable, or defaults to 4000.
*/
if (isMainModule(import.meta.url)) {
const port = process.env['PORT'] || 4200;
app.listen(port, () => {
console.log(`Node Express server listening on http://localhost:${port}`);
});
}
/**
* Request handler used by the Angular CLI (for dev-server and during build) or Firebase Cloud Functions.
*/
export const reqHandler = createNodeRequestHandler(app);

220
npm/ng-packs/packages/schematics/src/commands/ssr-add/files/server-builder/server.ts.template

@ -0,0 +1,220 @@
import 'zone.js/node';
import { APP_BASE_HREF } from '@angular/common';
import { CommonEngine } from '@angular/ssr/node';
import * as express from 'express';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import <% if (isStandalone) { %>bootstrap<% } else { %>AppServerModule<% } %> from './main.server';
import {environment} from './environments/environment';
import * as oidc from 'openid-client';
import { ServerCookieParser } from '@abp/ng.core';
import {COOKIES} from "@abp/ng.oauth";
if (environment.production === false) {
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0";
}
const ISSUER = new URL(environment.oAuthConfig.issuer);
const CLIENT_ID = environment.oAuthConfig.clientId;
const REDIRECT_URI = environment.oAuthConfig.redirectUri;
const SCOPE = environment.oAuthConfig.scope;
// @ts-ignore
const CLIENT_SECRET = environment.oAuthConfig.clientSecret || undefined;
let config: Awaited<ReturnType<typeof oidc.discovery>>;
const secureCookie = { httpOnly: true, sameSite: 'lax' as const, secure: environment.production, path: '/' };
const tokenCookie = { ...secureCookie, httpOnly: false };
async function initializeOIDC() {
config = await oidc.discovery(ISSUER, CLIENT_ID, CLIENT_SECRET);
}
// The Express app is exported so that it can be used by serverless Functions.
export function app(): express.Express {
const server = express();
const distFolder = join(process.cwd(), '<%= browserDistDirectory %>');
const indexHtml = existsSync(join(distFolder, 'index.original.html'))
? join(distFolder, 'index.original.html')
: join(distFolder, 'index.html');
const commonEngine = new CommonEngine();
server.set('view engine', 'html');
server.set('views', distFolder);
server.use(ServerCookieParser.middleware());
const sessions = new Map<string, { pkce?: string; state?: string; refresh?: string; at?: string, returnUrl?: string }>();
server.get('/authorize', async (_req, res) => {
const code_verifier = oidc.randomPKCECodeVerifier();
const code_challenge = await oidc.calculatePKCECodeChallenge(code_verifier);
const state = oidc.randomState();
if (_req.query.returnUrl) {
const returnUrl = String(_req.query.returnUrl || null);
res.cookie('returnUrl', returnUrl, { ...secureCookie, maxAge: 5 * 60 * 1000 });
}
const sid = crypto.randomUUID();
sessions.set(sid, { pkce: code_verifier, state });
res.cookie('sid', sid, secureCookie);
const url = oidc.buildAuthorizationUrl(config, {
redirect_uri: REDIRECT_URI,
scope: SCOPE,
code_challenge,
code_challenge_method: 'S256',
state,
});
res.redirect(url.toString());
});
server.get('/logout', async (req, res) => {
try {
const sid = req.cookies.sid;
if (sid && sessions.has(sid)) {
sessions.delete(sid);
}
res.clearCookie('sid', secureCookie);
res.clearCookie('access_token', tokenCookie);
res.clearCookie('refresh_token', secureCookie);
res.clearCookie('expires_at', tokenCookie);
res.clearCookie('returnUrl', secureCookie);
if (!config) {
return res.redirect('/');
}
const endSessionEndpoint = config.serverMetadata().end_session_endpoint;
if (endSessionEndpoint) {
const logoutUrl = new URL(endSessionEndpoint);
logoutUrl.searchParams.set('post_logout_redirect_uri', REDIRECT_URI);
logoutUrl.searchParams.set('client_id', CLIENT_ID);
return res.redirect(logoutUrl.toString());
}
res.redirect('/');
} catch (error) {
console.error('Logout error:', error);
res.status(500).send('Logout error');
}
});
server.get('/', async (req, res, next) => {
try {
const { code, state } = req.query as any;
if (!code || !state) return next();
const sid = req.cookies.sid;
const sess = sid && sessions.get(sid);
if (!sess || state !== sess.state) return res.status(400).send('invalid state');
const tokenEndpoint = config.serverMetadata().token_endpoint!;
const body = new URLSearchParams({
grant_type: 'authorization_code',
code: String(code),
redirect_uri: environment.oAuthConfig.redirectUri,
code_verifier: sess.pkce!,
client_id: CLIENT_ID
});
const resp = await fetch(tokenEndpoint, {
method: 'POST',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
body,
});
if (!resp.ok) {
const errTxt = await resp.text();
console.error('token error:', resp.status, errTxt);
return res.status(500).send('token error');
}
const tokens = await resp.json();
const expiresInSec =
Number(tokens.expires_in ?? tokens.expiresIn ?? 3600);
const skewSec = 60;
const accessExpiresAt = new Date(
Date.now() + Math.max(0, expiresInSec - skewSec) * 1000
);
sessions.set(sid, { ...sess, at: tokens.access_token, refresh: tokens.refresh_token });
res.cookie('access_token', tokens.access_token, {...tokenCookie, maxAge: accessExpiresAt.getTime()});
res.cookie('refresh_token', tokens.refresh_token, secureCookie);
res.cookie('expires_at', String(accessExpiresAt.getTime()), tokenCookie);
const returnUrl = req.cookies?.returnUrl ?? '/';
res.clearCookie('returnUrl', secureCookie);
return res.redirect(returnUrl);
} catch (e) {
console.error('OIDC error:', e);
return res.status(500).send('oidc error');
}
});
// Example Express Rest API endpoints
// server.get('/api/{*splat}', (req, res) => { });
// Serve static files from /browser
server.use(express.static(distFolder, {
maxAge: '1y',
index: false,
}));
// All regular routes use the Angular engine
server.use((req, res, next) => {
const { protocol, originalUrl, baseUrl, headers } = req;
commonEngine
.render({
<% if (isStandalone) { %>bootstrap<% } else { %>bootstrap: AppServerModule<% } %>,
documentFilePath: indexHtml,
url: `${protocol}://${headers.host}${originalUrl}`,
publicPath: distFolder,
providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }, { provide: COOKIES, useValue: JSON.stringify(req.headers.cookie) }],
})
.then((html) => {
res.cookie('ssr-init', 'true', {...secureCookie, httpOnly: false});
return res.send(html)
})
.catch((err) => next(err));
});
return server;
}
async function run(): Promise<void> {
const port = process.env['PORT'] || 4000;
console.log('🔐 Initializing OIDC configuration...');
await initializeOIDC();
console.log('✅ OIDC configuration loaded');
// Start up the Node server
const server = app();
server.listen(port, (error) => {
if (error) {
throw error;
}
console.log(`Node Express server listening on http://localhost:${port}`);
});
}
// Webpack will replace 'require' with '__webpack_require__'
// '__non_webpack_require__' is a proxy to Node 'require'
// The below code is to ensure that the server is run only when not requiring the bundle.
declare const __non_webpack_require__: NodeRequire;
const mainModule = __non_webpack_require__.main;
const moduleFilename = mainModule && mainModule.filename || '';
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
run();
}
export default <% if (isStandalone) { %>bootstrap<% } else { %>AppServerModule<% } %>;

472
npm/ng-packs/packages/schematics/src/commands/ssr-add/index.ts

@ -0,0 +1,472 @@
/**
* @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 { isJsonObject, join, normalize, strings } from '@angular-devkit/core';
import {
Rule,
SchematicContext,
SchematicsException,
Tree,
apply,
applyTemplates,
chain,
mergeWith,
move,
schematic,
url,
} from '@angular-devkit/schematics';
import { posix } from 'node:path';
// @ts-ignore
import { Schema as ServerOptions } from './server/schema';
import {
DependencyType,
ExistingBehavior,
InstallBehavior,
addDependency,
readWorkspace,
updateWorkspace,
} from '../../utils/angular';
import { JSONFile } from '../../utils/angular/json-file';
import { latestVersions } from '../../utils/angular/latest-versions';
import { isStandaloneApp } from '../../utils/angular/ng-ast-utils';
import {
isUsingApplicationBuilder,
targetBuildNotFoundError,
} from '../../utils/angular/project-targets';
import { getMainFilePath } from '../../utils/angular/standalone/util';
import { getWorkspace } from '../../utils/angular/workspace';
// @ts-ignore
import { Schema as SSROptions } from './schema';
const SERVE_SSR_TARGET_NAME = 'serve-ssr';
const PRERENDER_TARGET_NAME = 'prerender';
const DEFAULT_BROWSER_DIR = 'browser';
const DEFAULT_MEDIA_DIR = 'media';
const DEFAULT_SERVER_DIR = 'server';
async function getLegacyOutputPaths(
host: Tree,
projectName: string,
target: 'server' | 'build',
): Promise<string> {
// Generate new output paths
const workspace = await readWorkspace(host);
const project = workspace.projects.get(projectName);
const architectTarget = project?.targets.get(target);
if (!architectTarget?.options) {
throw new SchematicsException(`Cannot find 'options' for ${projectName} ${target} target.`);
}
const { outputPath } = architectTarget.options;
if (typeof outputPath !== 'string') {
throw new SchematicsException(
`outputPath for ${projectName} ${target} target is not a string.`,
);
}
return outputPath;
}
async function getApplicationBuilderOutputPaths(
host: Tree,
projectName: string,
): Promise<{ browser: string; server: string; base: string }> {
// Generate new output paths
const target = 'build';
const workspace = await readWorkspace(host);
const project = workspace.projects.get(projectName);
const architectTarget = project?.targets.get(target);
if (!architectTarget?.options) {
throw new SchematicsException(`Cannot find 'options' for ${projectName} ${target} target.`);
}
let { outputPath } = architectTarget.options;
// Use default if not explicitly specified
outputPath ??= posix.join('dist', projectName);
const defaultDirs = {
server: DEFAULT_SERVER_DIR,
browser: DEFAULT_BROWSER_DIR,
};
if (outputPath && isJsonObject(outputPath)) {
return {
...defaultDirs,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
...(outputPath as any),
};
}
if (typeof outputPath !== 'string') {
throw new SchematicsException(
`outputPath for ${projectName} ${target} target is not a string.`,
);
}
return {
base: outputPath,
...defaultDirs,
};
}
function addScriptsRule({ project }: SSROptions, isUsingApplicationBuilder: boolean): Rule {
return async host => {
const pkgPath = '/package.json';
const pkg = host.readJson(pkgPath) as { scripts?: Record<string, string> } | null;
if (pkg === null) {
throw new SchematicsException('Could not find package.json');
}
if (isUsingApplicationBuilder) {
const { base, server } = await getApplicationBuilderOutputPaths(host, project);
pkg.scripts ??= {};
pkg.scripts[`serve:ssr:${project}`] = `node ${posix.join(base, server)}/server.mjs`;
} else {
const serverDist = await getLegacyOutputPaths(host, project, 'server');
pkg.scripts = {
...pkg.scripts,
'dev:ssr': `ng run ${project}:${SERVE_SSR_TARGET_NAME}`,
'serve:ssr': `node ${serverDist}/main.js`,
'build:ssr': `ng build && ng run ${project}:server`,
prerender: `ng run ${project}:${PRERENDER_TARGET_NAME}`,
};
}
host.overwrite(pkgPath, JSON.stringify(pkg, null, 2));
};
}
function updateApplicationBuilderTsConfigRule(options: SSROptions): Rule {
return async host => {
const workspace = await readWorkspace(host);
const project = workspace.projects.get(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 json = new JSONFile(host, tsConfigPath);
const include = json.get(['include']);
if (Array.isArray(include) && include.includes('src/**/*.ts')) {
return;
}
const filesPath = ['files'];
const files = new Set((json.get(filesPath) as string[] | undefined) ?? []);
files.add('src/server.ts');
json.modify(filesPath, [...files]);
};
}
function updateRootTsConfigRule(options: SSROptions): Rule {
return async (host: Tree) => {
const workspace = await readWorkspace(host);
const project = workspace.projects.get(options.project);
let tsConfigPath: string | undefined = options.tsconfigPath;
if (!tsConfigPath && project) {
const projRoot = normalize(project.root || '');
const candidate = projRoot ? join(projRoot, 'tsconfig.json') : 'tsconfig.json';
if (host.exists(candidate)) {
tsConfigPath = candidate;
}
}
if (!tsConfigPath && host.exists('tsconfig.json')) {
tsConfigPath = 'tsconfig.json';
}
if (!tsConfigPath || !host.exists(tsConfigPath)) {
return;
}
const json = new JSONFile(host, tsConfigPath);
const moduleResolutionPath = ['compilerOptions', 'moduleResolution'];
const modulePath = ['compilerOptions', 'module'];
const currentModuleResolution = json.get(moduleResolutionPath);
if (currentModuleResolution !== 'bundler') {
json.modify(moduleResolutionPath, 'bundler');
}
const currentModule = json.get(modulePath);
if (currentModule !== 'preserve') {
json.modify(modulePath, 'preserve');
}
};
}
export function updateIndexHtml(options: SSROptions): Rule {
return async (host: Tree) => {
const workspace = await getWorkspace(host);
const project = workspace.projects.get(options.project);
if (!project) {
return;
}
const buildOptions = project.targets.get('build')?.options;
const indexPath = buildOptions?.index as string;
if (!indexPath || !host.exists(indexPath)) {
return;
}
const buffer = host.read(indexPath);
if (!buffer) return;
const content = buffer.toString('utf-8');
const loaderDiv = `<div id="lp-page-loader"></div>`;
let updatedContent = content.replace(loaderDiv, '');
host.overwrite(indexPath, updatedContent);
};
}
function updateApplicationBuilderWorkspaceConfigRule(
projectSourceRoot: string,
options: SSROptions,
{ logger }: SchematicContext,
): Rule {
return updateWorkspace(workspace => {
const buildTarget = workspace.projects.get(options.project)?.targets.get('build');
if (!buildTarget) {
return;
}
let outputPath = buildTarget.options?.outputPath;
if (outputPath && isJsonObject(outputPath)) {
if (outputPath.browser === '') {
const base = outputPath.base as string;
logger.warn(
`The output location of the browser build has been updated from "${base}" to "${posix.join(
base,
DEFAULT_BROWSER_DIR,
)}".
You might need to adjust your deployment pipeline.`,
);
if (
(outputPath.media && outputPath.media !== DEFAULT_MEDIA_DIR) ||
(outputPath.server && outputPath.server !== DEFAULT_SERVER_DIR)
) {
delete outputPath.browser;
} else {
outputPath = outputPath.base;
}
}
}
buildTarget.options = {
...buildTarget.options,
outputPath,
outputMode: 'server',
ssr: {
entry: join(normalize(projectSourceRoot), 'server.ts'),
},
};
});
}
function updateWebpackBuilderWorkspaceConfigRule(
projectSourceRoot: string,
options: SSROptions,
): 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 = posix.join(projectSourceRoot, '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: SSROptions): Rule {
return async host => {
const workspace = await readWorkspace(host);
const project = workspace.projects.get(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 = 'src/server.ts';
if (Array.isArray(filesAstNode) && !filesAstNode.some(({ text }) => text === serverFilePath)) {
tsConfig.modify(['files'], [...filesAstNode, serverFilePath]);
}
};
}
function addDependencies({ skipInstall }: SSROptions, isUsingApplicationBuilder: boolean): Rule {
const install = skipInstall ? InstallBehavior.None : InstallBehavior.Auto;
const rules: Rule[] = [
addDependency('express', latestVersions.dependencies.express, {
type: DependencyType.Default,
install,
existing: ExistingBehavior.Replace,
}),
addDependency('@types/express', latestVersions.dependencies['@types/express'], {
type: DependencyType.Dev,
install,
existing: ExistingBehavior.Replace,
}),
addDependency('openid-client', latestVersions.dependencies['openid-client'], {
type: DependencyType.Default,
install,
existing: ExistingBehavior.Skip,
}),
];
if (!isUsingApplicationBuilder) {
rules.push(
addDependency('browser-sync', latestVersions.dependencies['browser-sync'], {
type: DependencyType.Dev,
install,
}),
);
}
return chain(rules);
}
function addServerFile(
projectSourceRoot: string,
options: ServerOptions,
isStandalone: boolean,
): Rule {
return async host => {
const projectName = options.project;
const workspace = await readWorkspace(host);
const project = workspace.projects.get(projectName);
if (!project) {
throw new SchematicsException(`Invalid project name (${projectName})`);
}
const usingApplicationBuilder = isUsingApplicationBuilder(project);
const browserDistDirectory = usingApplicationBuilder
? (await getApplicationBuilderOutputPaths(host, projectName)).browser
: await getLegacyOutputPaths(host, projectName, 'build');
return mergeWith(
apply(url(`./files/${usingApplicationBuilder ? 'application-builder' : 'server-builder'}`), [
applyTemplates({
...strings,
...options,
browserDistDirectory,
isStandalone,
}),
move(projectSourceRoot),
]),
);
};
}
export default function (options: SSROptions): Rule {
return async (host, context) => {
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 usingApplicationBuilder = isUsingApplicationBuilder(clientProject);
const sourceRoot = clientProject.sourceRoot ?? posix.join(clientProject.root, 'src');
return chain([
schematic('server', {
...options,
skipInstall: true,
}),
...(usingApplicationBuilder
? [
updateApplicationBuilderWorkspaceConfigRule(sourceRoot, options, context),
updateApplicationBuilderTsConfigRule(options),
updateRootTsConfigRule(options)
]
: [
updateWebpackBuilderServerTsConfigRule(options),
updateWebpackBuilderWorkspaceConfigRule(sourceRoot, options),
]),
addServerFile(sourceRoot, options, isStandalone),
addScriptsRule(options, usingApplicationBuilder),
addDependencies(options, usingApplicationBuilder),
updateIndexHtml(options),
]);
};
}

23
npm/ng-packs/packages/schematics/src/commands/ssr-add/schema.json

@ -0,0 +1,23 @@
{
"$schema": "http://json-schema.org/schema",
"$id": "SchematicsAngularSSR",
"title": "ABP Angular SSR Options Schema",
"type": "object",
"description": "Enables Server-Side Rendering (SSR) for your Angular application. SSR allows your app to be rendered on the server, which can significantly improve its initial load performance and Search Engine Optimization (SEO). This schematic configures your project for SSR, generating the necessary files and making the required modifications to your project's structure.",
"properties": {
"project": {
"type": "string",
"description": "The name of the project you want to enable SSR for.",
"$default": {
"$source": "projectName"
}
},
"skipInstall": {
"description": "Skip the automatic installation of packages. You will need to manually install the dependencies later.",
"type": "boolean",
"default": false
}
},
"required": ["project"],
"additionalProperties": false
}

23
npm/ng-packs/packages/schematics/src/commands/ssr-add/server/files/application-builder/ngmodule-src/app/app.module.server.ts.template

@ -0,0 +1,23 @@
import { NgModule, inject, PLATFORM_ID, TransferState } from '@angular/core';
import { provideServerRendering, withRoutes } from '@angular/ssr';
import { <%= appComponentName %> } from '<%= appComponentPath %>';
import { <%= appModuleName %> } from '<%= appModulePath %>';
import { serverRoutes } from './app.routes.server';
import { SSR_FLAG } from '@abp/ng.core';
@NgModule({
imports: [<%= appModuleName %>],
providers: [{
provide: APP_INITIALIZER,
useFactory: () => {
const platformId = inject(PLATFORM_ID);
const transferState = inject<TransferState>(TransferState);
if (isPlatformServer(platformId)) {
transferState.set(SSR_FLAG, true);
}
},
multi: true
}, provideServerRendering(withRoutes(serverRoutes))],
bootstrap: [<%= appComponentName %>],
})
export class AppServerModule {}

8
npm/ng-packs/packages/schematics/src/commands/ssr-add/server/files/application-builder/ngmodule-src/app/app.routes.server.ts.template

@ -0,0 +1,8 @@
import { RenderMode, ServerRoute } from '@angular/ssr';
export const serverRoutes: ServerRoute[] = [
{
path: '**',
renderMode: RenderMode.Server
}
];

1
npm/ng-packs/packages/schematics/src/commands/ssr-add/server/files/application-builder/ngmodule-src/main.server.ts.template

@ -0,0 +1 @@
export { AppServerModule as default } from './app/app.module.server';

29
npm/ng-packs/packages/schematics/src/commands/ssr-add/server/files/application-builder/standalone-src/app/app.config.server.ts.template

@ -0,0 +1,29 @@
import {
mergeApplicationConfig,
ApplicationConfig,
provideAppInitializer,
inject,
PLATFORM_ID,
TransferState
} from '@angular/core';
import { isPlatformServer } from '@angular/common';
import { provideServerRendering, withRoutes } from '@angular/ssr';
import { appConfig } from './app.config';
import { serverRoutes } from './app.routes.server';
import { SSR_FLAG } from '@abp/ng.core';
const serverConfig: ApplicationConfig = {
providers: [
provideAppInitializer(() => {
const platformId = inject(PLATFORM_ID);
const transferState = inject<TransferState>(TransferState);
if (isPlatformServer(platformId)) {
transferState.set(SSR_FLAG, true);
}
}),
provideServerRendering(withRoutes(serverRoutes)),
],
};
export const config = mergeApplicationConfig(appConfig, serverConfig);

8
npm/ng-packs/packages/schematics/src/commands/ssr-add/server/files/application-builder/standalone-src/app/app.routes.server.ts.template

@ -0,0 +1,8 @@
import { RenderMode, ServerRoute } from '@angular/ssr';
export const serverRoutes: ServerRoute[] = [
{
path: '**',
renderMode: RenderMode.Server
}
];

7
npm/ng-packs/packages/schematics/src/commands/ssr-add/server/files/application-builder/standalone-src/main.server.ts.template

@ -0,0 +1,7 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { <%= appComponentName %> } from '<%= appComponentPath %>';
import { config } from './app/app.config.server';
const bootstrap = () => bootstrapApplication(<%= appComponentName %>, config);
export default bootstrap;

25
npm/ng-packs/packages/schematics/src/commands/ssr-add/server/files/server-builder/ngmodule-src/app/app.module.server.ts.template

@ -0,0 +1,25 @@
import {NgModule, inject, PLATFORM_ID, TransferState, provideAppInitializer} from '@angular/core';
import { ServerModule } from '@angular/platform-server';
import {isPlatformServer} from "@angular/common";
import { <%= appModuleName %> } from '<%= appModulePath %>';
import { <%= appComponentName %> } from '<%= appComponentPath %>';
import { SSR_FLAG } from '@abp/ng.core';
@NgModule({
imports: [
<%= appModuleName %>,
ServerModule,
],
providers: [
provideAppInitializer(() => {
const platformId = inject(PLATFORM_ID);
const transferState = inject<TransferState>(TransferState);
if (isPlatformServer(platformId)) {
transferState.set(SSR_FLAG, true);
}
}),
],
bootstrap: [<%= appComponentName %>],
})
export class AppServerModule {}

1
npm/ng-packs/packages/schematics/src/commands/ssr-add/server/files/server-builder/ngmodule-src/main.server.ts.template

@ -0,0 +1 @@
export { AppServerModule as default } from './app/app.module.server';

15
npm/ng-packs/packages/schematics/src/commands/ssr-add/server/files/server-builder/root/tsconfig.server.json.template

@ -0,0 +1,15 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "./<%= tsConfigExtends %>",
"compilerOptions": {
"outDir": "<%= relativePathToWorkspaceRoot %>/out-tsc/server",
"types": [
"node"<% if (hasLocalizePackage) { %>,
"@angular/localize"<% } %>
]
},
"files": [
"src/main.server.ts"
]
}

29
npm/ng-packs/packages/schematics/src/commands/ssr-add/server/files/server-builder/standalone-src/app/app.config.server.ts.template

@ -0,0 +1,29 @@
import {
mergeApplicationConfig,
ApplicationConfig,
provideAppInitializer,
inject,
PLATFORM_ID,
TransferState
} from '@angular/core';
import { isPlatformServer } from '@angular/common';
import { provideServerRendering, withRoutes } from '@angular/ssr';
import { appConfig } from './app.config';
import { serverRoutes } from './app.routes.server';
import { SSR_FLAG } from '@abp/ng.core';
const serverConfig: ApplicationConfig = {
providers: [
provideAppInitializer(() => {
const platformId = inject(PLATFORM_ID);
const transferState = inject<TransferState>(TransferState);
if (isPlatformServer(platformId)) {
transferState.set(SSR_FLAG, true);
}
}),
provideServerRendering(withRoutes(serverRoutes))
]
};
export const config = mergeApplicationConfig(appConfig, serverConfig);

7
npm/ng-packs/packages/schematics/src/commands/ssr-add/server/files/server-builder/standalone-src/main.server.ts.template

@ -0,0 +1,7 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { <%= appComponentName %> } from '<%= appComponentPath %>';
import { config } from './app/app.config.server';
const bootstrap = () => bootstrapApplication(<%= appComponentName %>, config);
export default bootstrap;

268
npm/ng-packs/packages/schematics/src/commands/ssr-add/server/index.ts

@ -0,0 +1,268 @@
/**
* @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 { JsonValue, Path, basename, dirname, join, normalize } from '@angular-devkit/core';
import {
Rule,
SchematicsException,
Tree,
apply,
applyTemplates,
chain,
mergeWith,
move,
strings,
url,
} from '@angular-devkit/schematics';
import { posix } from 'node:path';
import {
DependencyType,
InstallBehavior,
addDependency,
addRootProvider,
} from '../../../utils/angular';
import { getPackageJsonDependency } from '../../../utils/angular';
import { JSONFile } from '../../../utils/angular/json-file';
import { latestVersions } from '../../../utils/angular/latest-versions';
import { isStandaloneApp } from '../../../utils/angular/ng-ast-utils';
import { relativePathToWorkspaceRoot } from '../../../utils/angular/paths';
import {
isUsingApplicationBuilder,
targetBuildNotFoundError,
} from '../../../utils/angular/project-targets';
import { resolveBootstrappedComponentData } from '../../../utils/angular/standalone/app_component';
import { getMainFilePath } from '../../../utils/angular/standalone/util';
import { getWorkspace, updateWorkspace } from '../../../utils/angular/workspace';
import { Builders } from '../../../utils/angular/workspace-models';
// @ts-ignore
import { Schema as ServerOptions } from './schema';
const serverMainEntryName = 'main.server.ts';
function updateConfigFileBrowserBuilder(options: ServerOptions, tsConfigDirectory: Path): Rule {
return updateWorkspace(workspace => {
const clientProject = workspace.projects.get(options.project);
if (clientProject) {
// 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.
const getServerOptions = (options: Record<string, JsonValue | undefined> = {}): {} => {
return {
buildOptimizer: options?.buildOptimizer,
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,
inlineStyleLanguage: options?.inlineStyleLanguage,
vendorChunk: options?.vendorChunk,
};
};
const buildTarget = clientProject.targets.get('build');
if (buildTarget?.options) {
buildTarget.options.outputPath = `dist/${options.project}/browser`;
}
const buildConfigurations = buildTarget?.configurations;
const configurations: Record<string, {}> = {};
if (buildConfigurations) {
for (const [key, options] of Object.entries(buildConfigurations)) {
configurations[key] = getServerOptions(options);
}
}
const sourceRoot = clientProject.sourceRoot ?? join(normalize(clientProject.root), 'src');
const serverTsConfig = join(tsConfigDirectory, 'tsconfig.server.json');
clientProject.targets.add({
name: 'server',
builder: Builders.Server,
defaultConfiguration: 'production',
options: {
outputPath: `dist/${options.project}/server`,
main: join(normalize(sourceRoot), serverMainEntryName),
tsConfig: serverTsConfig,
...(buildTarget?.options ? getServerOptions(buildTarget?.options) : {}),
},
configurations,
});
}
});
}
function updateConfigFileApplicationBuilder(options: ServerOptions): Rule {
return updateWorkspace(workspace => {
const project = workspace.projects.get(options.project);
if (!project) {
return;
}
const buildTarget = project.targets.get('build');
if (!buildTarget) {
return;
}
buildTarget.options ??= {};
buildTarget.options['server'] = posix.join(
project.sourceRoot ?? posix.join(project.root, 'src'),
serverMainEntryName,
);
buildTarget.options['outputMode'] = 'static';
});
}
function updateTsConfigFile(tsConfigPath: string): Rule {
return (host: Tree) => {
const json = new JSONFile(host, tsConfigPath);
// Skip adding the files entry if the server entry would already be included.
const include = json.get(['include']);
if (!Array.isArray(include) || !include.includes('src/**/*.ts')) {
const filesPath = ['files'];
const files = new Set((json.get(filesPath) as string[] | undefined) ?? []);
files.add('src/' + serverMainEntryName);
json.modify(filesPath, [...files]);
}
const typePath = ['compilerOptions', 'types'];
const types = new Set((json.get(typePath) as string[] | undefined) ?? []);
types.add('node');
json.modify(typePath, [...types]);
};
}
function addDependencies(skipInstall: boolean | undefined): Rule {
return (host: Tree) => {
const coreDep = getPackageJsonDependency(host, '@angular/core');
if (coreDep === null) {
throw new SchematicsException('Could not find version.');
}
const install = skipInstall ? InstallBehavior.None : InstallBehavior.Auto;
return chain([
addDependency('@angular/ssr', '~20.0.0', {
type: DependencyType.Default,
install,
}),
addDependency('@angular/platform-server', coreDep.version, {
type: DependencyType.Default,
install,
}),
addDependency('@types/node', latestVersions.dependencies['@types/node'], {
type: DependencyType.Dev,
install,
}),
]);
};
}
export default function (options: ServerOptions): Rule {
return async (host: Tree) => {
const workspace = await getWorkspace(host);
const clientProject = workspace.projects.get(options.project);
if (clientProject?.extensions.projectType !== 'application') {
throw new SchematicsException(`Server schematic requires a project type of "application".`);
}
const clientBuildTarget = clientProject.targets.get('build');
if (!clientBuildTarget) {
throw targetBuildNotFoundError();
}
const usingApplicationBuilder = isUsingApplicationBuilder(clientProject);
if (
clientProject.targets.has('server') ||
(usingApplicationBuilder && clientBuildTarget.options?.server !== undefined)
) {
// Server has already been added.
return;
}
const clientBuildOptions = clientBuildTarget.options as Record<string, string>;
const browserEntryPoint = await getMainFilePath(host, options.project);
const isStandalone = isStandaloneApp(host, browserEntryPoint);
const sourceRoot = clientProject.sourceRoot ?? join(normalize(clientProject.root), 'src');
let filesUrl = `./files/${usingApplicationBuilder ? 'application-builder/' : 'server-builder/'}`;
filesUrl += isStandalone ? 'standalone-src' : 'ngmodule-src';
const { componentName, componentImportPathInSameFile, moduleName, moduleImportPathInSameFile } =
resolveBootstrappedComponentData(host, browserEntryPoint) || {
componentName: 'App',
componentImportPathInSameFile: './app/app',
moduleName: 'AppModule',
moduleImportPathInSameFile: './app/app.module',
};
const templateSource = apply(url(filesUrl), [
applyTemplates({
...strings,
...options,
appComponentName: componentName,
appComponentPath: componentImportPathInSameFile,
appModuleName: moduleName,
appModulePath:
moduleImportPathInSameFile === null
? null
: `./${posix.basename(moduleImportPathInSameFile)}`,
}),
move(sourceRoot),
]);
const clientTsConfig = normalize(clientBuildOptions.tsConfig);
const tsConfigExtends = basename(clientTsConfig);
const tsConfigDirectory = dirname(clientTsConfig);
return chain([
mergeWith(templateSource),
...(usingApplicationBuilder
? [
updateConfigFileApplicationBuilder(options),
updateTsConfigFile(clientBuildOptions.tsConfig),
]
: [
mergeWith(
apply(url('./files/server-builder/root'), [
applyTemplates({
...strings,
...options,
stripTsExtension: (s: string) => s.replace(/\.ts$/, ''),
tsConfigExtends,
hasLocalizePackage: !!getPackageJsonDependency(host, '@angular/localize'),
relativePathToWorkspaceRoot: relativePathToWorkspaceRoot(tsConfigDirectory),
}),
move(tsConfigDirectory),
]),
),
updateConfigFileBrowserBuilder(options, tsConfigDirectory),
]),
addDependencies(options.skipInstall),
addRootProvider(
options.project,
({ code, external }) =>
code`${external('provideClientHydration', '@angular/platform-browser')}(${external(
'withEventReplay',
'@angular/platform-browser',
)}(), ${external(
'withIncrementalHydration',
'@angular/platform-browser',
)}(), ${external('withHttpTransferCacheOptions', '@angular/platform-browser')}({}))`,
),
]);
};
}

23
npm/ng-packs/packages/schematics/src/commands/ssr-add/server/schema.json

@ -0,0 +1,23 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "SchematicsAngularServerApp",
"title": "Angular Server App Options Schema",
"type": "object",
"additionalProperties": false,
"description": "Sets up server-side rendering (SSR) for your Angular application. SSR allows your app to be rendered on the server, improving initial load performance and SEO. This schematic configures your project for SSR and generates the necessary files.",
"properties": {
"project": {
"type": "string",
"description": "The name of the project to enable server-side rendering for.",
"$default": {
"$source": "projectName"
}
},
"skipInstall": {
"description": "Skip the automatic installation of packages. You will need to manually install the dependencies later.",
"type": "boolean",
"default": false
}
},
"required": ["project"]
}

1
npm/ng-packs/packages/schematics/src/utils/angular/index.ts

@ -12,3 +12,4 @@ export * from './validation';
export * from './workspace';
export * from './workspace-models';
export * from './standalone';
export * from './dependency';

28
npm/ng-packs/packages/schematics/src/utils/angular/latest-versions/index.ts

@ -0,0 +1,28 @@
export const latestVersions = {
description: 'Package versions used by schematics in @schematics/angular.',
comment: 'This file is needed so that dependencies are synced by Renovate.',
private: true,
dependencies: {
'@types/express': '~5.0.0',
'@types/jasmine': '~5.1.0',
'@types/node': '~20.11.0',
'browser-sync': '^3.0.0',
express: '~5.1.0',
'jasmine-core': '~5.9.0',
'jasmine-spec-reporter': '~7.0.0',
'karma-chrome-launcher': '~3.2.0',
'karma-coverage': '~2.2.0',
'karma-jasmine-html-reporter': '~2.1.0',
'karma-jasmine': '~5.1.0',
karma: '~6.4.0',
less: '^4.2.0',
postcss: '^8.5.3',
protractor: '~7.0.0',
rxjs: '~7.8.0',
tslib: '^2.3.0',
'ts-node': '~10.9.0',
typescript: '~5.8.0',
'zone.js': '~0.15.0',
'openid-client': '^6.6.4',
},
};

2
npm/ng-packs/packages/schematics/src/utils/angular/workspace.ts

@ -161,3 +161,5 @@ export function* allTargetOptions(
}
}
}
export { getWorkspace as readWorkspace } from './workspace'; // for backwards compatibility

3
npm/ng-packs/scripts/build-schematics.ts

@ -40,6 +40,9 @@ const FILES_TO_COPY_AFTER_BUILD: (FileCopy | string)[] = [
{ src: 'src/commands/api/files-model', dest: 'commands/api/files-model' },
{ src: 'src/commands/api/files-service', dest: 'commands/api/files-service' },
{ src: 'src/commands/api/schema.json', dest: 'commands/api/schema.json' },
{ src: 'src/commands/ssr-add/schema.json', dest: 'commands/ssr-add/schema.json' },
{ src: 'src/commands/ssr-add/files', dest: 'commands/ssr-add/files' },
{ src: 'src/commands/ssr-add/server', dest: 'commands/ssr-add/server' },
{ src: 'src/collection.json', dest: 'collection.json' },
'package.json',
'README.md',

Loading…
Cancel
Save