diff --git a/.prettierignore b/.prettierignore
index c78211cd0633..ae6046eb4baf 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -12,6 +12,6 @@
/CONTRIBUTING.md
.yarn/
dist/
-third_party/
+third_party/github.com/
/tests/legacy-cli/e2e/assets/
-/tools/test/*.json
\ No newline at end of file
+/tools/test/*.json
diff --git a/package.json b/package.json
index b1623b0d07fd..5e380d55cd2a 100644
--- a/package.json
+++ b/package.json
@@ -83,6 +83,7 @@
"@bazel/buildifier": "7.1.2",
"@bazel/concatjs": "patch:@bazel/concatjs@npm%3A5.8.1#~/.yarn/patches/@bazel-concatjs-npm-5.8.1-1bf81df846.patch",
"@bazel/jasmine": "patch:@bazel/jasmine@npm%3A5.8.1#~/.yarn/patches/@bazel-jasmine-npm-5.8.1-3370fee155.patch",
+ "@bazel/runfiles": "^5.8.1",
"@discoveryjs/json-ext": "0.6.1",
"@inquirer/confirm": "3.1.22",
"@inquirer/prompts": "5.3.8",
@@ -199,6 +200,7 @@
"tslib": "2.6.3",
"typescript": "5.5.4",
"undici": "6.19.8",
+ "unenv": "^1.10.0",
"verdaccio": "5.32.1",
"verdaccio-auth-memory": "^10.0.0",
"vite": "5.4.2",
diff --git a/packages/angular/build/BUILD.bazel b/packages/angular/build/BUILD.bazel
index 49481ff36e87..b60ab7d83299 100644
--- a/packages/angular/build/BUILD.bazel
+++ b/packages/angular/build/BUILD.bazel
@@ -79,6 +79,7 @@ ts_library(
"@npm//browserslist",
"@npm//critters",
"@npm//esbuild",
+ "@npm//esbuild-wasm",
"@npm//fast-glob",
"@npm//https-proxy-agent",
"@npm//listr2",
diff --git a/packages/angular/build/src/typings.d.ts b/packages/angular/build/src/typings.d.ts
new file mode 100644
index 000000000000..c784b3c4220a
--- /dev/null
+++ b/packages/angular/build/src/typings.d.ts
@@ -0,0 +1,19 @@
+/**
+ * @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
+ */
+
+// The `bundled_critters` causes issues with module mappings in Bazel,
+// leading to unexpected behavior with esbuild. Specifically, the problem occurs
+// when esbuild resolves to a different module or version than expected, due to
+// how Bazel handles module mappings.
+//
+// This change aims to resolve esbuild types correctly and maintain consistency
+// in the Bazel build process.
+
+declare module 'esbuild' {
+ export * from 'esbuild-wasm';
+}
diff --git a/packages/angular/build/src/utils/index-file/inline-critical-css.ts b/packages/angular/build/src/utils/index-file/inline-critical-css.ts
index fe68c8abe105..ad531422989c 100644
--- a/packages/angular/build/src/utils/index-file/inline-critical-css.ts
+++ b/packages/angular/build/src/utils/index-file/inline-critical-css.ts
@@ -20,36 +20,46 @@ const MEDIA_SET_HANDLER_PATTERN = /^this\.media=["'](.*)["'];?$/;
const CSP_MEDIA_ATTR = 'ngCspMedia';
/**
- * Script text used to change the media value of the link tags.
+ * Script that dynamically updates the `media` attribute of `` tags based on a custom attribute (`CSP_MEDIA_ATTR`).
*
* NOTE:
* We do not use `document.querySelectorAll('link').forEach((s) => s.addEventListener('load', ...)`
- * because this does not always fire on Chome.
+ * because load events are not always triggered reliably on Chrome.
* See: https://github.com/angular/angular-cli/issues/26932 and https://crbug.com/1521256
+ *
+ * The script:
+ * - Ensures the event target is a `` tag with the `CSP_MEDIA_ATTR` attribute.
+ * - Updates the `media` attribute with the value of `CSP_MEDIA_ATTR` and then removes the attribute.
+ * - Removes the event listener when all relevant `` tags have been processed.
+ * - Uses event capturing (the `true` parameter) since load events do not bubble up the DOM.
*/
-const LINK_LOAD_SCRIPT_CONTENT = [
- '(() => {',
- ` const CSP_MEDIA_ATTR = '${CSP_MEDIA_ATTR}';`,
- ' const documentElement = document.documentElement;',
- ' const listener = (e) => {',
- ' const target = e.target;',
- ` if (!target || target.tagName !== 'LINK' || !target.hasAttribute(CSP_MEDIA_ATTR)) {`,
- ' return;',
- ' }',
-
- ' target.media = target.getAttribute(CSP_MEDIA_ATTR);',
- ' target.removeAttribute(CSP_MEDIA_ATTR);',
-
- // Remove onload listener when there are no longer styles that need to be loaded.
- ' if (!document.head.querySelector(`link[${CSP_MEDIA_ATTR}]`)) {',
- ` documentElement.removeEventListener('load', listener);`,
- ' }',
- ' };',
-
- // We use an event with capturing (the true parameter) because load events don't bubble.
- ` documentElement.addEventListener('load', listener, true);`,
- '})();',
-].join('\n');
+const LINK_LOAD_SCRIPT_CONTENT = `
+(() => {
+ const CSP_MEDIA_ATTR = '${CSP_MEDIA_ATTR}';
+ const documentElement = document.documentElement;
+
+ // Listener for load events on link tags.
+ const listener = (e) => {
+ const target = e.target;
+ if (
+ !target ||
+ target.tagName !== 'LINK' ||
+ !target.hasAttribute(CSP_MEDIA_ATTR)
+ ) {
+ return;
+ }
+
+ target.media = target.getAttribute(CSP_MEDIA_ATTR);
+ target.removeAttribute(CSP_MEDIA_ATTR);
+
+ if (!document.head.querySelector(\`link[\${CSP_MEDIA_ATTR}]\`)) {
+ documentElement.removeEventListener('load', listener);
+ }
+ };
+
+ documentElement.addEventListener('load', listener, true);
+})();
+`.trim();
export interface InlineCriticalCssProcessOptions {
outputPath: string;
@@ -85,22 +95,22 @@ interface PartialDocument {
querySelector(selector: string): PartialHTMLElement | null;
}
-/** Signature of the `Critters.embedLinkedStylesheet` method. */
-type EmbedLinkedStylesheetFn = (
- link: PartialHTMLElement,
- document: PartialDocument,
-) => Promise;
+/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
-class CrittersExtended extends Critters {
+// We use Typescript declaration merging because `embedLinkedStylesheet` it's not declared in
+// the `Critters` types which means that we can't call the `super` implementation.
+interface CrittersBase {
+ embedLinkedStylesheet(link: PartialHTMLElement, document: PartialDocument): Promise;
+}
+class CrittersBase extends Critters {}
+/* eslint-enable @typescript-eslint/no-unsafe-declaration-merging */
+
+class CrittersExtended extends CrittersBase {
readonly warnings: string[] = [];
readonly errors: string[] = [];
- private initialEmbedLinkedStylesheet: EmbedLinkedStylesheetFn;
private addedCspScriptsDocuments = new WeakSet();
private documentNonces = new WeakMap();
- // Inherited from `Critters`, but not exposed in the typings.
- protected declare embedLinkedStylesheet: EmbedLinkedStylesheetFn;
-
constructor(
private readonly optionsExtended: InlineCriticalCssProcessorOptions &
InlineCriticalCssProcessOptions,
@@ -119,17 +129,11 @@ class CrittersExtended extends Critters {
reduceInlineStyles: false,
mergeStylesheets: false,
// Note: if `preload` changes to anything other than `media`, the logic in
- // `embedLinkedStylesheetOverride` will have to be updated.
+ // `embedLinkedStylesheet` will have to be updated.
preload: 'media',
noscriptFallback: true,
inlineFonts: true,
});
-
- // We can't use inheritance to override `embedLinkedStylesheet`, because it's not declared in
- // the `Critters` .d.ts which means that we can't call the `super` implementation. TS doesn't
- // allow for `super` to be cast to a different type.
- this.initialEmbedLinkedStylesheet = this.embedLinkedStylesheet;
- this.embedLinkedStylesheet = this.embedLinkedStylesheetOverride;
}
public override readFile(path: string): Promise {
@@ -142,7 +146,10 @@ class CrittersExtended extends Critters {
* Override of the Critters `embedLinkedStylesheet` method
* that makes it work with Angular's CSP APIs.
*/
- private embedLinkedStylesheetOverride: EmbedLinkedStylesheetFn = async (link, document) => {
+ override async embedLinkedStylesheet(
+ link: PartialHTMLElement,
+ document: PartialDocument,
+ ): Promise {
if (link.getAttribute('media') === 'print' && link.next?.name === 'noscript') {
// Workaround for https://github.com/GoogleChromeLabs/critters/issues/64
// NB: this is only needed for the webpack based builders.
@@ -154,7 +161,7 @@ class CrittersExtended extends Critters {
}
}
- const returnValue = await this.initialEmbedLinkedStylesheet(link, document);
+ const returnValue = await super.embedLinkedStylesheet(link, document);
const cspNonce = this.findCspNonce(document);
if (cspNonce) {
@@ -182,7 +189,7 @@ class CrittersExtended extends Critters {
}
return returnValue;
- };
+ }
/**
* Finds the CSP nonce for a specific document.
diff --git a/packages/angular/ssr/BUILD.bazel b/packages/angular/ssr/BUILD.bazel
index dbd1662aee0f..0d0dea781e32 100644
--- a/packages/angular/ssr/BUILD.bazel
+++ b/packages/angular/ssr/BUILD.bazel
@@ -18,12 +18,12 @@ ts_library(
),
module_name = "@angular/ssr",
deps = [
+ "//packages/angular/ssr/third_party/critters:bundled_critters_lib",
"@npm//@angular/common",
"@npm//@angular/core",
"@npm//@angular/platform-server",
"@npm//@angular/router",
"@npm//@types/node",
- "@npm//critters",
],
)
@@ -32,8 +32,15 @@ ng_package(
package_name = "@angular/ssr",
srcs = [
":package.json",
+ "//packages/angular/ssr/third_party/critters:bundled_critters_lib",
+ ],
+ externals = [
+ "express",
+ "../../third_party/critters",
+ ],
+ nested_packages = [
+ "//packages/angular/ssr/schematics:npm_package",
],
- nested_packages = ["//packages/angular/ssr/schematics:npm_package"],
tags = ["release-package"],
deps = [
":ssr",
diff --git a/packages/angular/ssr/package.json b/packages/angular/ssr/package.json
index 608de72116e1..747189433350 100644
--- a/packages/angular/ssr/package.json
+++ b/packages/angular/ssr/package.json
@@ -13,7 +13,6 @@
"save": "dependencies"
},
"dependencies": {
- "critters": "0.0.24",
"tslib": "^2.3.0"
},
"peerDependencies": {
@@ -29,6 +28,7 @@
"@angular/platform-browser": "19.0.0-next.1",
"@angular/platform-server": "19.0.0-next.1",
"@angular/router": "19.0.0-next.1",
+ "@bazel/runfiles": "^5.8.1",
"zone.js": "^0.15.0"
},
"schematics": "./schematics/collection.json",
diff --git a/packages/angular/ssr/src/app.ts b/packages/angular/ssr/src/app.ts
index f8bc12fa17f0..06925a239a64 100644
--- a/packages/angular/ssr/src/app.ts
+++ b/packages/angular/ssr/src/app.ts
@@ -14,6 +14,7 @@ import { Hooks } from './hooks';
import { getAngularAppManifest } from './manifest';
import { ServerRouter } from './routes/router';
import { REQUEST, REQUEST_CONTEXT, RESPONSE_INIT } from './tokens';
+import { InlineCriticalCssProcessor } from './utils/inline-critical-css';
import { renderAngular } from './utils/ng';
/**
@@ -52,6 +53,11 @@ export class AngularServerApp {
*/
private router: ServerRouter | undefined;
+ /**
+ * The `inlineCriticalCssProcessor` is responsible for handling critical CSS inlining.
+ */
+ private inlineCriticalCssProcessor: InlineCriticalCssProcessor | undefined;
+
/**
* Renders a response for the given HTTP request using the server application.
*
@@ -177,10 +183,20 @@ export class AngularServerApp {
html = await hooks.run('html:transform:pre', { html });
}
- return new Response(
- await renderAngular(html, manifest.bootstrap(), new URL(request.url), platformProviders),
- responseInit,
- );
+ html = await renderAngular(html, manifest.bootstrap(), new URL(request.url), platformProviders);
+
+ if (manifest.inlineCriticalCss) {
+ // Optionally inline critical CSS.
+ this.inlineCriticalCssProcessor ??= new InlineCriticalCssProcessor((path: string) => {
+ const fileName = path.split('/').pop() ?? path;
+
+ return this.assets.getServerAsset(fileName);
+ });
+
+ html = await this.inlineCriticalCssProcessor.process(html);
+ }
+
+ return new Response(html, responseInit);
}
}
diff --git a/packages/angular/ssr/src/common-engine/common-engine.ts b/packages/angular/ssr/src/common-engine/common-engine.ts
index 02a7ba6c55a6..acbfb277cc1e 100644
--- a/packages/angular/ssr/src/common-engine/common-engine.ts
+++ b/packages/angular/ssr/src/common-engine/common-engine.ts
@@ -11,7 +11,7 @@ import { renderApplication, renderModule, ɵSERVER_CONTEXT } from '@angular/plat
import * as fs from 'node:fs';
import { dirname, join, normalize, resolve } from 'node:path';
import { URL } from 'node:url';
-import { InlineCriticalCssProcessor, InlineCriticalCssResult } from './inline-css-processor';
+import { CommonEngineInlineCriticalCssProcessor } from './inline-css-processor';
import {
noopRunMethodAndMeasurePerf,
printPerformanceLogs,
@@ -55,14 +55,10 @@ export interface CommonEngineRenderOptions {
export class CommonEngine {
private readonly templateCache = new Map();
- private readonly inlineCriticalCssProcessor: InlineCriticalCssProcessor;
+ private readonly inlineCriticalCssProcessor = new CommonEngineInlineCriticalCssProcessor();
private readonly pageIsSSG = new Map();
- constructor(private options?: CommonEngineOptions) {
- this.inlineCriticalCssProcessor = new InlineCriticalCssProcessor({
- minify: false,
- });
- }
+ constructor(private options?: CommonEngineOptions) {}
/**
* Render an HTML document for a specific URL with specified
@@ -81,17 +77,12 @@ export class CommonEngine {
html = await runMethod('Render Page', () => this.renderApplication(opts));
if (opts.inlineCriticalCss !== false) {
- const { content, errors, warnings } = await runMethod('Inline Critical CSS', () =>
+ const content = await runMethod('Inline Critical CSS', () =>
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.inlineCriticalCss(html!, opts),
);
html = content;
-
- // eslint-disable-next-line no-console
- warnings?.forEach((m) => console.warn(m));
- // eslint-disable-next-line no-console
- errors?.forEach((m) => console.error(m));
}
}
@@ -102,13 +93,11 @@ export class CommonEngine {
return html;
}
- private inlineCriticalCss(
- html: string,
- opts: CommonEngineRenderOptions,
- ): Promise {
- return this.inlineCriticalCssProcessor.process(html, {
- outputPath: opts.publicPath ?? (opts.documentFilePath ? dirname(opts.documentFilePath) : ''),
- });
+ private inlineCriticalCss(html: string, opts: CommonEngineRenderOptions): Promise {
+ const outputPath =
+ opts.publicPath ?? (opts.documentFilePath ? dirname(opts.documentFilePath) : '');
+
+ return this.inlineCriticalCssProcessor.process(html, outputPath);
}
private async retrieveSSGPage(opts: CommonEngineRenderOptions): Promise {
diff --git a/packages/angular/ssr/src/common-engine/inline-css-processor.ts b/packages/angular/ssr/src/common-engine/inline-css-processor.ts
index 9664621f02d3..d9e8d0e2c2d3 100644
--- a/packages/angular/ssr/src/common-engine/inline-css-processor.ts
+++ b/packages/angular/ssr/src/common-engine/inline-css-processor.ts
@@ -6,258 +6,23 @@
* found in the LICENSE file at https://angular.dev/license
*/
-import Critters from 'critters';
import { readFile } from 'node:fs/promises';
+import { InlineCriticalCssProcessor } from '../utils/inline-critical-css';
-/**
- * Pattern used to extract the media query set by Critters in an `onload` handler.
- */
-const MEDIA_SET_HANDLER_PATTERN = /^this\.media=["'](.*)["'];?$/;
-
-/**
- * Name of the attribute used to save the Critters media query so it can be re-assigned on load.
- */
-const CSP_MEDIA_ATTR = 'ngCspMedia';
-
-/**
- * Script text used to change the media value of the link tags.
- *
- * NOTE:
- * We do not use `document.querySelectorAll('link').forEach((s) => s.addEventListener('load', ...)`
- * because this does not always fire on Chome.
- * See: https://github.com/angular/angular-cli/issues/26932 and https://crbug.com/1521256
- */
-const LINK_LOAD_SCRIPT_CONTENT = [
- '(() => {',
- ` const CSP_MEDIA_ATTR = '${CSP_MEDIA_ATTR}';`,
- ' const documentElement = document.documentElement;',
- ' const listener = (e) => {',
- ' const target = e.target;',
- ` if (!target || target.tagName !== 'LINK' || !target.hasAttribute(CSP_MEDIA_ATTR)) {`,
- ' return;',
- ' }',
-
- ' target.media = target.getAttribute(CSP_MEDIA_ATTR);',
- ' target.removeAttribute(CSP_MEDIA_ATTR);',
-
- // Remove onload listener when there are no longer styles that need to be loaded.
- ' if (!document.head.querySelector(`link[${CSP_MEDIA_ATTR}]`)) {',
- ` documentElement.removeEventListener('load', listener);`,
- ' }',
- ' };',
-
- // We use an event with capturing (the true parameter) because load events don't bubble.
- ` documentElement.addEventListener('load', listener, true);`,
- '})();',
-].join('\n');
-
-export interface InlineCriticalCssProcessOptions {
- outputPath?: string;
-}
-
-export interface InlineCriticalCssProcessorOptions {
- minify?: boolean;
- deployUrl?: string;
-}
-
-export interface InlineCriticalCssResult {
- content: string;
- warnings?: string[];
- errors?: string[];
-}
-
-/** Partial representation of an `HTMLElement`. */
-interface PartialHTMLElement {
- getAttribute(name: string): string | null;
- setAttribute(name: string, value: string): void;
- hasAttribute(name: string): boolean;
- removeAttribute(name: string): void;
- appendChild(child: PartialHTMLElement): void;
- insertBefore(newNode: PartialHTMLElement, referenceNode?: PartialHTMLElement): void;
- remove(): void;
- name: string;
- textContent: string;
- tagName: string | null;
- children: PartialHTMLElement[];
- next: PartialHTMLElement | null;
- prev: PartialHTMLElement | null;
-}
-
-/** Partial representation of an HTML `Document`. */
-interface PartialDocument {
- head: PartialHTMLElement;
- createElement(tagName: string): PartialHTMLElement;
- querySelector(selector: string): PartialHTMLElement | null;
-}
-
-/** Signature of the `Critters.embedLinkedStylesheet` method. */
-type EmbedLinkedStylesheetFn = (
- link: PartialHTMLElement,
- document: PartialDocument,
-) => Promise;
-
-class CrittersExtended extends Critters {
- readonly warnings: string[] = [];
- readonly errors: string[] = [];
- private initialEmbedLinkedStylesheet: EmbedLinkedStylesheetFn;
- private addedCspScriptsDocuments = new WeakSet();
- private documentNonces = new WeakMap();
-
- // Inherited from `Critters`, but not exposed in the typings.
- protected declare embedLinkedStylesheet: EmbedLinkedStylesheetFn;
-
- constructor(
- readonly optionsExtended: InlineCriticalCssProcessorOptions & InlineCriticalCssProcessOptions,
- private readonly resourceCache: Map,
- ) {
- super({
- logger: {
- warn: (s: string) => this.warnings.push(s),
- error: (s: string) => this.errors.push(s),
- info: () => {},
- },
- logLevel: 'warn',
- path: optionsExtended.outputPath,
- publicPath: optionsExtended.deployUrl,
- compress: !!optionsExtended.minify,
- pruneSource: false,
- reduceInlineStyles: false,
- mergeStylesheets: false,
- // Note: if `preload` changes to anything other than `media`, the logic in
- // `embedLinkedStylesheetOverride` will have to be updated.
- preload: 'media',
- noscriptFallback: true,
- inlineFonts: true,
- });
-
- // We can't use inheritance to override `embedLinkedStylesheet`, because it's not declared in
- // the `Critters` .d.ts which means that we can't call the `super` implementation. TS doesn't
- // allow for `super` to be cast to a different type.
- this.initialEmbedLinkedStylesheet = this.embedLinkedStylesheet;
- this.embedLinkedStylesheet = this.embedLinkedStylesheetOverride;
- }
-
- public override async readFile(path: string): Promise {
- let resourceContent = this.resourceCache.get(path);
- if (resourceContent === undefined) {
- resourceContent = await readFile(path, 'utf-8');
- this.resourceCache.set(path, resourceContent);
- }
-
- return resourceContent;
- }
-
- /**
- * Override of the Critters `embedLinkedStylesheet` method
- * that makes it work with Angular's CSP APIs.
- */
- private embedLinkedStylesheetOverride: EmbedLinkedStylesheetFn = async (link, document) => {
- if (link.getAttribute('media') === 'print' && link.next?.name === 'noscript') {
- // Workaround for https://github.com/GoogleChromeLabs/critters/issues/64
- // NB: this is only needed for the webpack based builders.
- const media = link.getAttribute('onload')?.match(MEDIA_SET_HANDLER_PATTERN);
- if (media) {
- link.removeAttribute('onload');
- link.setAttribute('media', media[1]);
- link?.next?.remove();
- }
- }
-
- const returnValue = await this.initialEmbedLinkedStylesheet(link, document);
- const cspNonce = this.findCspNonce(document);
-
- if (cspNonce) {
- const crittersMedia = link.getAttribute('onload')?.match(MEDIA_SET_HANDLER_PATTERN);
-
- if (crittersMedia) {
- // If there's a Critters-generated `onload` handler and the file has an Angular CSP nonce,
- // we have to remove the handler, because it's incompatible with CSP. We save the value
- // in a different attribute and we generate a script tag with the nonce that uses
- // `addEventListener` to apply the media query instead.
- link.removeAttribute('onload');
- link.setAttribute(CSP_MEDIA_ATTR, crittersMedia[1]);
- this.conditionallyInsertCspLoadingScript(document, cspNonce, link);
- }
-
- // Ideally we would hook in at the time Critters inserts the `style` tags, but there isn't
- // a way of doing that at the moment so we fall back to doing it any time a `link` tag is
- // inserted. We mitigate it by only iterating the direct children of the `` which
- // should be pretty shallow.
- document.head.children.forEach((child) => {
- if (child.tagName === 'style' && !child.hasAttribute('nonce')) {
- child.setAttribute('nonce', cspNonce);
- }
- });
- }
-
- return returnValue;
- };
-
- /**
- * Finds the CSP nonce for a specific document.
- */
- private findCspNonce(document: PartialDocument): string | null {
- if (this.documentNonces.has(document)) {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- return this.documentNonces.get(document)!;
- }
-
- // HTML attribute are case-insensitive, but the parser used by Critters is case-sensitive.
- const nonceElement = document.querySelector('[ngCspNonce], [ngcspnonce]');
- const cspNonce =
- nonceElement?.getAttribute('ngCspNonce') || nonceElement?.getAttribute('ngcspnonce') || null;
-
- this.documentNonces.set(document, cspNonce);
-
- return cspNonce;
- }
-
- /**
- * Inserts the `script` tag that swaps the critical CSS at runtime,
- * if one hasn't been inserted into the document already.
- */
- private conditionallyInsertCspLoadingScript(
- document: PartialDocument,
- nonce: string,
- link: PartialHTMLElement,
- ): void {
- if (this.addedCspScriptsDocuments.has(document)) {
- return;
- }
-
- if (document.head.textContent.includes(LINK_LOAD_SCRIPT_CONTENT)) {
- // Script was already added during the build.
- this.addedCspScriptsDocuments.add(document);
-
- return;
- }
-
- const script = document.createElement('script');
- script.setAttribute('nonce', nonce);
- script.textContent = LINK_LOAD_SCRIPT_CONTENT;
- // Prepend the script to the head since it needs to
- // run as early as possible, before the `link` tags.
- document.head.insertBefore(script, link);
- this.addedCspScriptsDocuments.add(document);
- }
-}
-
-export class InlineCriticalCssProcessor {
+export class CommonEngineInlineCriticalCssProcessor {
private readonly resourceCache = new Map();
- constructor(protected readonly options: InlineCriticalCssProcessorOptions) {}
+ async process(html: string, outputPath: string | undefined): Promise {
+ const critters = new InlineCriticalCssProcessor(async (path) => {
+ let resourceContent = this.resourceCache.get(path);
+ if (resourceContent === undefined) {
+ resourceContent = await readFile(path, 'utf-8');
+ this.resourceCache.set(path, resourceContent);
+ }
- async process(
- html: string,
- options: InlineCriticalCssProcessOptions,
- ): Promise {
- const critters = new CrittersExtended({ ...this.options, ...options }, this.resourceCache);
- const content = await critters.process(html);
+ return resourceContent;
+ }, outputPath);
- return {
- content,
- errors: critters.errors.length ? critters.errors : undefined,
- warnings: critters.warnings.length ? critters.warnings : undefined,
- };
+ return critters.process(html);
}
}
diff --git a/packages/angular/ssr/src/utils/inline-critical-css.ts b/packages/angular/ssr/src/utils/inline-critical-css.ts
new file mode 100644
index 000000000000..648a4d51577c
--- /dev/null
+++ b/packages/angular/ssr/src/utils/inline-critical-css.ts
@@ -0,0 +1,224 @@
+/**
+ * @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 Critters from '../../third_party/critters';
+
+/**
+ * Pattern used to extract the media query set by Critters in an `onload` handler.
+ */
+const MEDIA_SET_HANDLER_PATTERN = /^this\.media=["'](.*)["'];?$/;
+
+/**
+ * Name of the attribute used to save the Critters media query so it can be re-assigned on load.
+ */
+const CSP_MEDIA_ATTR = 'ngCspMedia';
+
+/**
+ * Script that dynamically updates the `media` attribute of `` tags based on a custom attribute (`CSP_MEDIA_ATTR`).
+ *
+ * NOTE:
+ * We do not use `document.querySelectorAll('link').forEach((s) => s.addEventListener('load', ...)`
+ * because load events are not always triggered reliably on Chrome.
+ * See: https://github.com/angular/angular-cli/issues/26932 and https://crbug.com/1521256
+ *
+ * The script:
+ * - Ensures the event target is a `` tag with the `CSP_MEDIA_ATTR` attribute.
+ * - Updates the `media` attribute with the value of `CSP_MEDIA_ATTR` and then removes the attribute.
+ * - Removes the event listener when all relevant `` tags have been processed.
+ * - Uses event capturing (the `true` parameter) since load events do not bubble up the DOM.
+ */
+const LINK_LOAD_SCRIPT_CONTENT = `
+(() => {
+ const CSP_MEDIA_ATTR = '${CSP_MEDIA_ATTR}';
+ const documentElement = document.documentElement;
+
+ // Listener for load events on link tags.
+ const listener = (e) => {
+ const target = e.target;
+ if (
+ !target ||
+ target.tagName !== 'LINK' ||
+ !target.hasAttribute(CSP_MEDIA_ATTR)
+ ) {
+ return;
+ }
+
+ target.media = target.getAttribute(CSP_MEDIA_ATTR);
+ target.removeAttribute(CSP_MEDIA_ATTR);
+
+ if (!document.head.querySelector(\`link[\${CSP_MEDIA_ATTR}]\`)) {
+ documentElement.removeEventListener('load', listener);
+ }
+ };
+
+ documentElement.addEventListener('load', listener, true);
+})();
+`.trim();
+
+/** Partial representation of an `HTMLElement`. */
+interface PartialHTMLElement {
+ getAttribute(name: string): string | null;
+ setAttribute(name: string, value: string): void;
+ hasAttribute(name: string): boolean;
+ removeAttribute(name: string): void;
+ appendChild(child: PartialHTMLElement): void;
+ insertBefore(newNode: PartialHTMLElement, referenceNode?: PartialHTMLElement): void;
+ remove(): void;
+ name: string;
+ textContent: string;
+ tagName: string | null;
+ children: PartialHTMLElement[];
+ next: PartialHTMLElement | null;
+ prev: PartialHTMLElement | null;
+}
+
+/** Partial representation of an HTML `Document`. */
+interface PartialDocument {
+ head: PartialHTMLElement;
+ createElement(tagName: string): PartialHTMLElement;
+ querySelector(selector: string): PartialHTMLElement | null;
+}
+
+/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
+
+// We use Typescript declaration merging because `embedLinkedStylesheet` it's not declared in
+// the `Critters` types which means that we can't call the `super` implementation.
+interface CrittersBase {
+ embedLinkedStylesheet(link: PartialHTMLElement, document: PartialDocument): Promise;
+}
+class CrittersBase extends Critters {}
+/* eslint-enable @typescript-eslint/no-unsafe-declaration-merging */
+
+export class InlineCriticalCssProcessor extends CrittersBase {
+ private addedCspScriptsDocuments = new WeakSet();
+ private documentNonces = new WeakMap();
+
+ constructor(
+ public override readFile: (path: string) => Promise,
+ readonly outputPath?: string,
+ ) {
+ super({
+ logger: {
+ // eslint-disable-next-line no-console
+ warn: (s: string) => console.warn(s),
+ // eslint-disable-next-line no-console
+ error: (s: string) => console.error(s),
+ info: () => {},
+ },
+ logLevel: 'warn',
+ path: outputPath,
+ publicPath: undefined,
+ compress: false,
+ pruneSource: false,
+ reduceInlineStyles: false,
+ mergeStylesheets: false,
+ // Note: if `preload` changes to anything other than `media`, the logic in
+ // `embedLinkedStylesheet` will have to be updated.
+ preload: 'media',
+ noscriptFallback: true,
+ inlineFonts: true,
+ });
+ }
+
+ /**
+ * Override of the Critters `embedLinkedStylesheet` method
+ * that makes it work with Angular's CSP APIs.
+ */
+ override async embedLinkedStylesheet(
+ link: PartialHTMLElement,
+ document: PartialDocument,
+ ): Promise {
+ if (link.getAttribute('media') === 'print' && link.next?.name === 'noscript') {
+ // Workaround for https://github.com/GoogleChromeLabs/critters/issues/64
+ // NB: this is only needed for the webpack based builders.
+ const media = link.getAttribute('onload')?.match(MEDIA_SET_HANDLER_PATTERN);
+ if (media) {
+ link.removeAttribute('onload');
+ link.setAttribute('media', media[1]);
+ link?.next?.remove();
+ }
+ }
+
+ const returnValue = await super.embedLinkedStylesheet(link, document);
+ const cspNonce = this.findCspNonce(document);
+
+ if (cspNonce) {
+ const crittersMedia = link.getAttribute('onload')?.match(MEDIA_SET_HANDLER_PATTERN);
+
+ if (crittersMedia) {
+ // If there's a Critters-generated `onload` handler and the file has an Angular CSP nonce,
+ // we have to remove the handler, because it's incompatible with CSP. We save the value
+ // in a different attribute and we generate a script tag with the nonce that uses
+ // `addEventListener` to apply the media query instead.
+ link.removeAttribute('onload');
+ link.setAttribute(CSP_MEDIA_ATTR, crittersMedia[1]);
+ this.conditionallyInsertCspLoadingScript(document, cspNonce, link);
+ }
+
+ // Ideally we would hook in at the time Critters inserts the `style` tags, but there isn't
+ // a way of doing that at the moment so we fall back to doing it any time a `link` tag is
+ // inserted. We mitigate it by only iterating the direct children of the `` which
+ // should be pretty shallow.
+ document.head.children.forEach((child) => {
+ if (child.tagName === 'style' && !child.hasAttribute('nonce')) {
+ child.setAttribute('nonce', cspNonce);
+ }
+ });
+ }
+
+ return returnValue;
+ }
+
+ /**
+ * Finds the CSP nonce for a specific document.
+ */
+ private findCspNonce(document: PartialDocument): string | null {
+ if (this.documentNonces.has(document)) {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ return this.documentNonces.get(document)!;
+ }
+
+ // HTML attribute are case-insensitive, but the parser used by Critters is case-sensitive.
+ const nonceElement = document.querySelector('[ngCspNonce], [ngcspnonce]');
+ const cspNonce =
+ nonceElement?.getAttribute('ngCspNonce') || nonceElement?.getAttribute('ngcspnonce') || null;
+
+ this.documentNonces.set(document, cspNonce);
+
+ return cspNonce;
+ }
+
+ /**
+ * Inserts the `script` tag that swaps the critical CSS at runtime,
+ * if one hasn't been inserted into the document already.
+ */
+ private conditionallyInsertCspLoadingScript(
+ document: PartialDocument,
+ nonce: string,
+ link: PartialHTMLElement,
+ ): void {
+ if (this.addedCspScriptsDocuments.has(document)) {
+ return;
+ }
+
+ if (document.head.textContent.includes(LINK_LOAD_SCRIPT_CONTENT)) {
+ // Script was already added during the build.
+ this.addedCspScriptsDocuments.add(document);
+
+ return;
+ }
+
+ const script = document.createElement('script');
+ script.setAttribute('nonce', nonce);
+ script.textContent = LINK_LOAD_SCRIPT_CONTENT;
+ // Prepend the script to the head since it needs to
+ // run as early as possible, before the `link` tags.
+ document.head.insertBefore(script, link);
+ this.addedCspScriptsDocuments.add(document);
+ }
+}
diff --git a/packages/angular/ssr/test/BUILD.bazel b/packages/angular/ssr/test/BUILD.bazel
index abc2a4eefdb1..75adb017fb96 100644
--- a/packages/angular/ssr/test/BUILD.bazel
+++ b/packages/angular/ssr/test/BUILD.bazel
@@ -13,7 +13,7 @@ ts_library(
testonly = True,
srcs = glob(
include = ["**/*_spec.ts"],
- exclude = ESM_TESTS,
+ exclude = ESM_TESTS + ["npm_package/**"],
),
deps = [
"//packages/angular/ssr",
diff --git a/packages/angular/ssr/test/npm_package/BUILD.bazel b/packages/angular/ssr/test/npm_package/BUILD.bazel
new file mode 100644
index 000000000000..48a239493667
--- /dev/null
+++ b/packages/angular/ssr/test/npm_package/BUILD.bazel
@@ -0,0 +1,58 @@
+load("@bazel_skylib//rules:diff_test.bzl", "diff_test")
+load("@bazel_skylib//rules:write_file.bzl", "write_file")
+load("@npm//@bazel/jasmine:index.bzl", "jasmine_node_test")
+load("//tools:defaults.bzl", "ts_library")
+
+ts_library(
+ name = "unit_test_lib",
+ testonly = True,
+ srcs = glob(["**/*.ts"]),
+ deps = [
+ "@npm//@bazel/runfiles",
+ ],
+)
+
+jasmine_node_test(
+ name = "test",
+ srcs = [":unit_test_lib"],
+ data = [
+ "//packages/angular/ssr:npm_package",
+ ],
+)
+
+genrule(
+ name = "critters_license_file",
+ srcs = [
+ "//packages/angular/ssr:npm_package",
+ ],
+ outs = [
+ "THIRD_PARTY_LICENSES.txt",
+ ],
+ cmd = """
+ cp $(location //packages/angular/ssr:npm_package)/third_party/critters/THIRD_PARTY_LICENSES.txt $(location :THIRD_PARTY_LICENSES.txt)
+ """,
+)
+
+diff_test(
+ name = "critters_license_test",
+ failure_message = """
+
+ To accept the new golden file, execute:
+ yarn bazel run //packages/angular/ssr/test/npm_package:critters_license_test.accept
+ """,
+ file1 = ":THIRD_PARTY_LICENSES.txt.golden",
+ file2 = ":critters_license_file",
+)
+
+write_file(
+ name = "critters_license_test.accept",
+ out = "critters_license_file_accept.sh",
+ content =
+ [
+ "#!/usr/bin/env bash",
+ "cd ${BUILD_WORKSPACE_DIRECTORY}",
+ "yarn bazel build //packages/angular/ssr:npm_package",
+ "cp -fv dist/bin/packages/angular/ssr/npm_package/third_party/critters/THIRD_PARTY_LICENSES.txt packages/angular/ssr/test/npm_package/THIRD_PARTY_LICENSES.txt.golden",
+ ],
+ is_executable = True,
+)
diff --git a/packages/angular/ssr/test/npm_package/THIRD_PARTY_LICENSES.txt.golden b/packages/angular/ssr/test/npm_package/THIRD_PARTY_LICENSES.txt.golden
new file mode 100644
index 000000000000..067f2370fd4c
--- /dev/null
+++ b/packages/angular/ssr/test/npm_package/THIRD_PARTY_LICENSES.txt.golden
@@ -0,0 +1,350 @@
+
+--------------------------------------------------------------------------------
+Package: ansi-styles
+License: "MIT"
+
+
+--------------------------------------------------------------------------------
+Package: boolbase
+License: "ISC"
+
+
+--------------------------------------------------------------------------------
+Package: chalk
+License: "MIT"
+
+
+--------------------------------------------------------------------------------
+Package: color-convert
+License: "MIT"
+
+Copyright (c) 2011-2016 Heather Arthur
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+--------------------------------------------------------------------------------
+Package: color-name
+License: "MIT"
+
+The MIT License (MIT)
+Copyright (c) 2015 Dmitry Ivanov
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+--------------------------------------------------------------------------------
+Package: critters
+License: "Apache-2.0"
+
+
+--------------------------------------------------------------------------------
+Package: css-select
+License: "BSD-2-Clause"
+
+Copyright (c) Felix Böhm
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+
+Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+
+THIS IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+--------------------------------------------------------------------------------
+Package: css-what
+License: "BSD-2-Clause"
+
+Copyright (c) Felix Böhm
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+
+Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+
+THIS IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+--------------------------------------------------------------------------------
+Package: dom-serializer
+License: "MIT"
+
+License
+
+(The MIT License)
+
+Copyright (c) 2014 The cheeriojs contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+--------------------------------------------------------------------------------
+Package: domelementtype
+License: "BSD-2-Clause"
+
+Copyright (c) Felix Böhm
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+
+Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+
+THIS IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+--------------------------------------------------------------------------------
+Package: domhandler
+License: "BSD-2-Clause"
+
+Copyright (c) Felix Böhm
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+
+Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+
+THIS IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+--------------------------------------------------------------------------------
+Package: domutils
+License: "BSD-2-Clause"
+
+Copyright (c) Felix Böhm
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+
+Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+
+THIS IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+--------------------------------------------------------------------------------
+Package: entities
+License: "BSD-2-Clause"
+
+Copyright (c) Felix Böhm
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+
+Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+
+THIS IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+--------------------------------------------------------------------------------
+Package: htmlparser2
+License: "MIT"
+
+Copyright 2010, 2011, Chris Winberry . All rights reserved.
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to
+deal in the Software without restriction, including without limitation the
+rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+sell copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+IN THE SOFTWARE.
+--------------------------------------------------------------------------------
+Package: nanoid
+License: "MIT"
+
+The MIT License (MIT)
+
+Copyright 2017 Andrey Sitnik
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+--------------------------------------------------------------------------------
+Package: nth-check
+License: "BSD-2-Clause"
+
+Copyright (c) Felix Böhm
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+
+Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+
+THIS IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+--------------------------------------------------------------------------------
+Package: pathe
+License: "MIT"
+
+MIT License
+
+Copyright (c) Pooya Parsa - Daniel Roe
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+--------------------------------------------------------------------------------
+
+Copyright Joyent, Inc. and other Node contributors.
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to permit
+persons to whom the Software is furnished to do so, subject to the
+following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+USE OR OTHER DEALINGS IN THE SOFTWARE.
+--------------------------------------------------------------------------------
+Package: picocolors
+License: "ISC"
+
+ISC License
+
+Copyright (c) 2021 Alexey Raspopov, Kostiantyn Denysov, Anton Verinov
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+--------------------------------------------------------------------------------
+Package: postcss
+License: "MIT"
+
+The MIT License (MIT)
+
+Copyright 2013 Andrey Sitnik
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+--------------------------------------------------------------------------------
+Package: postcss-media-query-parser
+License: "MIT"
+
+
+--------------------------------------------------------------------------------
+Package: supports-color
+License: "MIT"
+
+
+--------------------------------------------------------------------------------
+Package: unenv
+License: "MIT"
+
+MIT License
+
+Copyright (c) Pooya Parsa
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+--------------------------------------------------------------------------------
diff --git a/packages/angular/ssr/test/npm_package/package_spec.ts b/packages/angular/ssr/test/npm_package/package_spec.ts
new file mode 100644
index 000000000000..c4f9fad7f402
--- /dev/null
+++ b/packages/angular/ssr/test/npm_package/package_spec.ts
@@ -0,0 +1,48 @@
+/**
+ * @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 { runfiles } from '@bazel/runfiles';
+import { existsSync } from 'node:fs';
+import { readFile } from 'node:fs/promises';
+import { dirname, join } from 'node:path';
+
+/**
+ * Resolve paths for the Critters license file and the golden reference file.
+ */
+const ANGULAR_SSR_PACKAGE_PATH = dirname(
+ runfiles.resolve('angular_cli/packages/angular/ssr/npm_package/package.json'),
+);
+
+/**
+ * Path to the actual license file for the Critters library.
+ * This file is located in the `third_party/critters` directory of the Angular CLI npm package.
+ */
+const CRITTERS_ACTUAL_LICENSE_FILE_PATH = join(
+ ANGULAR_SSR_PACKAGE_PATH,
+ 'third_party/critters/THIRD_PARTY_LICENSES.txt',
+);
+
+/**
+ * Path to the golden reference license file for the Critters library.
+ * This file is used as a reference for comparison and is located in the same directory as this script.
+ */
+const CRITTERS_GOLDEN_LICENSE_FILE_PATH = join(__dirname, 'THIRD_PARTY_LICENSES.txt.golden');
+
+describe('NPM Package Tests', () => {
+ it('should not include the contents of third_party/critters/index.js in the FESM bundle', async () => {
+ const fesmFilePath = join(ANGULAR_SSR_PACKAGE_PATH, 'fesm2022/ssr.mjs');
+ const fesmContent = await readFile(fesmFilePath, 'utf-8');
+ expect(fesmContent).toContain(`import Critters from '../third_party/critters/index.js'`);
+ });
+
+ describe('third_party/critters/THIRD_PARTY_LICENSES.txt', () => {
+ it('should exist', () => {
+ expect(existsSync(CRITTERS_ACTUAL_LICENSE_FILE_PATH)).toBe(true);
+ });
+ });
+});
diff --git a/packages/angular/ssr/third_party/critters/BUILD.bazel b/packages/angular/ssr/third_party/critters/BUILD.bazel
new file mode 100644
index 000000000000..8dde77495c4e
--- /dev/null
+++ b/packages/angular/ssr/third_party/critters/BUILD.bazel
@@ -0,0 +1,55 @@
+load("@npm//@angular/build-tooling/bazel/esbuild:index.bzl", "esbuild", "esbuild_config")
+load("//tools:defaults.bzl", "js_library")
+
+package(default_visibility = ["//visibility:public"])
+
+esbuild(
+ name = "bundled_critters",
+ config = ":esbuild_config",
+ entry_point = "@npm//:node_modules/critters/dist/critters.mjs",
+ metafile = True,
+ splitting = True,
+ deps = [
+ "@npm//critters",
+ "@npm//unenv",
+ ],
+)
+
+esbuild_config(
+ name = "esbuild_config",
+ config_file = "esbuild.config.mjs",
+)
+
+js_library(
+ name = "bundled_critters_lib",
+ srcs = [
+ "index.d.ts",
+ ":bundled_critters_files",
+ ],
+ deps = [
+ "@npm//critters",
+ ],
+)
+
+# Filter out esbuild metadata files and only copy the necessary files
+genrule(
+ name = "bundled_critters_files",
+ srcs = [
+ ":bundled_critters",
+ ],
+ outs = [
+ "index.js",
+ "index.js.map",
+ "THIRD_PARTY_LICENSES.txt",
+ ],
+ cmd = """
+ for f in $(locations :bundled_critters); do
+ # Only process files inside the bundled_critters directory
+ if [[ "$${f}" == *bundled_critters ]]; then
+ cp "$${f}/index.js" $(location :index.js)
+ cp "$${f}/index.js.map" $(location :index.js.map)
+ cp "$${f}/THIRD_PARTY_LICENSES.txt" $(location :THIRD_PARTY_LICENSES.txt)
+ fi
+ done
+ """,
+)
diff --git a/packages/angular/ssr/third_party/critters/esbuild.config.mjs b/packages/angular/ssr/third_party/critters/esbuild.config.mjs
new file mode 100644
index 000000000000..31b367d33965
--- /dev/null
+++ b/packages/angular/ssr/third_party/critters/esbuild.config.mjs
@@ -0,0 +1,204 @@
+/**
+ * @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 { readFile, writeFile } from 'node:fs/promises';
+import { join, basename, dirname } from 'node:path';
+import { nodeless } from 'unenv';
+
+const { path, fs } = nodeless.alias;
+
+export default {
+ format: 'esm',
+ platform: 'browser',
+ entryNames: 'index',
+ legalComments: 'eof',
+ alias: {
+ fs,
+ path,
+ },
+ plugins: [
+ {
+ name: 'angular-license-extractor',
+ setup(build) {
+ build.onEnd(async (result) => {
+ const licenses = await extractLicenses(result.metafile);
+
+ return writeFile(join(build.initialOptions.outdir, 'THIRD_PARTY_LICENSES.txt'), licenses);
+ });
+ },
+ },
+ ],
+};
+
+// Note: The following content has been adapted from the Angular CLI license extractor.
+
+/**
+ * The path segment used to signify that a file is part of a package.
+ */
+const NODE_MODULE_SEGMENT = 'node_modules';
+
+/**
+ * String constant for the NPM recommended custom license wording.
+ *
+ * See: https://docs.npmjs.com/cli/v9/configuring-npm/package-json#license
+ *
+ * Example:
+ * ```
+ * {
+ * "license" : "SEE LICENSE IN "
+ * }
+ * ```
+ */
+const CUSTOM_LICENSE_TEXT = 'SEE LICENSE IN ';
+
+/**
+ * A list of commonly named license files found within packages.
+ */
+const LICENSE_FILES = ['LICENSE', 'LICENSE.txt', 'LICENSE.md'];
+
+/**
+ * Header text that will be added to the top of the output license extraction file.
+ */
+const EXTRACTION_FILE_HEADER = '';
+
+/**
+ * The package entry separator to use within the output license extraction file.
+ */
+const EXTRACTION_FILE_SEPARATOR = '-'.repeat(80) + '\n';
+
+/**
+ * Extracts license information for each node module package included in the output
+ * files of the built code. This includes JavaScript and CSS output files. The esbuild
+ * metafile generated during the bundling steps is used as the source of information
+ * regarding what input files where included and where they are located. A path segment
+ * of `node_modules` is used to indicate that a file belongs to a package and its license
+ * should be include in the output licenses file.
+ *
+ * The package name and license field are extracted from the `package.json` file for the
+ * package. If a license file (e.g., `LICENSE`) is present in the root of the package, it
+ * will also be included in the output licenses file.
+ *
+ * @param metafile An esbuild metafile object.
+ * @returns A string containing the content of the output licenses file.
+ */
+async function extractLicenses(metafile) {
+ const seenPaths = new Set();
+ const seenPackages = new Set();
+ const extractedLicenses = {};
+
+ for (const entry of Object.values(metafile.outputs)) {
+ for (const [inputPath, { bytesInOutput }] of Object.entries(entry.inputs)) {
+ // Skip if not included in output
+ if (bytesInOutput <= 0) {
+ continue;
+ }
+
+ // Skip already processed paths
+ if (seenPaths.has(inputPath)) {
+ continue;
+ }
+ seenPaths.add(inputPath);
+
+ // Skip non-package paths
+ if (!inputPath.includes(NODE_MODULE_SEGMENT)) {
+ continue;
+ }
+
+ // Extract the package name from the path
+ let baseDirectory = join(process.cwd(), inputPath);
+ let nameOrScope, nameOrFile;
+ let found = false;
+ while (baseDirectory !== dirname(baseDirectory)) {
+ const segment = basename(baseDirectory);
+ if (segment === NODE_MODULE_SEGMENT) {
+ found = true;
+ break;
+ }
+
+ nameOrFile = nameOrScope;
+ nameOrScope = segment;
+ baseDirectory = dirname(baseDirectory);
+ }
+
+ // Skip non-package path edge cases that are not caught in the includes check above
+ if (!found || !nameOrScope) {
+ continue;
+ }
+
+ const packageName = nameOrScope.startsWith('@')
+ ? `${nameOrScope}/${nameOrFile}`
+ : nameOrScope;
+ const packageDirectory = join(baseDirectory, packageName);
+
+ // Load the package's metadata to find the package's name, version, and license type
+ const packageJsonPath = join(packageDirectory, 'package.json');
+ let packageJson;
+ try {
+ packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8'));
+ } catch {
+ // Invalid package
+ continue;
+ }
+
+ // Skip already processed packages
+ const packageId = `${packageName}@${packageJson.version}`;
+ if (seenPackages.has(packageId)) {
+ continue;
+ }
+ seenPackages.add(packageId);
+
+ // Attempt to find license text inside package
+ let licenseText = '';
+ if (
+ typeof packageJson.license === 'string' &&
+ packageJson.license.toLowerCase().startsWith(CUSTOM_LICENSE_TEXT)
+ ) {
+ // Attempt to load the package's custom license
+ let customLicensePath;
+ const customLicenseFile = normalize(
+ packageJson.license.slice(CUSTOM_LICENSE_TEXT.length + 1).trim(),
+ );
+ if (customLicenseFile.startsWith('..') || isAbsolute(customLicenseFile)) {
+ // Path is attempting to access files outside of the package
+ // TODO: Issue warning?
+ } else {
+ customLicensePath = join(packageDirectory, customLicenseFile);
+ try {
+ licenseText = await readFile(customLicensePath, 'utf-8');
+ break;
+ } catch {}
+ }
+ } else {
+ // Search for a license file within the root of the package
+ for (const potentialLicense of LICENSE_FILES) {
+ const packageLicensePath = join(packageDirectory, potentialLicense);
+ try {
+ licenseText = await readFile(packageLicensePath, 'utf-8');
+ break;
+ } catch {}
+ }
+ }
+
+ // Generate the package's license entry in the output content
+ let extractedLicenseContent = `Package: ${packageJson.name}\n`;
+ extractedLicenseContent += `License: ${JSON.stringify(packageJson.license, undefined, 2)}\n`;
+ extractedLicenseContent += `\n${licenseText.trim().replace(/\r?\n/g, '\n')}\n`;
+ extractedLicenseContent += EXTRACTION_FILE_SEPARATOR;
+
+ extractedLicenses[packageJson.name] = extractedLicenseContent;
+ }
+ }
+
+ // Get the keys of the object and sort them and etract and join the values corresponding to the sorted keys
+ const joinedLicenseContent = Object.keys(extractedLicenses)
+ .sort()
+ .map((pkgName) => extractedLicenses[pkgName])
+ .join('');
+
+ return `${EXTRACTION_FILE_HEADER}\n${EXTRACTION_FILE_SEPARATOR}${joinedLicenseContent}`;
+}
diff --git a/packages/angular/ssr/third_party/critters/index.d.ts b/packages/angular/ssr/third_party/critters/index.d.ts
new file mode 100644
index 000000000000..2d7efd309d20
--- /dev/null
+++ b/packages/angular/ssr/third_party/critters/index.d.ts
@@ -0,0 +1,9 @@
+/**
+ * @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
+ */
+
+export { default } from 'critters';
diff --git a/scripts/build.mts b/scripts/build.mts
index 828d73a822f6..6c5312759df6 100644
--- a/scripts/build.mts
+++ b/scripts/build.mts
@@ -95,9 +95,7 @@ async function _build(logger: Console, mode: BuildMode): Promise {
logger.group(`Building (mode=${mode})...`);
logger.group('Finding targets...');
- const queryTargetsCmd =
- `${bazelCmd} query --output=label "attr(name, npm_package_archive, //packages/...` +
- ' except //packages/angular/ssr/schematics/...)"';
+ const queryTargetsCmd = `${bazelCmd} query --output=label "attr(name, npm_package_archive, //packages/...)"`;
const targets = (await _exec(queryTargetsCmd, true, logger)).split(/\r?\n/);
logger.groupEnd();
diff --git a/tools/defaults.bzl b/tools/defaults.bzl
index 20e20d699b13..5b385d8b7614 100644
--- a/tools/defaults.bzl
+++ b/tools/defaults.bzl
@@ -24,6 +24,8 @@ NPM_PACKAGE_SUBSTITUTIONS = {
"0.0.0-ENGINES-NODE": RELEASE_ENGINES_NODE,
"0.0.0-ENGINES-NPM": RELEASE_ENGINES_NPM,
"0.0.0-ENGINES-YARN": RELEASE_ENGINES_YARN,
+ # The below is needed for @angular/ssr FESM file.
+ "\\./(.+)/packages/angular/ssr/third_party/critters": "../third_party/critters/index.js",
}
NO_STAMP_PACKAGE_SUBSTITUTIONS = dict(NPM_PACKAGE_SUBSTITUTIONS, **{
@@ -218,13 +220,14 @@ def pkg_npm(name, pkg_deps = [], use_prodmode_output = False, **kwargs):
**kwargs
)
- pkg_tar(
- name = name + "_archive",
- srcs = [":%s" % name],
- extension = "tgz",
- strip_prefix = "./%s" % name,
- visibility = visibility,
- )
+ if pkg_json:
+ pkg_tar(
+ name = name + "_archive",
+ srcs = [":%s" % name],
+ extension = "tgz",
+ strip_prefix = "./%s" % name,
+ visibility = visibility,
+ )
def ng_module(name, tsconfig = None, entry_point = None, testonly = False, deps = [], module_name = None, package_name = None, **kwargs):
"""Default values for ng_module"""
@@ -268,12 +271,6 @@ def ng_module(name, tsconfig = None, entry_point = None, testonly = False, deps
def ng_package(deps = [], **kwargs):
_ng_package(
deps = deps,
- externals = [
- "xhr2",
- "critters",
- "express-engine",
- "express",
- ],
substitutions = select({
"//:stamp": NPM_PACKAGE_SUBSTITUTIONS,
"//conditions:default": NO_STAMP_PACKAGE_SUBSTITUTIONS,
diff --git a/yarn.lock b/yarn.lock
index 78d3301b868d..2f7f0984ca0e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -659,6 +659,7 @@ __metadata:
"@bazel/buildifier": "npm:7.1.2"
"@bazel/concatjs": "patch:@bazel/concatjs@npm%3A5.8.1#~/.yarn/patches/@bazel-concatjs-npm-5.8.1-1bf81df846.patch"
"@bazel/jasmine": "patch:@bazel/jasmine@npm%3A5.8.1#~/.yarn/patches/@bazel-jasmine-npm-5.8.1-3370fee155.patch"
+ "@bazel/runfiles": "npm:^5.8.1"
"@discoveryjs/json-ext": "npm:0.6.1"
"@inquirer/confirm": "npm:3.1.22"
"@inquirer/prompts": "npm:5.3.8"
@@ -775,6 +776,7 @@ __metadata:
tslib: "npm:2.6.3"
typescript: "npm:5.5.4"
undici: "npm:6.19.8"
+ unenv: "npm:^1.10.0"
verdaccio: "npm:5.32.1"
verdaccio-auth-memory: "npm:^10.0.0"
vite: "npm:5.4.2"
@@ -964,7 +966,7 @@ __metadata:
"@angular/platform-browser": "npm:19.0.0-next.1"
"@angular/platform-server": "npm:19.0.0-next.1"
"@angular/router": "npm:19.0.0-next.1"
- critters: "npm:0.0.24"
+ "@bazel/runfiles": "npm:^5.8.1"
tslib: "npm:^2.3.0"
zone.js: "npm:^0.15.0"
peerDependencies:
@@ -7923,6 +7925,13 @@ __metadata:
languageName: node
linkType: hard
+"consola@npm:^3.2.3":
+ version: 3.2.3
+ resolution: "consola@npm:3.2.3"
+ checksum: 10c0/c606220524ec88a05bb1baf557e9e0e04a0c08a9c35d7a08652d99de195c4ddcb6572040a7df57a18ff38bbc13ce9880ad032d56630cef27bef72768ef0ac078
+ languageName: node
+ linkType: hard
+
"content-disposition@npm:0.5.4, content-disposition@npm:~0.5.2":
version: 0.5.4
resolution: "content-disposition@npm:0.5.4"
@@ -8402,6 +8411,13 @@ __metadata:
languageName: node
linkType: hard
+"defu@npm:^6.1.4":
+ version: 6.1.4
+ resolution: "defu@npm:6.1.4"
+ checksum: 10c0/2d6cc366262dc0cb8096e429368e44052fdf43ed48e53ad84cc7c9407f890301aa5fcb80d0995abaaf842b3949f154d060be4160f7a46cb2bc2f7726c81526f5
+ languageName: node
+ linkType: hard
+
"degenerator@npm:^5.0.0":
version: 5.0.1
resolution: "degenerator@npm:5.0.1"
@@ -12974,7 +12990,7 @@ __metadata:
languageName: node
linkType: hard
-"mime@npm:3.0.0":
+"mime@npm:3.0.0, mime@npm:^3.0.0":
version: 3.0.0
resolution: "mime@npm:3.0.0"
bin:
@@ -13437,6 +13453,13 @@ __metadata:
languageName: node
linkType: hard
+"node-fetch-native@npm:^1.6.4":
+ version: 1.6.4
+ resolution: "node-fetch-native@npm:1.6.4"
+ checksum: 10c0/78334dc6def5d1d95cfe87b33ac76c4833592c5eb84779ad2b0c23c689f9dd5d1cfc827035ada72d6b8b218f717798968c5a99aeff0a1a8bf06657e80592f9c3
+ languageName: node
+ linkType: hard
+
"node-fetch@npm:*":
version: 3.3.2
resolution: "node-fetch@npm:3.3.2"
@@ -14331,6 +14354,13 @@ __metadata:
languageName: node
linkType: hard
+"pathe@npm:^1.1.2":
+ version: 1.1.2
+ resolution: "pathe@npm:1.1.2"
+ checksum: 10c0/64ee0a4e587fb0f208d9777a6c56e4f9050039268faaaaecd50e959ef01bf847b7872785c36483fa5cdcdbdfdb31fef2ff222684d4fc21c330ab60395c681897
+ languageName: node
+ linkType: hard
+
"peek-stream@npm:^1.1.0":
version: 1.1.3
resolution: "peek-stream@npm:1.1.3"
@@ -17451,6 +17481,19 @@ __metadata:
languageName: node
linkType: hard
+"unenv@npm:^1.10.0":
+ version: 1.10.0
+ resolution: "unenv@npm:1.10.0"
+ dependencies:
+ consola: "npm:^3.2.3"
+ defu: "npm:^6.1.4"
+ mime: "npm:^3.0.0"
+ node-fetch-native: "npm:^1.6.4"
+ pathe: "npm:^1.1.2"
+ checksum: 10c0/354180647e21204b6c303339e7364b920baadb2672b540a88af267bc827636593e0bf79f59753dcc6b7ab5d4c83e71d69a9171a3596befb8bf77e0bb3c7612b9
+ languageName: node
+ linkType: hard
+
"unicode-canonical-property-names-ecmascript@npm:^2.0.0":
version: 2.0.0
resolution: "unicode-canonical-property-names-ecmascript@npm:2.0.0"