Skip to content

feat(@angular/build): add dataurl, base64 loaders #30422

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions packages/angular/build/src/builders/application/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,13 +206,22 @@ export async function normalizeOptions(
}
}

let loaderExtensions: Record<string, 'text' | 'binary' | 'file'> | undefined;
let loaderExtensions:
| Record<string, 'text' | 'binary' | 'file' | 'dataurl' | 'base64'>
| 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 ??= {};
Expand Down
4 changes: 2 additions & 2 deletions packages/angular/build/src/builders/application/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions packages/angular/build/src/builders/karma/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down