Skip to content

Commit

Permalink
feat(@ngtools/webpack): support generating data URIs for inline compo…
Browse files Browse the repository at this point in the history
…nent 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`).
  • Loading branch information
clydin committed Apr 12, 2021
1 parent e1180ab commit 5e5b2d9
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 23 deletions.
1 change: 1 addition & 0 deletions packages/ngtools/webpack/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,4 @@ The loader works with webpack plugin to compile the application's TypeScript. It
* `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.
* `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.
* `fileReplacements` [default: none] - Allows replacing TypeScript files with other TypeScript files in the build. This option acts on fully resolved file paths.
* `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.
1 change: 1 addition & 0 deletions packages/ngtools/webpack/src/ivy/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export interface AngularWebpackPluginOptions {
emitClassMetadata: boolean;
emitNgModuleScope: boolean;
jitMode: boolean;
inlineStyleMimeType?: string;
}

// Add support for missing properties in Webpack types as well as the loader's file emitter
Expand Down
15 changes: 12 additions & 3 deletions packages/ngtools/webpack/src/ivy/transformation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,18 @@ export function createAotTransformers(

export function createJitTransformers(
builder: ts.BuilderProgram,
options: { directTemplateLoading?: boolean },
options: { directTemplateLoading?: boolean; inlineStyleMimeType?: string },
): ts.CustomTransformers {
const getTypeChecker = () => builder.getProgram().getTypeChecker();

return {
before: [
replaceResources(() => true, getTypeChecker, options.directTemplateLoading),
replaceResources(
() => true,
getTypeChecker,
options.directTemplateLoading,
options.inlineStyleMimeType,
),
constructorParametersDownlevelTransform(builder.getProgram()),
],
};
Expand Down Expand Up @@ -89,7 +94,11 @@ export function replaceBootstrap(
bootstrapImport = nodeFactory.createImportDeclaration(
undefined,
undefined,
nodeFactory.createImportClause(false, undefined, nodeFactory.createNamespaceImport(bootstrapNamespace)),
nodeFactory.createImportClause(
false,
undefined,
nodeFactory.createNamespaceImport(bootstrapNamespace),
),
nodeFactory.createStringLiteral('@angular/platform-browser'),
);
}
Expand Down
71 changes: 52 additions & 19 deletions packages/ngtools/webpack/src/transformers/replace_resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ export function replaceResources(
shouldTransform: (fileName: string) => boolean,
getTypeChecker: () => ts.TypeChecker,
directTemplateLoading = false,
inlineStyleMimeType?: string,
): ts.TransformerFactory<ts.SourceFile> {
if (inlineStyleMimeType && !/^text\/[-.\w]+$/.test(inlineStyleMimeType)) {
throw new Error('Invalid inline style MIME type.');
}

return (context: ts.TransformationContext) => {
const typeChecker = getTypeChecker();
const resourceImportDeclarations: ts.ImportDeclaration[] = [];
Expand All @@ -20,9 +25,17 @@ export function replaceResources(

const visitNode: ts.Visitor = (node: ts.Node) => {
if (ts.isClassDeclaration(node)) {
const decorators = ts.visitNodes(node.decorators, node =>
const decorators = ts.visitNodes(node.decorators, (node) =>
ts.isDecorator(node)
? visitDecorator(nodeFactory, node, typeChecker, directTemplateLoading, resourceImportDeclarations, moduleKind)
? visitDecorator(
nodeFactory,
node,
typeChecker,
directTemplateLoading,
resourceImportDeclarations,
moduleKind,
inlineStyleMimeType,
)
: node,
);

Expand Down Expand Up @@ -72,6 +85,7 @@ function visitDecorator(
directTemplateLoading: boolean,
resourceImportDeclarations: ts.ImportDeclaration[],
moduleKind?: ts.ModuleKind,
inlineStyleMimeType?: string,
): ts.Decorator {
if (!isComponentDecorator(node, typeChecker)) {
return node;
Expand All @@ -92,9 +106,17 @@ function visitDecorator(
const styleReplacements: ts.Expression[] = [];

// visit all properties
let properties = ts.visitNodes(objectExpression.properties, node =>
let properties = ts.visitNodes(objectExpression.properties, (node) =>
ts.isObjectLiteralElementLike(node)
? visitComponentMetadata(nodeFactory, node, styleReplacements, directTemplateLoading, resourceImportDeclarations, moduleKind)
? visitComponentMetadata(
nodeFactory,
node,
styleReplacements,
directTemplateLoading,
resourceImportDeclarations,
moduleKind,
inlineStyleMimeType,
)
: node,
);

Expand Down Expand Up @@ -123,6 +145,7 @@ function visitComponentMetadata(
directTemplateLoading: boolean,
resourceImportDeclarations: ts.ImportDeclaration[],
moduleKind?: ts.ModuleKind,
inlineStyleMimeType?: string,
): ts.ObjectLiteralElementLike | undefined {
if (!ts.isPropertyAssignment(node) || ts.isComputedPropertyName(node.name)) {
return node;
Expand All @@ -134,10 +157,14 @@ function visitComponentMetadata(
return undefined;

case 'templateUrl':
const url = getResourceUrl(node.initializer, directTemplateLoading ? '!raw-loader!' : '');
if (!url) {
return node;
}

const importName = createResourceImport(
nodeFactory,
node.initializer,
directTemplateLoading ? '!raw-loader!' : '',
url,
resourceImportDeclarations,
moduleKind,
);
Expand All @@ -156,21 +183,33 @@ function visitComponentMetadata(
return node;
}

const isInlineStyles = name === 'styles';
const isInlineStyle = name === 'styles';
const styles = ts.visitNodes(node.initializer.elements, node => {
if (!ts.isStringLiteral(node) && !ts.isNoSubstitutionTemplateLiteral(node)) {
return node;
}

if (isInlineStyles) {
return nodeFactory.createStringLiteral(node.text);
let url;
if (isInlineStyle) {
if (inlineStyleMimeType) {
const data = Buffer.from(node.text).toString('base64');
url = `data:${inlineStyleMimeType};charset=utf-8;base64,${data}`;
} else {
return nodeFactory.createStringLiteral(node.text);
}
} else {
url = getResourceUrl(node);
}

return createResourceImport(nodeFactory, node, undefined, resourceImportDeclarations, moduleKind) || node;
if (!url) {
return node;
}

return createResourceImport(nodeFactory, url, resourceImportDeclarations, moduleKind);
});

// Styles should be placed first
if (isInlineStyles) {
if (isInlineStyle) {
styleReplacements.unshift(...styles);
} else {
styleReplacements.push(...styles);
Expand Down Expand Up @@ -206,16 +245,10 @@ function isComponentDecorator(node: ts.Node, typeChecker: ts.TypeChecker): node

function createResourceImport(
nodeFactory: ts.NodeFactory,
node: ts.Node,
loader: string | undefined,
url: string,
resourceImportDeclarations: ts.ImportDeclaration[],
moduleKind = ts.ModuleKind.ES2015,
): ts.Identifier | ts.Expression | null {
const url = getResourceUrl(node, loader);
if (!url) {
return null;
}

): ts.Identifier | ts.Expression {
const urlLiteral = nodeFactory.createStringLiteral(url);

if (moduleKind < ts.ModuleKind.ES2015) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ function transform(
directTemplateLoading = true,
importHelpers = true,
module: ts.ModuleKind = ts.ModuleKind.ESNext,
inlineStyleMimeType?: string,
) {
const { program, compilerHost } = createTypescriptContext(input, undefined, undefined, { importHelpers, module });
const getTypeChecker = () => program.getTypeChecker();
const transformer = replaceResources(
() => shouldTransform, getTypeChecker, directTemplateLoading);
() => shouldTransform, getTypeChecker, directTemplateLoading, inlineStyleMimeType);

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

it('should create data URIs for inline styles when inlineStyleMimeType is set', () => {
const input = tags.stripIndent`
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styles: ['a { color: red }'],
})
export class AppComponent {
title = 'app';
}
`;
const output = tags.stripIndent`
import { __decorate } from "tslib";
import __NG_CLI_RESOURCE__0 from "!raw-loader!./app.component.html";
import __NG_CLI_RESOURCE__1 from "data:text/css;charset=utf-8;base64,YSB7IGNvbG9yOiByZWQgfQ==";
import { Component } from '@angular/core';
let AppComponent = class AppComponent {
constructor() {
this.title = 'app';
}
};
AppComponent = __decorate([
Component({
selector: 'app-root',
template: __NG_CLI_RESOURCE__0,
styles: [__NG_CLI_RESOURCE__1]
})
], AppComponent);
export { AppComponent };
`;

const result = transform(input, true, true, true, ts.ModuleKind.ESNext, 'text/css');
expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`);
});

it('should throw error if inlineStyleMimeType value has invalid format', () => {
expect(() =>
transform('', true, true, true, ts.ModuleKind.ESNext, 'asdfsd;sdfsd//sdfsdf'),
).toThrowError('Invalid inline style MIME type.');
});

it('should replace resources with backticks', () => {
const input = `
import { Component } from '@angular/core';
Expand Down

0 comments on commit 5e5b2d9

Please sign in to comment.