Skip to content

Commit

Permalink
feat(@angular-devkit/build-angular): inline Google and Adobe fonts lo…
Browse files Browse the repository at this point in the history
…cated in stylesheets

`@import url()` to Google and Adobe fonts that are located in global and component CSS will now be inlined when using the esbuild based builders.

Input
```css
@import url(https://fonts.googleapis.com/css?family=Roboto:300,400,500);
```

Output
```css
/* latin */
@font-face {
  font-family: 'Roboto';
  font-style: normal;
  font-weight: 500;
  src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmEU9fBBc4AMP6lQ.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
```

Closes #23054
  • Loading branch information
alan-agius4 committed Dec 9, 2023
1 parent bf5fbdd commit f6e67df
Show file tree
Hide file tree
Showing 7 changed files with 231 additions and 24 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import { buildApplication } from '../../index';
import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup';

describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
describe('Option: "fonts.inline"', () => {
beforeEach(async () => {
await harness.modifyFile('/src/index.html', (content) =>
content.replace(
'<head>',
`<head><link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" rel="stylesheet">`,
),
);

await harness.writeFile(
'src/styles.css',
'@import url(https://fonts.googleapis.com/css?family=Roboto:300,400,500);',
);

await harness.writeFile(
'src/app/app.component.css',
'@import url(https://fonts.googleapis.com/css?family=Roboto:300,400,500);',
);
});

it(`should not inline fonts when fonts optimization is set to false`, async () => {
harness.useTarget('build', {
...BASE_OPTIONS,
optimization: {
scripts: true,
styles: true,
fonts: false,
},
styles: ['src/styles.css'],
});

const { result } = await harness.executeOnce();

expect(result?.success).toBeTrue();
for (const file of ['styles.css', 'index.html', 'main.js']) {
harness
.expectFile(`dist/browser/${file}`)
.content.toContain(`https://fonts.googleapis.com/css?family=Roboto:300,400,500`);
}
});

it(`should inline fonts when fonts optimization is unset`, async () => {
harness.useTarget('build', {
...BASE_OPTIONS,
optimization: {
scripts: true,
styles: true,
fonts: undefined,
},
styles: ['src/styles.css'],
});

const { result } = await harness.executeOnce();

expect(result?.success).toBeTrue();
for (const file of ['styles.css', 'index.html', 'main.js']) {
harness
.expectFile(`dist/browser/${file}`)
.content.not.toContain(`https://fonts.googleapis.com/css?family=Roboto:300,400,500`);
harness
.expectFile(`dist/browser/${file}`)
.content.toMatch(/@font-face{font-family:'?Roboto/);
}
});

it(`should inline fonts when fonts optimization is true`, async () => {
harness.useTarget('build', {
...BASE_OPTIONS,
optimization: {
scripts: true,
styles: true,
fonts: true,
},
styles: ['src/styles.css'],
});

const { result } = await harness.executeOnce();

expect(result?.success).toBeTrue();
for (const file of ['styles.css', 'index.html', 'main.js']) {
harness
.expectFile(`dist/browser/${file}`)
.content.not.toContain(`https://fonts.googleapis.com/css?family=Roboto:300,400,500`);
harness
.expectFile(`dist/browser/${file}`)
.content.toMatch(/@font-face{font-family:'?Roboto/);
}
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export class ComponentStylesheetBundler {
namespace,
};
});
build.onLoad({ filter: /^css;/, namespace }, async () => {
build.onLoad({ filter: /^css;/, namespace }, () => {
return {
contents: data,
loader: 'css',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ export function createCompilerPluginOptions(
advancedOptimizations,
inlineStyleLanguage,
jit,
cacheOptions,
tailwindConfiguration,
publicPath,
} = options;

return {
Expand All @@ -52,6 +54,7 @@ export function createCompilerPluginOptions(
// Component stylesheet options
styleOptions: {
workspaceRoot,
inlineFonts: !!optimizationOptions.fonts.inline,
optimization: !!optimizationOptions.styles.minify,
sourcemap:
// Hidden component stylesheet sourcemaps are inaccessible which is effectively
Expand All @@ -65,7 +68,8 @@ export function createCompilerPluginOptions(
inlineStyleLanguage,
preserveSymlinks,
tailwindConfiguration,
publicPath: options.publicPath,
cacheOptions,
publicPath,
},
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export function createGlobalStylesBundleOptions(
externalDependencies,
stylePreprocessorOptions,
tailwindConfiguration,
cacheOptions,
publicPath,
} = options;

const namespace = 'angular:styles/global';
Expand All @@ -49,6 +51,7 @@ export function createGlobalStylesBundleOptions(
{
workspaceRoot,
optimization: !!optimizationOptions.styles.minify,
inlineFonts: !!optimizationOptions.fonts.inline,
sourcemap: !!sourcemapOptions.styles,
preserveSymlinks,
target,
Expand All @@ -61,7 +64,8 @@ export function createGlobalStylesBundleOptions(
},
includePaths: stylePreprocessorOptions?.includePaths,
tailwindConfiguration,
publicPath: options.publicPath,
cacheOptions,
publicPath,
},
loadCache,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
* found in the LICENSE file at https://angular.io/license
*/

import type { BuildOptions } from 'esbuild';
import type { BuildOptions, Plugin } from 'esbuild';
import path from 'node:path';
import { NormalizedCachedOptions } from '../../../utils/normalize-cache';
import { LoadResultCache } from '../load-result-cache';
import { createCssInlineFontsPlugin } from './css-inline-fonts-plugin';
import { CssStylesheetLanguage } from './css-language';
import { createCssResourcePlugin } from './css-resource-plugin';
import { LessStylesheetLanguage } from './less-language';
Expand All @@ -18,6 +20,7 @@ import { StylesheetPluginFactory } from './stylesheet-plugin-factory';
export interface BundleStylesheetOptions {
workspaceRoot: string;
optimization: boolean;
inlineFonts: boolean;
preserveSymlinks?: boolean;
sourcemap: boolean | 'external' | 'inline';
outputNames: { bundles: string; media: string };
Expand All @@ -26,6 +29,7 @@ export interface BundleStylesheetOptions {
target: string[];
tailwindConfiguration?: { file: string; package: string };
publicPath?: string;
cacheOptions: NormalizedCachedOptions;
}

export function createStylesheetBundleOptions(
Expand All @@ -48,6 +52,17 @@ export function createStylesheetBundleOptions(
cache,
);

const plugins: Plugin[] = [
pluginFactory.create(SassStylesheetLanguage),
pluginFactory.create(LessStylesheetLanguage),
pluginFactory.create(CssStylesheetLanguage),
createCssResourcePlugin(cache),
];

if (options.inlineFonts) {
plugins.push(createCssInlineFontsPlugin({ cache, cacheOptions: options.cacheOptions }));
}

return {
absWorkingDir: options.workspaceRoot,
bundle: true,
Expand All @@ -66,11 +81,6 @@ export function createStylesheetBundleOptions(
publicPath: options.publicPath,
conditions: ['style', 'sass', 'less'],
mainFields: ['style', 'sass'],
plugins: [
pluginFactory.create(SassStylesheetLanguage),
pluginFactory.create(LessStylesheetLanguage),
pluginFactory.create(CssStylesheetLanguage),
createCssResourcePlugin(cache),
],
plugins,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import type { Plugin, PluginBuild } from 'esbuild';
import { InlineFontsProcessor } from '../../../utils/index-file/inline-fonts';
import { NormalizedCachedOptions } from '../../../utils/normalize-cache';
import { LoadResultCache, createCachedLoad } from '../load-result-cache';

/**
* Options for the createCssInlineFontsPlugin
* @see createCssInlineFontsPlugin
*/
export interface CssInlineFontsPluginOptions {
/** Disk cache normalized options */
cacheOptions?: NormalizedCachedOptions;
/** Load results cache. */
cache?: LoadResultCache;
}

/**
* Creates an esbuild {@link Plugin} that inlines fonts imported via import-rule.
* within the build configuration.
*/
export function createCssInlineFontsPlugin({
cache,
cacheOptions,
}: CssInlineFontsPluginOptions): Plugin {
return {
name: 'angular-css-inline-fonts-plugin',
setup(build: PluginBuild): void {
const inlineFontsProcessor = new InlineFontsProcessor({ cache: cacheOptions, minify: false });

build.onResolve({ filter: /fonts\.googleapis\.com|use\.typekit\.net/ }, (args) => {
// Only attempt to resolve import-rule tokens which only exist inside CSS.
if (args.kind !== 'import-rule') {
return null;
}

if (!inlineFontsProcessor.canInlineRequest(args.path)) {
return null;
}

return {
path: args.path,
namespace: 'css-inline-fonts',
};
});

build.onLoad(
{ filter: /./, namespace: 'css-inline-fonts' },
createCachedLoad(cache, async (args) => {
try {
return {
contents: await inlineFontsProcessor.processURL(args.path),
loader: 'css',
};
} catch (error) {
return {
loader: 'css',
errors: [
{
text: `Failed to inline external stylesheet '${args.path}'.`,
detail: error,
},
],
};
}
}),
);
},
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export class InlineFontsProcessor {
continue;
}

const content = await this.processHref(url);
const content = await this.processURL(url);
if (content === undefined) {
continue;
}
Expand Down Expand Up @@ -258,13 +258,18 @@ export class InlineFontsProcessor {
return data;
}

private async processHref(url: URL): Promise<string | undefined> {
const provider = this.getFontProviderDetails(url);
async processURL(url: string | URL): Promise<string | undefined> {
const normalizedURL = url instanceof URL ? url : this.createNormalizedUrl(url);
if (!normalizedURL) {
return;
}

const provider = this.getFontProviderDetails(normalizedURL);
if (!provider) {
return undefined;
}

let cssContent = await this.getResponse(url);
let cssContent = await this.getResponse(normalizedURL);

if (this.options.minify) {
cssContent = cssContent
Expand All @@ -279,23 +284,28 @@ export class InlineFontsProcessor {
return cssContent;
}

canInlineRequest(url: string): boolean {
const normalizedUrl = this.createNormalizedUrl(url);

return normalizedUrl ? !!this.getFontProviderDetails(normalizedUrl) : false;
}

private getFontProviderDetails(url: URL): FontProviderDetails | undefined {
return SUPPORTED_PROVIDERS[url.hostname];
}

private createNormalizedUrl(value: string): URL | undefined {
// Need to convert '//' to 'https://' because the URL parser will fail with '//'.
const normalizedHref = value.startsWith('//') ? `https:${value}` : value;
if (!normalizedHref.startsWith('http')) {
// Non valid URL.
// Example: relative path styles.css.
return undefined;
}
const url = new URL(value.startsWith('//') ? `https:${value}` : value, 'resolve://');

const url = new URL(normalizedHref);
// Force HTTPS protocol
url.protocol = 'https:';
switch (url.protocol) {
case 'http:':
case 'https:':
url.protocol = 'https:';

return url;
return url;
default:
return undefined;
}
}
}

0 comments on commit f6e67df

Please sign in to comment.