From e1412cb4d451a955e82128ea23f462e9b64f3679 Mon Sep 17 00:00:00 2001 From: nagahara-naoki <118710021+nagahara-naoki@users.noreply.github.com> Date: Sat, 14 Nov 2026 15:33:46 +0900 Subject: [PATCH 1/2] fix(@angular/build): restore font stylesheet caching Create the font cache directory before writing and validate cached stylesheets using the same hexadecimal hash format used when saving them. Add regression tests for cache reuse and failure handling. Fixes #34719 --- .../src/utils/index-file/inline-fonts.ts | 7 +- .../src/utils/index-file/inline-fonts_spec.ts | 92 +++++++++++++++++++ 2 files changed, 96 insertions(+), 3 deletions(-) diff --git a/packages/angular/build/src/utils/index-file/inline-fonts.ts b/packages/angular/build/src/utils/index-file/inline-fonts.ts index 52181200c885..9004317faf49 100644 --- a/packages/angular/build/src/utils/index-file/inline-fonts.ts +++ b/packages/angular/build/src/utils/index-file/inline-fonts.ts @@ -7,9 +7,9 @@ */ import { createHash } from 'node:crypto'; -import { readFile, rm, writeFile } from 'node:fs/promises'; +import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'; import { type Agent, get as httpsGet } from 'node:https'; -import { join } from 'node:path'; +import { dirname, join } from 'node:path'; import { NormalizedCachedOptions } from '../normalize-cache'; import { htmlRewritingStream } from './html-rewriting-stream'; @@ -181,7 +181,7 @@ export class InlineFontsProcessor { if (data.length > CONTENT_HASH_LENGTH) { const storedHash = data.slice(0, CONTENT_HASH_LENGTH); const content = data.slice(CONTENT_HASH_LENGTH); - const contentHash = createHash(CONTENT_HASH_ALGORITHM).update(content).digest('base64'); + const contentHash = createHash(CONTENT_HASH_ALGORITHM).update(content).digest('hex'); if (storedHash === contentHash) { // Return valid content return content; @@ -255,6 +255,7 @@ export class InlineFontsProcessor { if (cacheFile) { try { + await mkdir(dirname(cacheFile), { recursive: true }); const dataHash = createHash(CONTENT_HASH_ALGORITHM).update(data).digest('hex'); await writeFile(cacheFile, dataHash + data); } catch {} diff --git a/packages/angular/build/src/utils/index-file/inline-fonts_spec.ts b/packages/angular/build/src/utils/index-file/inline-fonts_spec.ts index 46511f853ecf..b2dea06b570a 100644 --- a/packages/angular/build/src/utils/index-file/inline-fonts_spec.ts +++ b/packages/angular/build/src/utils/index-file/inline-fonts_spec.ts @@ -6,9 +6,101 @@ * found in the LICENSE file at https://angular.dev/license */ +import assert from 'node:assert'; +import { createHash } from 'node:crypto'; +import { EventEmitter } from 'node:events'; +import { existsSync } from 'node:fs'; +import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'; +import type { ClientRequest, IncomingMessage } from 'node:http'; +import https from 'node:https'; +import { join } from 'node:path'; +import { Readable } from 'node:stream'; import { InlineFontsProcessor } from './inline-fonts'; describe('InlineFontsProcessor', () => { + describe('disk cache', () => { + const url = 'https://fonts.googleapis.com/css2?family=Roboto'; + const content = '@font-face { font-family: Roboto; src: url(roboto.woff2); }'; + let tempRoot: string; + let cache: { enabled: boolean; path: string; basePath: string }; + let cacheDirectory: string; + let cacheFile: string; + let getSpy: jasmine.Spy; + + beforeEach(async () => { + const baseTmpDir = process.env['TEST_TMPDIR']; + assert(baseTmpDir, 'TEST_TMPDIR is not set'); + tempRoot = await mkdtemp(join(baseTmpDir, 'angular-cli-font-cache-test-')); + cache = { enabled: true, path: join(tempRoot, 'cache'), basePath: tempRoot }; + cacheDirectory = join(cache.path, 'angular-build-fonts'); + cacheFile = join(cacheDirectory, createHash('sha256').update(url).digest('hex')); + + getSpy = spyOn(https, 'get').and.callFake((...args: unknown[]) => { + const callback = args.at(-1); + assert(typeof callback === 'function'); + const response = Readable.from([content]) as IncomingMessage; + response.statusCode = 200; + callback(response); + + return new EventEmitter() as ClientRequest; + }); + }); + + afterEach(async () => { + if (tempRoot) { + await rm(tempRoot, { recursive: true, force: true }); + } + }); + + it('should create the cache directory and save the stylesheet', async () => { + const processor = new InlineFontsProcessor({ cache }); + + expect(await processor.processURL(url)).toBe(content); + expect(await readdir(cacheDirectory)).toHaveSize(1); + expect(await readFile(cacheFile, 'utf8')).toBe( + createHash('sha256').update(content).digest('hex') + content, + ); + }); + + it('should reuse the stylesheet across processor instances without another request', async () => { + await mkdir(cacheDirectory, { recursive: true }); + expect(await new InlineFontsProcessor({ cache }).processURL(url)).toBe(content); + getSpy.calls.reset(); + getSpy.and.throwError('Unexpected font request'); + + expect(await new InlineFontsProcessor({ cache }).processURL(url)).toBe(content); + expect(getSpy).not.toHaveBeenCalled(); + expect(existsSync(cacheFile)).toBeTrue(); + }); + + it('should replace corrupted cache content with a downloaded stylesheet', async () => { + await mkdir(cacheDirectory, { recursive: true }); + await writeFile(cacheFile, '0'.repeat(64) + content); + + expect(await new InlineFontsProcessor({ cache }).processURL(url)).toBe(content); + expect(getSpy).toHaveBeenCalledTimes(1); + expect(await readFile(cacheFile, 'utf8')).toBe( + createHash('sha256').update(content).digest('hex') + content, + ); + }); + + it('should not cache stylesheets when the cache is disabled', async () => { + const processor = new InlineFontsProcessor({ cache: { ...cache, enabled: false } }); + + expect(await processor.processURL(url)).toBe(content); + expect(await processor.processURL(url)).toBe(content); + expect(getSpy).toHaveBeenCalledTimes(2); + expect(existsSync(cache.path)).toBeFalse(); + }); + + it('should still return the stylesheet when the cache cannot be written', async () => { + await writeFile(cache.path, 'not a directory'); + + expect(await new InlineFontsProcessor({ cache }).processURL(url)).toBe(content); + expect(getSpy).toHaveBeenCalledTimes(1); + }); + }); + describe('Google fonts', () => { const content = ` From 8b51597fd83cf4d64409108c236b7ddee852df05 Mon Sep 17 00:00:00 2001 From: nagahara-naoki <118710021+nagahara-naoki@users.noreply.github.com> Date: Sat, 14 Nov 2026 15:37:19 +0900 Subject: [PATCH 2/2] fixup! fix(@angular/build): restore font stylesheet caching --- packages/angular/build/src/utils/index-file/inline-fonts.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/angular/build/src/utils/index-file/inline-fonts.ts b/packages/angular/build/src/utils/index-file/inline-fonts.ts index 9004317faf49..387eb479cd60 100644 --- a/packages/angular/build/src/utils/index-file/inline-fonts.ts +++ b/packages/angular/build/src/utils/index-file/inline-fonts.ts @@ -9,7 +9,7 @@ import { createHash } from 'node:crypto'; import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'; import { type Agent, get as httpsGet } from 'node:https'; -import { dirname, join } from 'node:path'; +import { join } from 'node:path'; import { NormalizedCachedOptions } from '../normalize-cache'; import { htmlRewritingStream } from './html-rewriting-stream'; @@ -253,9 +253,9 @@ export class InlineFontsProcessor { ); }); - if (cacheFile) { + if (cacheFile && this.cachePath) { try { - await mkdir(dirname(cacheFile), { recursive: true }); + await mkdir(this.cachePath, { recursive: true }); const dataHash = createHash(CONTENT_HASH_ALGORITHM).update(data).digest('hex'); await writeFile(cacheFile, dataHash + data); } catch {}