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
3 changes: 0 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@
"@eslint/compat": "2.1.0",
"@eslint/eslintrc": "3.3.6",
"@eslint/js": "10.0.1",
"@rollup/plugin-alias": "^6.0.0",
"@rollup/plugin-commonjs": "^29.0.0",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "16.0.3",
Expand Down Expand Up @@ -122,14 +121,12 @@
"puppeteer": "25.9.0",
"quicktype-core": "26.0.0",
"rollup": "4.63.0",
"rollup-license-plugin": "~3.2.0",
"rollup-plugin-dts": "6.5.1",
"rollup-plugin-sourcemaps2": "0.5.8",
"semver": "7.8.5",
"source-map-support": "0.5.21",
"tslib": "2.8.1",
"undici": "8.10.0",
"unenv": "^1.10.0",
"verdaccio": "6.10.0",
"verdaccio-auth-memory": "^13.0.0",
"zone.js": "^0.16.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ export async function executePostBundleSteps(
// Create server manifest
const initialFilesPaths = new Set(initialFiles.keys());
if (serverEntryPoint && (outputMode || prerenderOptions || appShellOptions || ssrOptions)) {
const { manifestContent, serverAssetsChunks } = generateAngularServerAppManifest(
const { manifestContent, serverAssetsChunks } = await generateAngularServerAppManifest(
additionalHtmlOutputFiles,
outputFiles,
optimizationOptions.styles.inlineCritical ?? false,
Expand Down Expand Up @@ -209,7 +209,7 @@ export async function executePostBundleSteps(
const manifest = additionalOutputFiles.find((f) => f.path === SERVER_APP_MANIFEST_FILENAME);
assert(manifest, `${SERVER_APP_MANIFEST_FILENAME} was not found in output files.`);

const { manifestContent, serverAssetsChunks } = generateAngularServerAppManifest(
const { manifestContent, serverAssetsChunks } = await generateAngularServerAppManifest(
additionalHtmlOutputFiles,
outputFiles,
optimizationOptions.styles.inlineCritical ?? false,
Expand Down
2 changes: 1 addition & 1 deletion packages/angular/build/src/utils/index-file/nonce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export async function addNonce(html: string): Promise<string> {
}

/** Finds the Angular nonce in an HTML string. */
async function findNonce(html: string): Promise<string | null> {
export async function findNonce(html: string): Promise<string | null> {
// Inexpensive check to avoid parsing the HTML when we're sure there's no nonce.
if (!NONCE_ATTR_PATTERN.test(html)) {
return null;
Expand Down
42 changes: 30 additions & 12 deletions packages/angular/build/src/utils/server-rendering/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,17 @@
import type { Metafile } from 'esbuild';
import { Buffer } from 'node:buffer';
import { extname } from 'node:path';
import { NormalizedApplicationBuildOptions } from '../../builders/application/options';
import {
INDEX_HTML_SERVER,
NormalizedApplicationBuildOptions,
} from '../../builders/application/options';
import {
type BuildOutputFile,
BuildOutputFileType,
createOutputFile,
} from '../../tools/esbuild/bundler-files';
import { findNonce } from '../index-file/nonce';
import { joinUrlParts } from '../url';

export const SERVER_APP_MANIFEST_FILENAME = 'angular-app-manifest.mjs';
export const SERVER_APP_ENGINE_MANIFEST_FILENAME = 'angular-app-engine-manifest.mjs';
Expand All @@ -33,11 +38,6 @@ export const SERVER_GENERATED_EXTERNALS = new Set([
'./' + SERVER_APP_ENGINE_MANIFEST_FILENAME,
]);

interface FilesMapping {
path: string;
dynamicImport: boolean;
}

const MAIN_SERVER_OUTPUT_FILENAME = 'main.server.mjs';

/**
Expand Down Expand Up @@ -143,7 +143,7 @@ export default {
* - `manifestContent`: A string of the SSR manifest content.
* - `serverAssetsChunks`: An array of build output files containing the generated assets for the server.
*/
export function generateAngularServerAppManifest(
export async function generateAngularServerAppManifest(
additionalHtmlOutputFiles: Map<string, BuildOutputFile>,
outputFiles: BuildOutputFile[],
inlineCriticalCss: boolean,
Expand All @@ -153,16 +153,23 @@ export function generateAngularServerAppManifest(
initialFiles: Set<string>,
metafile: Metafile,
publicPath: string | undefined,
): {
): Promise<{
manifestContent: string;
serverAssetsChunks: BuildOutputFile[];
} {
}> {
const serverAssetsChunks: BuildOutputFile[] = [];
const serverAssets: Record<string, string> = {};
const criticalCssPlans: unknown[] = [];
let nonce: string | undefined;

// TODO(alanagius): This is done here as we do not use module resolution bundler/node16
const { compileSheet, encodePlan } = (await import(
'beasties/compiler' as string
)) as typeof import('beasties/compiler', { with: { 'resolution-mode': 'import' } });
Comment thread
clydin marked this conversation as resolved.

for (const file of [...additionalHtmlOutputFiles.values(), ...outputFiles]) {
const extension = extname(file.path);
if (extension === '.html' || (inlineCriticalCss && extension === '.css')) {
if (extension === '.html') {
const jsChunkFilePath = `assets-chunks/${file.path.replace(/[./]/g, '_')}.mjs`;
const escapedContent = escapeUnsafeChars(file.text);

Expand All @@ -185,9 +192,19 @@ export function generateAngularServerAppManifest(

serverAssets[file.path] =
`{size: ${size}, hash: '${file.hash}', text: () => import('./${jsChunkFilePath}').then(m => m.default)}`;
} else if (inlineCriticalCss && extension === '.css') {
const sheet = compileSheet(file.text, {
href: joinUrlParts(publicPath ?? '', file.path),
});
criticalCssPlans.push(encodePlan(sheet));
}
}

const indexHtml = additionalHtmlOutputFiles.get(INDEX_HTML_SERVER)?.text;
if (indexHtml) {
nonce = (await findNonce(indexHtml)) ?? undefined;
}

// When routes have been extracted, mappings are no longer needed, as preloads will be included in the metadata.
const entryPointToBrowserMapping = routes?.length
? undefined
Expand All @@ -196,9 +213,10 @@ export function generateAngularServerAppManifest(
const manifestContent = `
export default {
bootstrap: () => import('./main.server.mjs').then(m => m.default),
inlineCriticalCss: ${inlineCriticalCss},
baseHref: '${baseHref}',
locale: ${JSON.stringify(locale)},
${criticalCssPlans.length ? ` criticalCssPlans: ${JSON.stringify(criticalCssPlans)},\n` : ''}${
nonce ? ` nonce: ${JSON.stringify(nonce)},\n` : ''
} locale: ${JSON.stringify(locale)},
routes: ${JSON.stringify(routes, undefined, 2)},
entryPointToBrowserMapping: ${JSON.stringify(entryPointToBrowserMapping, undefined, 2)},
assets: {
Expand Down
143 changes: 143 additions & 0 deletions packages/angular/build/src/utils/server-rendering/manifest_spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/**
* @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 type { Metafile } from 'esbuild';
import { BuildOutputFileType, createOutputFile } from '../../tools/esbuild/bundler-files';
import { initializeHash } from '../hash';
import { generateAngularServerAppManifest } from './manifest';

describe('generateAngularServerAppManifest', () => {
beforeAll(async () => {
await initializeHash();
});

const dummyMetafile = { inputs: {}, outputs: {} } as unknown as Metafile;

it('should include criticalCssPlans when inlineCriticalCss is true', async () => {
const additionalHtml = new Map([
[
'index.server.html',
createOutputFile(
'index.server.html',
'<html><body><app-root></app-root></body></html>',
BuildOutputFileType.ServerApplication,
),
],
]);
const outputFiles = [
createOutputFile('styles.css', 'h1 { color: blue; }', BuildOutputFileType.Browser),
];

const { manifestContent } = await generateAngularServerAppManifest(
additionalHtml,
outputFiles,
true,
undefined,
undefined,
'/',
new Set(),
dummyMetafile,
undefined,
);

expect(manifestContent).toContain('criticalCssPlans: [');
expect(manifestContent).toContain('styles.css');
expect(manifestContent).not.toContain('nonce:');
});

it('should not include criticalCssPlans when inlineCriticalCss is false', async () => {
const additionalHtml = new Map([
[
'index.server.html',
createOutputFile(
'index.server.html',
'<html><body><app-root></app-root></body></html>',
BuildOutputFileType.ServerApplication,
),
],
]);
const outputFiles = [
createOutputFile('styles.css', 'h1 { color: blue; }', BuildOutputFileType.Browser),
];

const { manifestContent } = await generateAngularServerAppManifest(
additionalHtml,
outputFiles,
false,
undefined,
undefined,
'/',
new Set(),
dummyMetafile,
undefined,
);

expect(manifestContent).not.toContain('criticalCssPlans:');
});

it('should extract template nonce from index HTML when present', async () => {
const additionalHtml = new Map([
[
'index.server.html',
createOutputFile(
'index.server.html',
'<html><body><app-root ngCspNonce="{% nonce %}"></app-root></body></html>',
BuildOutputFileType.ServerApplication,
),
],
]);
const outputFiles = [
createOutputFile('styles.css', 'h1 { color: blue; }', BuildOutputFileType.Browser),
];

const { manifestContent } = await generateAngularServerAppManifest(
additionalHtml,
outputFiles,
true,
undefined,
undefined,
'/',
new Set(),
dummyMetafile,
undefined,
);

expect(manifestContent).toContain('nonce: "{% nonce %}"');
});

it('should not include css files in serverAssetsChunks or assets', async () => {
const additionalHtml = new Map([
[
'index.server.html',
createOutputFile(
'index.server.html',
'<html><body></body></html>',
BuildOutputFileType.ServerApplication,
),
],
]);
const outputFiles = [
createOutputFile('styles.css', 'h1 { color: blue; }', BuildOutputFileType.Browser),
];

const { manifestContent, serverAssetsChunks } = await generateAngularServerAppManifest(
additionalHtml,
outputFiles,
true,
undefined,
undefined,
'/',
new Set(),
dummyMetafile,
undefined,
);

expect(serverAssetsChunks.some((chunk) => chunk.path.includes('styles'))).toBeFalse();
expect(manifestContent).not.toContain("'styles.css':");
});
});
15 changes: 1 addition & 14 deletions packages/angular/ssr/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -22,43 +22,30 @@ ts_project(
"--lib",
"dom.iterable,dom,es2022",
],
data = [
"//packages/angular/ssr/third_party/beasties:beasties_bundled",
],
source_map = True,
tsconfig = "//:build-tsconfig-esm",
deps = [
":node_modules/beasties",
"//:node_modules/@angular/common",
"//:node_modules/@angular/core",
"//:node_modules/@angular/platform-browser",
"//:node_modules/@angular/platform-server",
"//:node_modules/@angular/router",
"//:node_modules/tslib",
"//packages/angular/ssr/third_party/beasties:beasties_dts",
],
)

ng_package(
name = "npm_package",
srcs = [
":package.json",
"//packages/angular/ssr/third_party/beasties:beasties_bundled",
],
externals = [
"@angular/ssr",
"@angular/ssr/node",
"../../third_party/beasties",
],
extra_substitutions = {
# Needed for ssr.d.ts file
"\\./third_party/beasties": "../third_party/beasties",
# Needed for the FESM file.
"\\./(.+)/packages/angular/ssr/third_party/beasties": "../third_party/beasties/index.js",
},
nested_packages = [
"//packages/angular/ssr/schematics:pkg",
# Included directly as the generated types reference the types file in this location.
"//packages/angular/ssr/third_party/beasties:beasties_dts",
],
package = "@angular/ssr",
readme_md = ":README.md",
Expand Down
1 change: 1 addition & 0 deletions packages/angular/ssr/node/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@ ts_project(
"//:node_modules/@angular/platform-server",
"//:node_modules/@types/node",
"//packages/angular/ssr",
"//packages/angular/ssr:node_modules/beasties",
],
)
Loading