diff --git a/packages/angular/build/src/builders/application/execute-build.ts b/packages/angular/build/src/builders/application/execute-build.ts index 51351c3aa3..d07e2a7937 100644 --- a/packages/angular/build/src/builders/application/execute-build.ts +++ b/packages/angular/build/src/builders/application/execute-build.ts @@ -240,11 +240,6 @@ export async function executeBuild( ); } - // Override auto-CSP settings if we are serving through Vite middleware. - if (context.builder.builderName === 'dev-server' && options.security) { - options.security.autoCsp = false; - } - // Perform i18n translation inlining if enabled if (i18nOptions.shouldInline) { const result = await inlineI18n(metafile, options, executionResult, initialFiles); diff --git a/packages/angular/build/src/builders/application/options.ts b/packages/angular/build/src/builders/application/options.ts index 17d5eb65a1..2c11e57aae 100644 --- a/packages/angular/build/src/builders/application/options.ts +++ b/packages/angular/build/src/builders/application/options.ts @@ -386,6 +386,15 @@ export async function normalizeOptions( } } + const autoCsp = options.security?.autoCsp; + const security = { + autoCsp: autoCsp + ? { + unsafeEval: autoCsp === true ? false : !!autoCsp.unsafeEval, + } + : undefined, + }; + // Initial options to keep const { allowedCommonJsDependencies, @@ -415,7 +424,6 @@ export async function normalizeOptions( partialSSRBuild = false, externalRuntimeStyles, instrumentForCoverage, - security, } = options; // Return all the normalized options diff --git a/packages/angular/build/src/builders/dev-server/vite-server.ts b/packages/angular/build/src/builders/dev-server/vite-server.ts index 6773e8945c..4ca9925213 100644 --- a/packages/angular/build/src/builders/dev-server/vite-server.ts +++ b/packages/angular/build/src/builders/dev-server/vite-server.ts @@ -112,6 +112,11 @@ export async function* serveWithVite( browserOptions.ssr ||= true; } + // Disable auto CSP. + browserOptions.security = { + autoCsp: false, + }; + // Set all packages as external to support Vite's prebundle caching browserOptions.externalPackages = serverOptions.prebundle; diff --git a/packages/angular/build/src/tools/esbuild/index-html-generator.ts b/packages/angular/build/src/tools/esbuild/index-html-generator.ts index 4d11ed4fa4..4ccd48f2b5 100644 --- a/packages/angular/build/src/tools/esbuild/index-html-generator.ts +++ b/packages/angular/build/src/tools/esbuild/index-html-generator.ts @@ -80,15 +80,6 @@ export async function generateIndexHtml( throw new Error(`Output file does not exist: ${relativefilePath}`); }; - // Read the Auto CSP options. - const autoCsp = buildOptions.security?.autoCsp; - const autoCspOptions = - autoCsp === true - ? { unsafeEval: false } - : autoCsp - ? { unsafeEval: !!autoCsp.unsafeEval } - : undefined; - // Create an index HTML generator that reads from the in-memory output files const indexHtmlGenerator = new IndexHtmlGenerator({ indexPath: indexHtmlOptions.input, @@ -103,7 +94,7 @@ export async function generateIndexHtml( buildOptions.prerenderOptions || buildOptions.appShellOptions ), - autoCsp: autoCspOptions, + autoCsp: buildOptions.security.autoCsp, }); indexHtmlGenerator.readAsset = readAsset; diff --git a/packages/angular/build/src/utils/index-file/auto-csp.ts b/packages/angular/build/src/utils/index-file/auto-csp.ts index 8a5e339a9a..c50e0bfce3 100644 --- a/packages/angular/build/src/utils/index-file/auto-csp.ts +++ b/packages/angular/build/src/utils/index-file/auto-csp.ts @@ -98,7 +98,7 @@ export async function autoCsp(html: string, unsafeEval = false): Promise scriptContent = []; } - rewriter.on('startTag', (tag, html) => { + rewriter.on('startTag', (tag) => { if (tag.tagName === 'script') { openedScriptTag = tag; const src = getScriptAttributeValue(tag, 'src'); diff --git a/packages/angular/build/src/utils/index-file/index-html-generator.ts b/packages/angular/build/src/utils/index-file/index-html-generator.ts index 9bfb929c5d..52a926ef58 100644 --- a/packages/angular/build/src/utils/index-file/index-html-generator.ts +++ b/packages/angular/build/src/utils/index-file/index-html-generator.ts @@ -82,7 +82,7 @@ export class IndexHtmlGenerator { // CSR plugins if (options?.optimization?.styles?.inlineCritical) { - this.csrPlugins.push(inlineCriticalCssPlugin(this)); + this.csrPlugins.push(inlineCriticalCssPlugin(this, !!options.autoCsp)); } this.csrPlugins.push(addNoncePlugin()); @@ -197,11 +197,15 @@ function inlineFontsPlugin({ options }: IndexHtmlGenerator): IndexHtmlGeneratorP return async (html) => inlineFontsProcessor.process(html); } -function inlineCriticalCssPlugin(generator: IndexHtmlGenerator): IndexHtmlGeneratorPlugin { +function inlineCriticalCssPlugin( + generator: IndexHtmlGenerator, + autoCsp: boolean, +): IndexHtmlGeneratorPlugin { const inlineCriticalCssProcessor = new InlineCriticalCssProcessor({ minify: generator.options.optimization?.styles.minify, deployUrl: generator.options.deployUrl, readAsset: (filePath) => generator.readAsset(filePath), + autoCsp, }); return async (html, options) => 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 47bee21456..e5106f39cf 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 @@ -68,6 +68,7 @@ export interface InlineCriticalCssProcessorOptions { minify?: boolean; deployUrl?: string; readAsset?: (path: string) => Promise; + autoCsp?: boolean; } /** Partial representation of an `HTMLElement`. */ @@ -163,7 +164,7 @@ class BeastiesExtended extends BeastiesBase { const returnValue = await super.embedLinkedStylesheet(link, document); const cspNonce = this.findCspNonce(document); - if (cspNonce) { + if (cspNonce || this.optionsExtended.autoCsp) { const beastiesMedia = link.getAttribute('onload')?.match(MEDIA_SET_HANDLER_PATTERN); if (beastiesMedia) { @@ -180,11 +181,13 @@ class BeastiesExtended extends BeastiesBase { // 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); - } - }); + if (cspNonce) { + document.head.children.forEach((child) => { + if (child.tagName === 'style' && !child.hasAttribute('nonce')) { + child.setAttribute('nonce', cspNonce); + } + }); + } } return returnValue; @@ -215,7 +218,7 @@ class BeastiesExtended extends BeastiesBase { */ private conditionallyInsertCspLoadingScript( document: PartialDocument, - nonce: string, + nonce: string | null, link: PartialHTMLElement, ): void { if (this.addedCspScriptsDocuments.has(document)) { @@ -223,8 +226,11 @@ class BeastiesExtended extends BeastiesBase { } const script = document.createElement('script'); - script.setAttribute('nonce', nonce); script.textContent = LINK_LOAD_SCRIPT_CONTENT; + if (nonce) { + script.setAttribute('nonce', nonce); + } + // Prepend the script to the head since it needs to // run as early as possible, before the `link` tags. document.head.insertBefore(script, link); diff --git a/packages/angular/build/src/utils/index-file/inline-critical-css_spec.ts b/packages/angular/build/src/utils/index-file/inline-critical-css_spec.ts index 4c68304cd9..86e38b39f8 100644 --- a/packages/angular/build/src/utils/index-file/inline-critical-css_spec.ts +++ b/packages/angular/build/src/utils/index-file/inline-critical-css_spec.ts @@ -106,6 +106,26 @@ describe('InlineCriticalCssProcessor', () => { expect(content).toContain(''); }); + it(`should process the inline 'onload' handlers if a 'autoCsp' is true`, async () => { + const inlineCssProcessor = new InlineCriticalCssProcessor({ + readAsset, + autoCsp: true, + }); + + const { content } = await inlineCssProcessor.process(getContent(''), { + outputPath: '/dist/', + }); + + expect(content).toContain( + '', + ); + expect(tags.stripIndents`${content}`).toContain(tags.stripIndents` + `); + }); + it('should process the inline `onload` handlers if a CSP nonce is specified', async () => { const inlineCssProcessor = new InlineCriticalCssProcessor({ readAsset, diff --git a/tests/legacy-cli/e2e/tests/build/auto-csp.ts b/tests/legacy-cli/e2e/tests/build/auto-csp.ts index 1ea1aa5641..6b975dd5a3 100644 --- a/tests/legacy-cli/e2e/tests/build/auto-csp.ts +++ b/tests/legacy-cli/e2e/tests/build/auto-csp.ts @@ -1,6 +1,6 @@ import assert from 'node:assert'; import { getGlobalVariable } from '../../utils/env'; -import { expectFileToMatch, writeMultipleFiles } from '../../utils/fs'; +import { expectFileToMatch, writeFile, writeMultipleFiles } from '../../utils/fs'; import { findFreePort } from '../../utils/network'; import { execAndWaitForOutputToMatch, ng } from '../../utils/process'; import { updateJsonFile } from '../../utils/project'; @@ -13,6 +13,9 @@ export default async function () { 'This test should not be called in the Webpack suite.', ); + // Add global css to trigger critical css inlining + await writeFile('src/styles.css', `body { color: green }`); + // Turn on auto-CSP await updateJsonFile('angular.json', (json) => { const build = json['projects']['test-project']['architect']['build']; @@ -54,7 +57,7 @@ export default async function () { - +