Skip to content

Commit f474bf5

Browse files
committed
fix(@angular-devkit/build-angular): process stylesheet resources from url tokens with esbuild browser builder
Stylesheet url tokens (`url(....)`) will now be processed when using the esbuild-based experimental browser application builder. The paths will be resolved via the bundler's resolution system and then loaded via the bundler's `file` loader. The functionality is implemented using an esbuild plugin to allow for all file types to be supported without the need to manually specify each current and future file extension within the build configuration. The `externalDependencies` option also applies to the referenced resources. This allows for resource paths specified with the option to remain unprocessed within the application output. This is useful if the relative path for the resource does not exist on disk but will be available when the application is deployed.
1 parent c0d1bec commit f474bf5

File tree

4 files changed

+98
-4
lines changed

4 files changed

+98
-4
lines changed

packages/angular_devkit/build_angular/src/builders/browser-esbuild/compiler-plugin.ts

+27-3
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,14 @@
99
import type { CompilerHost } from '@angular/compiler-cli';
1010
import { transformAsync } from '@babel/core';
1111
import * as assert from 'assert';
12-
import type { OnStartResult, PartialMessage, PartialNote, Plugin, PluginBuild } from 'esbuild';
12+
import type {
13+
OnStartResult,
14+
OutputFile,
15+
PartialMessage,
16+
PartialNote,
17+
Plugin,
18+
PluginBuild,
19+
} from 'esbuild';
1320
import { promises as fs } from 'fs';
1421
import * as path from 'path';
1522
import ts from 'typescript';
@@ -190,9 +197,15 @@ export function createCompilerPlugin(
190197
// The file emitter created during `onStart` that will be used during the build in `onLoad` callbacks for TS files
191198
let fileEmitter: FileEmitter | undefined;
192199

200+
// The stylesheet resources from component stylesheets that will be added to the build results output files
201+
let stylesheetResourceFiles: OutputFile[];
202+
193203
build.onStart(async () => {
194204
const result: OnStartResult = {};
195205

206+
// Reset stylesheet resource output files
207+
stylesheetResourceFiles = [];
208+
196209
// Create TypeScript compiler host
197210
const host = ts.createIncrementalCompilerHost(compilerOptions);
198211

@@ -205,10 +218,14 @@ export function createCompilerPlugin(
205218
return this.readFile(fileName) ?? '';
206219
}
207220

208-
const { contents, errors, warnings } = await bundleStylesheetFile(fileName, styleOptions);
221+
const { contents, resourceFiles, errors, warnings } = await bundleStylesheetFile(
222+
fileName,
223+
styleOptions,
224+
);
209225

210226
(result.errors ??= []).push(...errors);
211227
(result.warnings ??= []).push(...warnings);
228+
stylesheetResourceFiles.push(...resourceFiles);
212229

213230
return contents;
214231
};
@@ -224,7 +241,7 @@ export function createCompilerPlugin(
224241
// or the file containing the inline component style text (containingFile).
225242
const file = context.resourceFile ?? context.containingFile;
226243

227-
const { contents, errors, warnings } = await bundleStylesheetText(
244+
const { contents, resourceFiles, errors, warnings } = await bundleStylesheetText(
228245
data,
229246
{
230247
resolvePath: path.dirname(file),
@@ -235,6 +252,7 @@ export function createCompilerPlugin(
235252

236253
(result.errors ??= []).push(...errors);
237254
(result.warnings ??= []).push(...warnings);
255+
stylesheetResourceFiles.push(...resourceFiles);
238256

239257
return { content: contents };
240258
};
@@ -429,6 +447,12 @@ export function createCompilerPlugin(
429447
loader: 'js',
430448
};
431449
});
450+
451+
build.onEnd((result) => {
452+
if (stylesheetResourceFiles.length) {
453+
result.outputFiles?.push(...stylesheetResourceFiles);
454+
}
455+
});
432456
},
433457
};
434458
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import type { Plugin, PluginBuild } from 'esbuild';
10+
import { readFile } from 'fs/promises';
11+
12+
/**
13+
* Symbol marker used to indicate CSS resource resolution is being attempted.
14+
* This is used to prevent an infinite loop within the plugin's resolve hook.
15+
*/
16+
const CSS_RESOURCE_RESOLUTION = Symbol('CSS_RESOURCE_RESOLUTION');
17+
18+
/**
19+
* Creates an esbuild {@link Plugin} that loads all CSS url token references using the
20+
* built-in esbuild `file` loader. A plugin is used to allow for all file extensions
21+
* and types to be supported without needing to manually specify all extensions
22+
* within the build configuration.
23+
*
24+
* @returns An esbuild {@link Plugin} instance.
25+
*/
26+
export function createCssResourcePlugin(): Plugin {
27+
return {
28+
name: 'angular-css-resource',
29+
setup(build: PluginBuild): void {
30+
build.onResolve({ filter: /.*/ }, async (args) => {
31+
// Only attempt to resolve url tokens which only exist inside CSS.
32+
// Also, skip this plugin if already attempting to resolve the url-token.
33+
if (args.kind !== 'url-token' || args.pluginData?.[CSS_RESOURCE_RESOLUTION]) {
34+
return null;
35+
}
36+
37+
const { importer, kind, resolveDir, namespace, pluginData = {} } = args;
38+
pluginData[CSS_RESOURCE_RESOLUTION] = true;
39+
40+
const result = await build.resolve(args.path, {
41+
importer,
42+
kind,
43+
namespace,
44+
pluginData,
45+
resolveDir,
46+
});
47+
48+
return {
49+
...result,
50+
namespace: 'css-resource',
51+
};
52+
});
53+
54+
build.onLoad({ filter: /.*/, namespace: 'css-resource' }, async (args) => {
55+
return {
56+
contents: await readFile(args.path),
57+
loader: 'file',
58+
};
59+
});
60+
},
61+
};
62+
}

packages/angular_devkit/build_angular/src/builders/browser-esbuild/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ export async function buildEsbuildBrowser(
168168
outputNames: noInjectNames.includes(name) ? { media: outputNames.media } : outputNames,
169169
includePaths: options.stylePreprocessorOptions?.includePaths,
170170
preserveSymlinks: options.preserveSymlinks,
171+
externalDependencies: options.externalDependencies,
171172
},
172173
);
173174

@@ -354,6 +355,7 @@ async function bundleCode(
354355
!!sourcemapOptions.styles && (sourcemapOptions.hidden ? false : 'inline'),
355356
outputNames,
356357
includePaths: options.stylePreprocessorOptions?.includePaths,
358+
externalDependencies: options.externalDependencies,
357359
},
358360
),
359361
],

packages/angular_devkit/build_angular/src/builders/browser-esbuild/stylesheets.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import type { BuildOptions, OutputFile } from 'esbuild';
1010
import * as path from 'path';
11+
import { createCssResourcePlugin } from './css-resource-plugin';
1112
import { bundle } from './esbuild';
1213
import { createSassPlugin } from './sass-plugin';
1314

@@ -18,6 +19,7 @@ export interface BundleStylesheetOptions {
1819
sourcemap: boolean | 'external' | 'inline';
1920
outputNames?: { bundles?: string; media?: string };
2021
includePaths?: string[];
22+
externalDependencies?: string[];
2123
}
2224

2325
async function bundleStylesheet(
@@ -42,9 +44,13 @@ async function bundleStylesheet(
4244
write: false,
4345
platform: 'browser',
4446
preserveSymlinks: options.preserveSymlinks,
47+
external: options.externalDependencies,
4548
conditions: ['style', 'sass'],
4649
mainFields: ['style', 'sass'],
47-
plugins: [createSassPlugin({ sourcemap: !!options.sourcemap, loadPaths })],
50+
plugins: [
51+
createSassPlugin({ sourcemap: !!options.sourcemap, loadPaths }),
52+
createCssResourcePlugin(),
53+
],
4854
});
4955

5056
// Extract the result of the bundling from the output files

0 commit comments

Comments
 (0)