Skip to content

Commit

Permalink
feat: add ability to minify css-tagged templates
Browse files Browse the repository at this point in the history
Fixes #3
  • Loading branch information
asyncLiz committed Feb 13, 2019
1 parent 5ae184a commit d37a037
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 49 deletions.
62 changes: 35 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,13 @@ Be sure to minify template literals _before_ transpiling to ES5. Otherwise, the

The following options are common to typical use cases.

| Property | Type | Default | Description |
| ---------------- | -------------------------------------------------------------------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `fileName` | string | | _Required._ The name of the file, used for syntax parsing and source maps. |
| `minifyOptions?` | [html-minifier options](https://www.npmjs.com/package/html-minifier#options-quick-reference) | `defaultMinifyOptions` | Defaults to production-ready minification. |
| `shouldMinify?` | function | `defaultShouldMinify` | A function that determines whether or not a template should be minified. Defaults to minify all tagged templates whose tag name contains "html" (case insensitive). |
| Property | Type | Default | Description |
| --------------------------- | -------------------------------------------------------------------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `fileName` | string | | _Required._ The name of the file, used for syntax parsing and source maps. |
| `minifyOptions?` | [html-minifier options](https://www.npmjs.com/package/html-minifier#options-quick-reference) | `defaultMinifyOptions` | Defaults to production-ready minification. |
| `minifyOptions?.minifyCSS?` | [clean-css options](https://www.npmjs.com/package/clean-css#constructor-options) | `defaultMinifyCSSOptions` | Uses clean-css defaults. |
| `shouldMinify?` | function | `defaultShouldMinify` | A function that determines whether or not an HTML template should be minified. Defaults to minify all tagged templates whose tag name contains "html" (case insensitive). |
| `shouldMinifyCSS?` | function | `defaultShouldMinifyCSS` | A function that determines whether or not a CSS template should be minified. Defaults to minify all tagged templates whose tag name contains "css" (case insensitive). |

### Advanced

Expand All @@ -93,48 +95,54 @@ All aspects of the API are exposed and customizable. The following options will

## Customization Examples

### Do not minify CSS

```js
import { minifyHTMLLiterals, defaultMinifyOptions } from 'minify-html-literals';

minifyHTMLLiterals(source, {
fileName: 'render.js',
minifyOptions: {
...defaultMinifyOptions,
minifyCSS: false
}
});
```

### Minify non-tagged templates

> This is particularly useful for libraries that define templates without using tags, such as Polymer's `<dom-module>`.
```js
import { minifyHTMLLiterals, defaultShouldMinify } from 'minify-html-literals';

minifyHTMLLiterals(
`function render() {
return html\`
<h1>This tagged template is minified</h1>
\${\`
<div>and so is this non-tagged template</div>
\`}
`
template.innerHTML = \`
<dom-module id="custom-styles">
<style>
html {
--custom-color: blue;
}
</style>
</dom-module>
\`;
}`,
`,
{
fileName: 'render.js',
shouldMinify(template) {
return (
defaultShouldMinify(template) ||
template.parts.some(part => {
return part.text.includes('<div>');
return part.text.includes('<dom-module>');
})
);
}
}
);
```

### Do not minify CSS

```js
import { minifyHTMLLiterals, defaultMinifyOptions } from 'minify-html-literals';

minifyHTMLLiterals(source, {
fileName: 'render.js',
minifyOptions: {
...defaultMinifyOptions,
minifyCSS: false
},
shouldMinifyCSS: () => false
});
```

### Modify generated SourceMap

```js
Expand Down
12 changes: 7 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,14 @@
},
"dependencies": {
"@types/html-minifier": "^3.5.2",
"clean-css": "^4.2.1",
"html-minifier": "^3.5.21",
"magic-string": "^0.25.0",
"parse-literals": "^1.1.0"
},
"devDependencies": {
"@types/chai": "^4.1.4",
"@types/clean-css": "^4.2.0",
"@types/mocha": "^5.2.5",
"@types/node": "^10.5.2",
"@types/sinon": "^5.0.1",
Expand Down
53 changes: 48 additions & 5 deletions src/minifyHTMLLiterals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,15 @@ export interface BaseOptions {
* @returns true if the template should be minified
*/
shouldMinify?(template: Template): boolean;
/**
* Determines whether or not a CSS template should be minified. The default is
* to minify all tagged template whose tag name contains "css" (case
* insensitive).
*
* @param template the template to check
* @returns true if the template should be minified
*/
shouldMinifyCSS?(template: Template): boolean;
/**
* Override custom validation or set to false to disable validation. This is
* only useful when implementing your own strategy that may return
Expand Down Expand Up @@ -192,6 +201,18 @@ export function defaultShouldMinify(template: Template) {
return !!template.tag && template.tag.toLowerCase().includes('html');
}

/**
* The default method to determine whether or not to minify a CSS template. It
* will return true for all tagged templates whose tag name contains "css" (case
* insensitive).
*
* @param template the template to check
* @returns true if the template should be minified
*/
export function defaultShouldMinifyCSS(template: Template) {
return !!template.tag && template.tag.toLowerCase().includes('css');
}

/**
* The default validation.
*/
Expand Down Expand Up @@ -253,29 +274,51 @@ export function minifyHTMLLiterals(
options.shouldMinify = defaultShouldMinify;
}

if (!options.shouldMinifyCSS) {
options.shouldMinifyCSS = defaultShouldMinifyCSS;
}

options.parseLiteralsOptions = {
...{ fileName: options.fileName },
...(options.parseLiteralsOptions || {})
};

const templates = options.parseLiterals(source, options.parseLiteralsOptions);
const strategy = (<CustomOptions<any>>options).strategy || defaultStrategy;
const { shouldMinify } = options;
const strategy =
<Strategy>(<CustomOptions<any>>options).strategy || defaultStrategy;
const { shouldMinify, shouldMinifyCSS } = options;
let validate: Validation | undefined;
if (options.validate !== false) {
validate = options.validate || defaultValidation;
}

const ms = new options.MagicString(source);
templates.forEach(template => {
if (shouldMinify(template)) {
const minifyHTML = shouldMinify(template);
const minifyCSS = !!strategy.minifyCSS && shouldMinifyCSS(template);
if (minifyHTML || minifyCSS) {
const placeholder = strategy.getPlaceholder(template.parts);
if (validate) {
validate.ensurePlaceholderValid(placeholder);
}

const html = strategy.combineHTMLStrings(template.parts, placeholder);
const min = strategy.minifyHTML(html, options.minifyOptions);
const combined = strategy.combineHTMLStrings(template.parts, placeholder);
let min: string;
if (minifyCSS) {
const minifyCSSOptions = (options.minifyOptions || {}).minifyCSS;
if (typeof minifyCSSOptions === 'function') {
min = minifyCSSOptions(combined);
} else if (minifyCSSOptions === false) {
min = combined;
} else {
const cssOptions =
typeof minifyCSSOptions === 'object' ? minifyCSSOptions : undefined;
min = strategy.minifyCSS!(combined, cssOptions);
}
} else {
min = strategy.minifyHTML(combined, options.minifyOptions);
}

const minParts = strategy.splitHTMLByPlaceholder(min, placeholder);
if (validate) {
validate.ensureHTMLPartsValid(template.parts, minParts);
Expand Down
43 changes: 35 additions & 8 deletions src/strategy.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { Options, minify } from 'html-minifier';
import * as CleanCSS from 'clean-css';
import { Options as HTMLOptions, minify } from 'html-minifier';
import { TemplatePart } from 'parse-literals';

/**
* A strategy on how to minify HTML.
* A strategy on how to minify HTML and optionally CSS.
*
* @template O minify HTML options
* @template C minify CSS options
*/
export interface Strategy<O = any> {
export interface Strategy<O = any, C = any> {
/**
* Retrieve a placeholder for the given array of template parts. The
* placeholder returned should be the same if the function is invoked with the
Expand All @@ -31,10 +35,18 @@ export interface Strategy<O = any> {
* Minfies the provided HTML string.
*
* @param html the html to minify
* @param options minify options
* @param options html minify options
* @returns minified HTML string
*/
minifyHTML(html: string, options?: O): string;
/**
* Minifies the provided CSS string.
*
* @param css the css to minfiy
* @param options css minify options
* @returns minified CSS string
*/
minifyCSS?(css: string, options?: C): string;
/**
* Splits a minfied HTML string back into an array of strings from the
* provided placeholder. The returned array of strings should be the same
Expand All @@ -47,15 +59,21 @@ export interface Strategy<O = any> {
splitHTMLByPlaceholder(html: string, placeholder: string): string[];
}

/**
* The default <code>clean-css</code> options, optimized for production
* minification.
*/
export const defaultMinifyCSSOptions: CleanCSS.Options = {};

/**
* The default <code>html-minifier</code> options, optimized for production
* minification.
*/
export const defaultMinifyOptions: Options = {
export const defaultMinifyOptions: HTMLOptions = {
caseSensitive: true,
collapseWhitespace: true,
decodeEntities: true,
minifyCSS: true,
minifyCSS: defaultMinifyCSSOptions,
minifyJS: true,
processConditionalComments: true,
removeAttributeQuotes: true,
Expand All @@ -67,9 +85,10 @@ export const defaultMinifyOptions: Options = {
};

/**
* The default strategy. This uses <code>html-minifier</code> to minify HTML.
* The default strategy. This uses <code>html-minifier</code> to minify HTML and
* <code>clean-css</code> to minify CSS.
*/
export const defaultStrategy: Strategy<Options> = {
export const defaultStrategy: Strategy<HTMLOptions, CleanCSS.Options> = {
getPlaceholder(parts) {
// Using @ and (); will cause the expression not to be removed in CSS.
// However, sometimes the semicolon can be removed (ex: inline styles).
Expand Down Expand Up @@ -123,6 +142,14 @@ export const defaultStrategy: Strategy<Options> = {
minifyCSS: minifyCSSOptions
});
},
minifyCSS(css, options = {}) {
const output = new CleanCSS(options).minify(css);
if (output.errors && output.errors.length) {
throw new Error(output.errors.join('\n\n'));
}

return output.styles;
},
splitHTMLByPlaceholder(html, placeholder) {
// Make the last character (a semicolon) optional. See above.
// return html.split(new RegExp(`${placeholder}?`, 'g'));
Expand Down
9 changes: 8 additions & 1 deletion test/exports.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,26 @@ import * as min from '../index';
import {
defaultGenerateSourceMap,
defaultShouldMinify,
defaultShouldMinifyCSS,
defaultValidation,
minifyHTMLLiterals
} from '../src/minifyHTMLLiterals';
import { defaultMinifyOptions, defaultStrategy } from '../src/strategy';
import {
defaultMinifyCSSOptions,
defaultMinifyOptions,
defaultStrategy
} from '../src/strategy';

describe('exports', () => {
it('should export minifyHTMLLiterals() function and defaults', () => {
expect(min).to.deep.equal({
minifyHTMLLiterals,
defaultGenerateSourceMap,
defaultShouldMinify,
defaultShouldMinifyCSS,
defaultValidation,
defaultMinifyOptions,
defaultMinifyCSSOptions,
defaultStrategy
});
});
Expand Down

0 comments on commit d37a037

Please sign in to comment.