Skip to content

Commit 5e5b2d9

Browse files
committed
feat(@ngtools/webpack): support generating data URIs for inline component styles in JIT
This change adds the new `inlineStyleMimeType` option. When set to a valid MIME type, enables conversion of an Angular Component's inline styles into data URIs. This allows a Webpack 5 configuration rule to use the `mimetype` condition to process the inline styles. A valid MIME type is a string starting with `text/` (Example for CSS: `text/css`).
1 parent e1180ab commit 5e5b2d9

File tree

5 files changed

+112
-23
lines changed

5 files changed

+112
-23
lines changed

packages/ngtools/webpack/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,4 @@ The loader works with webpack plugin to compile the application's TypeScript. It
3636
* `jitMode` [default: `false`] - Enables JIT compilation and do not refactor the code to bootstrap. This replaces `templateUrl: "string"` with `template: require("string")` (and similar for styles) to allow for webpack to properly link the resources.
3737
* `directTemplateLoading` [default: `true`] - Causes the plugin to load component templates (HTML) directly from the filesystem. This is more efficient if only using the `raw-loader` to load component templates. Do not enable this option if additional loaders are configured for component templates.
3838
* `fileReplacements` [default: none] - Allows replacing TypeScript files with other TypeScript files in the build. This option acts on fully resolved file paths.
39+
* `inlineStyleMimeType` [default: none] - When set to a valid MIME type, enables conversion of an Angular Component's inline styles into data URIs. This allows a Webpack 5 configuration rule to use the `mimetype` condition to process the inline styles. A valid MIME type is a string starting with `text/` (Example for CSS: `text/css`). Currently only supported in JIT mode.

packages/ngtools/webpack/src/ivy/plugin.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export interface AngularWebpackPluginOptions {
4747
emitClassMetadata: boolean;
4848
emitNgModuleScope: boolean;
4949
jitMode: boolean;
50+
inlineStyleMimeType?: string;
5051
}
5152

5253
// Add support for missing properties in Webpack types as well as the loader's file emitter

packages/ngtools/webpack/src/ivy/transformation.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,18 @@ export function createAotTransformers(
3535

3636
export function createJitTransformers(
3737
builder: ts.BuilderProgram,
38-
options: { directTemplateLoading?: boolean },
38+
options: { directTemplateLoading?: boolean; inlineStyleMimeType?: string },
3939
): ts.CustomTransformers {
4040
const getTypeChecker = () => builder.getProgram().getTypeChecker();
4141

4242
return {
4343
before: [
44-
replaceResources(() => true, getTypeChecker, options.directTemplateLoading),
44+
replaceResources(
45+
() => true,
46+
getTypeChecker,
47+
options.directTemplateLoading,
48+
options.inlineStyleMimeType,
49+
),
4550
constructorParametersDownlevelTransform(builder.getProgram()),
4651
],
4752
};
@@ -89,7 +94,11 @@ export function replaceBootstrap(
8994
bootstrapImport = nodeFactory.createImportDeclaration(
9095
undefined,
9196
undefined,
92-
nodeFactory.createImportClause(false, undefined, nodeFactory.createNamespaceImport(bootstrapNamespace)),
97+
nodeFactory.createImportClause(
98+
false,
99+
undefined,
100+
nodeFactory.createNamespaceImport(bootstrapNamespace),
101+
),
93102
nodeFactory.createStringLiteral('@angular/platform-browser'),
94103
);
95104
}

packages/ngtools/webpack/src/transformers/replace_resources.ts

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ export function replaceResources(
1111
shouldTransform: (fileName: string) => boolean,
1212
getTypeChecker: () => ts.TypeChecker,
1313
directTemplateLoading = false,
14+
inlineStyleMimeType?: string,
1415
): ts.TransformerFactory<ts.SourceFile> {
16+
if (inlineStyleMimeType && !/^text\/[-.\w]+$/.test(inlineStyleMimeType)) {
17+
throw new Error('Invalid inline style MIME type.');
18+
}
19+
1520
return (context: ts.TransformationContext) => {
1621
const typeChecker = getTypeChecker();
1722
const resourceImportDeclarations: ts.ImportDeclaration[] = [];
@@ -20,9 +25,17 @@ export function replaceResources(
2025

2126
const visitNode: ts.Visitor = (node: ts.Node) => {
2227
if (ts.isClassDeclaration(node)) {
23-
const decorators = ts.visitNodes(node.decorators, node =>
28+
const decorators = ts.visitNodes(node.decorators, (node) =>
2429
ts.isDecorator(node)
25-
? visitDecorator(nodeFactory, node, typeChecker, directTemplateLoading, resourceImportDeclarations, moduleKind)
30+
? visitDecorator(
31+
nodeFactory,
32+
node,
33+
typeChecker,
34+
directTemplateLoading,
35+
resourceImportDeclarations,
36+
moduleKind,
37+
inlineStyleMimeType,
38+
)
2639
: node,
2740
);
2841

@@ -72,6 +85,7 @@ function visitDecorator(
7285
directTemplateLoading: boolean,
7386
resourceImportDeclarations: ts.ImportDeclaration[],
7487
moduleKind?: ts.ModuleKind,
88+
inlineStyleMimeType?: string,
7589
): ts.Decorator {
7690
if (!isComponentDecorator(node, typeChecker)) {
7791
return node;
@@ -92,9 +106,17 @@ function visitDecorator(
92106
const styleReplacements: ts.Expression[] = [];
93107

94108
// visit all properties
95-
let properties = ts.visitNodes(objectExpression.properties, node =>
109+
let properties = ts.visitNodes(objectExpression.properties, (node) =>
96110
ts.isObjectLiteralElementLike(node)
97-
? visitComponentMetadata(nodeFactory, node, styleReplacements, directTemplateLoading, resourceImportDeclarations, moduleKind)
111+
? visitComponentMetadata(
112+
nodeFactory,
113+
node,
114+
styleReplacements,
115+
directTemplateLoading,
116+
resourceImportDeclarations,
117+
moduleKind,
118+
inlineStyleMimeType,
119+
)
98120
: node,
99121
);
100122

@@ -123,6 +145,7 @@ function visitComponentMetadata(
123145
directTemplateLoading: boolean,
124146
resourceImportDeclarations: ts.ImportDeclaration[],
125147
moduleKind?: ts.ModuleKind,
148+
inlineStyleMimeType?: string,
126149
): ts.ObjectLiteralElementLike | undefined {
127150
if (!ts.isPropertyAssignment(node) || ts.isComputedPropertyName(node.name)) {
128151
return node;
@@ -134,10 +157,14 @@ function visitComponentMetadata(
134157
return undefined;
135158

136159
case 'templateUrl':
160+
const url = getResourceUrl(node.initializer, directTemplateLoading ? '!raw-loader!' : '');
161+
if (!url) {
162+
return node;
163+
}
164+
137165
const importName = createResourceImport(
138166
nodeFactory,
139-
node.initializer,
140-
directTemplateLoading ? '!raw-loader!' : '',
167+
url,
141168
resourceImportDeclarations,
142169
moduleKind,
143170
);
@@ -156,21 +183,33 @@ function visitComponentMetadata(
156183
return node;
157184
}
158185

159-
const isInlineStyles = name === 'styles';
186+
const isInlineStyle = name === 'styles';
160187
const styles = ts.visitNodes(node.initializer.elements, node => {
161188
if (!ts.isStringLiteral(node) && !ts.isNoSubstitutionTemplateLiteral(node)) {
162189
return node;
163190
}
164191

165-
if (isInlineStyles) {
166-
return nodeFactory.createStringLiteral(node.text);
192+
let url;
193+
if (isInlineStyle) {
194+
if (inlineStyleMimeType) {
195+
const data = Buffer.from(node.text).toString('base64');
196+
url = `data:${inlineStyleMimeType};charset=utf-8;base64,${data}`;
197+
} else {
198+
return nodeFactory.createStringLiteral(node.text);
199+
}
200+
} else {
201+
url = getResourceUrl(node);
167202
}
168203

169-
return createResourceImport(nodeFactory, node, undefined, resourceImportDeclarations, moduleKind) || node;
204+
if (!url) {
205+
return node;
206+
}
207+
208+
return createResourceImport(nodeFactory, url, resourceImportDeclarations, moduleKind);
170209
});
171210

172211
// Styles should be placed first
173-
if (isInlineStyles) {
212+
if (isInlineStyle) {
174213
styleReplacements.unshift(...styles);
175214
} else {
176215
styleReplacements.push(...styles);
@@ -206,16 +245,10 @@ function isComponentDecorator(node: ts.Node, typeChecker: ts.TypeChecker): node
206245

207246
function createResourceImport(
208247
nodeFactory: ts.NodeFactory,
209-
node: ts.Node,
210-
loader: string | undefined,
248+
url: string,
211249
resourceImportDeclarations: ts.ImportDeclaration[],
212250
moduleKind = ts.ModuleKind.ES2015,
213-
): ts.Identifier | ts.Expression | null {
214-
const url = getResourceUrl(node, loader);
215-
if (!url) {
216-
return null;
217-
}
218-
251+
): ts.Identifier | ts.Expression {
219252
const urlLiteral = nodeFactory.createStringLiteral(url);
220253

221254
if (moduleKind < ts.ModuleKind.ES2015) {

packages/ngtools/webpack/src/transformers/replace_resources_spec.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@ function transform(
1616
directTemplateLoading = true,
1717
importHelpers = true,
1818
module: ts.ModuleKind = ts.ModuleKind.ESNext,
19+
inlineStyleMimeType?: string,
1920
) {
2021
const { program, compilerHost } = createTypescriptContext(input, undefined, undefined, { importHelpers, module });
2122
const getTypeChecker = () => program.getTypeChecker();
2223
const transformer = replaceResources(
23-
() => shouldTransform, getTypeChecker, directTemplateLoading);
24+
() => shouldTransform, getTypeChecker, directTemplateLoading, inlineStyleMimeType);
2425

2526
return transformTypescript(input, [transformer], program, compilerHost);
2627
}
@@ -219,6 +220,50 @@ describe('@ngtools/webpack transformers', () => {
219220
expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`);
220221
});
221222

223+
it('should create data URIs for inline styles when inlineStyleMimeType is set', () => {
224+
const input = tags.stripIndent`
225+
import { Component } from '@angular/core';
226+
227+
@Component({
228+
selector: 'app-root',
229+
templateUrl: './app.component.html',
230+
styles: ['a { color: red }'],
231+
})
232+
export class AppComponent {
233+
title = 'app';
234+
}
235+
`;
236+
const output = tags.stripIndent`
237+
import { __decorate } from "tslib";
238+
import __NG_CLI_RESOURCE__0 from "!raw-loader!./app.component.html";
239+
import __NG_CLI_RESOURCE__1 from "data:text/css;charset=utf-8;base64,YSB7IGNvbG9yOiByZWQgfQ==";
240+
import { Component } from '@angular/core';
241+
242+
let AppComponent = class AppComponent {
243+
constructor() {
244+
this.title = 'app';
245+
}
246+
};
247+
AppComponent = __decorate([
248+
Component({
249+
selector: 'app-root',
250+
template: __NG_CLI_RESOURCE__0,
251+
styles: [__NG_CLI_RESOURCE__1]
252+
})
253+
], AppComponent);
254+
export { AppComponent };
255+
`;
256+
257+
const result = transform(input, true, true, true, ts.ModuleKind.ESNext, 'text/css');
258+
expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`);
259+
});
260+
261+
it('should throw error if inlineStyleMimeType value has invalid format', () => {
262+
expect(() =>
263+
transform('', true, true, true, ts.ModuleKind.ESNext, 'asdfsd;sdfsd//sdfsdf'),
264+
).toThrowError('Invalid inline style MIME type.');
265+
});
266+
222267
it('should replace resources with backticks', () => {
223268
const input = `
224269
import { Component } from '@angular/core';

0 commit comments

Comments
 (0)