From 2f7b2625fd310a2f385bc4f94fc69a84bbeb2ac1 Mon Sep 17 00:00:00 2001 From: Ilia Mirkin Date: Tue, 27 May 2025 23:38:53 -0400 Subject: [PATCH] feat(@angular/build): add dataurl, base64 loaders Pass through the existing esbuild functionality to support dataurl and base64 loaders, in addition to the pre-existing text, binary, and file loaders. Both the dataurl and base64 loaders return strings with the relevant content. There is no way to control whether the dataurl method uses base64 or plain text encoding, and no way to control the mime type it detects. Fixes #30391 --- .../build/src/builders/application/options.ts | 13 ++++- .../src/builders/application/schema.json | 4 +- .../behavior/loader-import-attribute_spec.ts | 35 +++++++++++++ .../application/tests/options/loader_spec.ts | 49 +++++++++++++++++++ .../build/src/builders/karma/schema.json | 4 +- .../esbuild/loader-import-attribute-plugin.ts | 2 +- 6 files changed, 100 insertions(+), 7 deletions(-) diff --git a/packages/angular/build/src/builders/application/options.ts b/packages/angular/build/src/builders/application/options.ts index c3b13699a06e..1b3a15b8cd56 100644 --- a/packages/angular/build/src/builders/application/options.ts +++ b/packages/angular/build/src/builders/application/options.ts @@ -206,13 +206,22 @@ export async function normalizeOptions( } } - let loaderExtensions: Record | undefined; + let loaderExtensions: + | Record + | undefined; if (options.loader) { for (const [extension, value] of Object.entries(options.loader)) { if (extension[0] !== '.' || /\.[cm]?[jt]sx?$/.test(extension)) { continue; } - if (value !== 'text' && value !== 'binary' && value !== 'file' && value !== 'empty') { + if ( + value !== 'text' && + value !== 'binary' && + value !== 'file' && + value !== 'dataurl' && + value !== 'base64' && + value !== 'empty' + ) { continue; } loaderExtensions ??= {}; diff --git a/packages/angular/build/src/builders/application/schema.json b/packages/angular/build/src/builders/application/schema.json index ef4cbb75b82a..3ee8699e097f 100644 --- a/packages/angular/build/src/builders/application/schema.json +++ b/packages/angular/build/src/builders/application/schema.json @@ -279,10 +279,10 @@ ] }, "loader": { - "description": "Defines the type of loader to use with a specified file extension when used with a JavaScript `import`. `text` inlines the content as a string; `binary` inlines the content as a Uint8Array; `file` emits the file and provides the runtime location of the file; `empty` considers the content to be empty and not include it in bundles.", + "description": "Defines the type of loader to use with a specified file extension when used with a JavaScript `import`. `text` inlines the content as a string; `binary` inlines the content as a Uint8Array; `file` emits the file and provides the runtime location of the file; `dataurl` inlines the content as a data URL with best guess of MIME type; `base64` inlines the content as a Base64-encoded string; `empty` considers the content to be empty and not include it in bundles.", "type": "object", "patternProperties": { - "^\\.\\S+$": { "enum": ["text", "binary", "file", "empty"] } + "^\\.\\S+$": { "enum": ["text", "binary", "file", "dataurl", "base64", "empty"] } } }, "define": { diff --git a/packages/angular/build/src/builders/application/tests/behavior/loader-import-attribute_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/loader-import-attribute_spec.ts index 2f360047b278..91c4cafc571a 100644 --- a/packages/angular/build/src/builders/application/tests/behavior/loader-import-attribute_spec.ts +++ b/packages/angular/build/src/builders/application/tests/behavior/loader-import-attribute_spec.ts @@ -51,6 +51,41 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { harness.expectFile('dist/browser/main.js').content.not.toContain('ABC'); }); + it('should inline base64 content for file extension set to "base64"', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + await harness.writeFile('./src/a.unknown', 'ABC'); + await harness.writeFile( + 'src/main.ts', + '// @ts-expect-error\nimport contents from "./a.unknown" with { loader: "base64" };\n console.log(contents);', + ); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBe(true); + // Should contain the base64 encoding used esbuild and not the text content + harness.expectFile('dist/browser/main.js').content.toContain('QUJD'); + harness.expectFile('dist/browser/main.js').content.not.toContain('ABC'); + }); + + it('should inline dataurl content for file extension set to "dataurl"', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + await harness.writeFile('./src/a.svg', 'ABC'); + await harness.writeFile( + 'src/main.ts', + '// @ts-expect-error\nimport contents from "./a.svg" with { loader: "dataurl" };\n console.log(contents);', + ); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBe(true); + // Should contain the dataurl encoding used esbuild and not the text content + harness.expectFile('dist/browser/main.js').content.toContain('data:image/svg+xml,ABC'); + }); + it('should emit an output file for loader attribute set to "file"', async () => { harness.useTarget('build', { ...BASE_OPTIONS, diff --git a/packages/angular/build/src/builders/application/tests/options/loader_spec.ts b/packages/angular/build/src/builders/application/tests/options/loader_spec.ts index d7a6858d6e4b..2945df7bb4eb 100644 --- a/packages/angular/build/src/builders/application/tests/options/loader_spec.ts +++ b/packages/angular/build/src/builders/application/tests/options/loader_spec.ts @@ -108,6 +108,55 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { harness.expectFile('dist/browser/main.js').content.not.toContain('ABC'); }); + it('should inline base64 content for file extension set to "base64"', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + loader: { + '.unknown': 'base64', + }, + }); + + await harness.writeFile( + './src/types.d.ts', + 'declare module "*.unknown" { const content: string; export default content; }', + ); + await harness.writeFile('./src/a.unknown', 'ABC'); + await harness.writeFile( + 'src/main.ts', + 'import contents from "./a.unknown";\n console.log(contents);', + ); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBe(true); + // Should contain the base64 encoding used esbuild and not the text content + harness.expectFile('dist/browser/main.js').content.toContain('QUJD'); + harness.expectFile('dist/browser/main.js').content.not.toContain('ABC'); + }); + + it('should inline dataurl content for file extension set to "dataurl"', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + loader: { + '.svg': 'dataurl', + }, + }); + + await harness.writeFile( + './src/types.d.ts', + 'declare module "*.svg" { const content: string; export default content; }', + ); + await harness.writeFile('./src/a.svg', 'ABC'); + await harness.writeFile( + 'src/main.ts', + 'import contents from "./a.svg";\n console.log(contents);', + ); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBe(true); + // Should contain the dataurl encoding used esbuild and not the text content + harness.expectFile('dist/browser/main.js').content.toContain('data:image/svg+xml,ABC'); + }); + it('should emit an output file for file extension set to "file"', async () => { harness.useTarget('build', { ...BASE_OPTIONS, diff --git a/packages/angular/build/src/builders/karma/schema.json b/packages/angular/build/src/builders/karma/schema.json index cf7125217846..d24313ba29a6 100644 --- a/packages/angular/build/src/builders/karma/schema.json +++ b/packages/angular/build/src/builders/karma/schema.json @@ -163,10 +163,10 @@ "default": [] }, "loader": { - "description": "Defines the type of loader to use with a specified file extension when used with a JavaScript `import`. `text` inlines the content as a string; `binary` inlines the content as a Uint8Array; `file` emits the file and provides the runtime location of the file; `empty` considers the content to be empty and not include it in bundles.", + "description": "Defines the type of loader to use with a specified file extension when used with a JavaScript `import`. `text` inlines the content as a string; `binary` inlines the content as a Uint8Array; `file` emits the file and provides the runtime location of the file; `dataurl` inlines the content as a data URL with best guess of MIME type; `base64` inlines the content as a Base64-encoded string; `empty` considers the content to be empty and not include it in bundles.", "type": "object", "patternProperties": { - "^\\.\\S+$": { "enum": ["text", "binary", "file", "empty"] } + "^\\.\\S+$": { "enum": ["text", "binary", "file", "dataurl", "base64", "empty"] } } }, "define": { diff --git a/packages/angular/build/src/tools/esbuild/loader-import-attribute-plugin.ts b/packages/angular/build/src/tools/esbuild/loader-import-attribute-plugin.ts index 2856ad5c4100..8a9588b63a7b 100644 --- a/packages/angular/build/src/tools/esbuild/loader-import-attribute-plugin.ts +++ b/packages/angular/build/src/tools/esbuild/loader-import-attribute-plugin.ts @@ -9,7 +9,7 @@ import type { Loader, Plugin } from 'esbuild'; import { readFile } from 'node:fs/promises'; -const SUPPORTED_LOADERS: Loader[] = ['binary', 'file', 'text']; +const SUPPORTED_LOADERS: Loader[] = ['base64', 'binary', 'dataurl', 'file', 'text']; export function createLoaderImportAttributePlugin(): Plugin { return {