Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: supports optional dependencies #168

Merged
merged 2 commits into from
Nov 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions docs/docs/050-modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,16 @@ or [PurgeCSS](https://github.com/FullHuman/purgecss).

#### With uncss

You have to install `uncss` in order to use this feature:

```bash
npm install --save-dev uncss
# if you prefer yarn
# yarn add --dev uncss
# if you prefer pnpm
# pnpm install --save-dev uncss
```

##### Options
See [the documentation of uncss](https://github.com/uncss/uncss) for all supported options.

Expand All @@ -225,6 +235,16 @@ The following uncss options are ignored if passed to the module:

Use PurgeCSS instead of uncss by adding `tool: 'purgeCSS'` to the options.

You have to install `purgecss` in order to use this feature:

```bash
npm install --save-dev purgecss
# if you prefer yarn
# yarn add --dev purgecss
# if you prefer pnpm
# pnpm install --save-dev purgecss
```

##### Options

See [the documentation of PurgeCSS](https://www.purgecss.com) for all supported options.
Expand Down Expand Up @@ -275,6 +295,16 @@ Optimized:
### minifyCss
Minifies CSS with [cssnano](http://cssnano.co/) inside `<style>` tags and `style` attributes.

You have to install `cssnano` and `postcss` in order to use this feature:

```bash
npm install --save-dev cssnano postcss
# if you prefer yarn
# yarn add --dev cssnano postcss
# if you prefer pnpm
# pnpm install --save-dev cssnano postcss
```

#### Options
See [the documentation of cssnano](http://cssnano.co/docs/optimisations/) for all supported optimizations.
By default CSS is minified with preset `default`, which shouldn't have any side-effects.
Expand Down Expand Up @@ -316,6 +346,16 @@ Minified:
### minifyJs
Minifies JS using [Terser](https://github.com/fabiosantoscode/terser) inside `<script>` tags.

You have to install `terser` in order to use this feature:

```bash
npm install --save-dev terser
# if you prefer yarn
# yarn add --dev terser
# if you prefer pnpm
# pnpm install --save-dev terser
```

#### Options
See [the documentation of Terser](https://github.com/fabiosantoscode/terser#api-reference) for all supported options.
Terser options can be passed directly to the `minifyJs` module:
Expand Down Expand Up @@ -664,6 +704,16 @@ Processed:
### minifyUrls
Convert absolute URL to relative URL using [relateurl](https://www.npmjs.com/package/relateurl).

You have to install `relateurl`, `terser` and `srcset` in order to use this feature:

```bash
npm install --save-dev relateurl terser srcset
# if you prefer yarn
# yarn add --dev relateurl terser srcset
# if you prefer pnpm
# pnpm install --save-dev relateurl terser srcset
```

#### Options

The base URL to resolve against. Support `String` & `URL`.
Expand Down
11 changes: 11 additions & 0 deletions lib/helpers.es6
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,14 @@ export function extractCssFromStyleNode(node) {
export function isEventHandler(attributeName) {
return attributeName && attributeName.slice && attributeName.slice(0, 2).toLowerCase() === 'on' && attributeName.length >= 5;
}

export function optionalRequire(moduleName) {
try {
return require(moduleName);
} catch (e) {
if (e.code === 'MODULE_NOT_FOUND') {
return null;
}
throw e;
}
}
37 changes: 31 additions & 6 deletions lib/htmlnano.es6
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,20 @@ const presets = {
};

export function loadConfig(options, preset, configPath) {
if (! options?.skipConfigLoading) {
if (!options?.skipConfigLoading) {
const explorer = cosmiconfigSync(packageJson.name);
const rc = configPath ? explorer.load(configPath) : explorer.search();
if (rc) {
const { preset: presetName } = rc.config;
if (presetName) {
if (! preset && presets[presetName]) {
if (!preset && presets[presetName]) {
preset = presets[presetName];
}

delete rc.config.preset;
}
if (! options) {

if (!options) {
options = rc.config;
}
}
Expand All @@ -37,6 +37,13 @@ export function loadConfig(options, preset, configPath) {
];
}

const optionalDependencies = {
minifyCss: ['cssnano', 'postcss'],
minifyJs: ['terser'],
minifyUrl: ['relateurl', 'srcset', 'terser'],
minifySvg: ['svgo'],
};
Comment on lines +40 to +45
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

purgecss and uncss are not included here since only one of them is required when enabled.


function htmlnano(optionsRun, presetRun) {
let [options, preset] = loadConfig(optionsRun, presetRun);

Expand All @@ -45,7 +52,7 @@ function htmlnano(optionsRun, presetRun) {
let promise = Promise.resolve(tree);

for (const [moduleName, moduleOptions] of Object.entries(options)) {
if (! moduleOptions) {
if (!moduleOptions) {
// The module is disabled
continue;
}
Expand All @@ -54,6 +61,18 @@ function htmlnano(optionsRun, presetRun) {
throw new Error('Module "' + moduleName + '" is not defined');
}

(optionalDependencies[moduleName] || []).forEach(dependency => {
try {
require(dependency);
} catch (e) {
if (e.code === 'MODULE_NOT_FOUND') {
console.warn(`You have to install "${dependency}" in order to use htmlnano's "${moduleName}" module`);
} else {
throw e;
}
}
});
Comment on lines +64 to +74
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try to resolve the required dependencies before HTML actually being minified. With Node.js's require cache it won't add any performance overhead when those dependencies are required again.


let module = require('./modules/' + moduleName);
promise = promise.then(tree => module.default(tree, options, moduleOptions));
}
Expand All @@ -62,6 +81,12 @@ function htmlnano(optionsRun, presetRun) {
};
}

htmlnano.getRequiredOptionalDependencies = function (optionsRun, presetRun) {
const [options] = loadConfig(optionsRun, presetRun);

return [...new Set(Object.keys(options).filter(moduleName => options[moduleName]).map(moduleName => optionalDependencies[moduleName]).flat())];
};
Comment on lines +84 to +88
Copy link
Contributor Author

@SukkaW SukkaW Nov 21, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getRequiredOptionalDependencies API is designed for other libraries that utilize htmlnano.

For example, parcel-bundler has a plugin @parcel/optimizer-htmlnano built on top of htmlnano. This API should enable the parcel to install those optional dependencies automatically.

Ideas and opinions from parcel developers (@devongovett @mischnic) would be appreciated.



htmlnano.process = function (html, options, preset, postHtmlOptions) {
return posthtml([htmlnano(options, preset)])
Expand Down
11 changes: 8 additions & 3 deletions lib/modules/minifyCss.es6
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { isStyleNode, extractCssFromStyleNode } from '../helpers';
import postcss from 'postcss';
import cssnano from 'cssnano';
import { isStyleNode, extractCssFromStyleNode, optionalRequire } from '../helpers';

const cssnano = optionalRequire('cssnano');
const postcss = optionalRequire('postcss');

const postcssOptions = {
// Prevent the following warning from being shown:
Expand All @@ -11,6 +12,10 @@ const postcssOptions = {

/** Minify CSS with cssnano */
export default function minifyCss(tree, options, cssnanoOptions) {
if (!cssnano || !postcss) {
return tree;
}

let promises = [];
tree.walk(node => {
if (isStyleNode(node)) {
Expand Down
6 changes: 4 additions & 2 deletions lib/modules/minifyJs.es6
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import terser from 'terser';
import { isEventHandler } from '../helpers';
import { isEventHandler, optionalRequire } from '../helpers';
import { redundantScriptTypes } from './removeRedundantAttributes';

const terser = optionalRequire('terser');

/** Minify JS with Terser */
export default function minifyJs(tree, options, terserOptions) {
if (!terser) return tree;

let promises = [];
tree.walk(node => {
if (node.tag && node.tag === 'script') {
Expand Down
8 changes: 6 additions & 2 deletions lib/modules/minifySvg.es6
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { optimize } from 'svgo';
import { optionalRequire } from '../helpers';

const svgo = optionalRequire('svgo');

/** Minify SVG with SVGO */
export default function minifySvg(tree, options, svgoOptions = {}) {
if (!svgo) return tree;

tree.match({tag: 'svg'}, node => {
let svgStr = tree.render(node, { closingSingleTag: 'slash', quoteAllAttributes: true });
const result = optimize(svgStr, svgoOptions);
const result = svgo.optimize(svgStr, svgoOptions);
node.tag = false;
node.attrs = {};
node.content = result.data;
Expand Down
51 changes: 31 additions & 20 deletions lib/modules/minifyUrls.es6
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import RelateUrl from 'relateurl';
import srcset from 'srcset';
import terser from 'terser';
import { optionalRequire } from '../helpers';

const RelateUrl = optionalRequire('relateurl');
const srcset = optionalRequire('srcset');
const terser = optionalRequire('terser');

// Adopts from https://github.com/kangax/html-minifier/blob/51ce10f4daedb1de483ffbcccecc41be1c873da2/src/htmlminifier.js#L209-L221
const tagsHaveUriValuesForAttributes = new Set([
Expand Down Expand Up @@ -134,7 +136,9 @@ export default function minifyUrls(tree, options, moduleOptions) {
* e.g. unit tests cases.
*/
if (!relateUrlInstance || STORED_URL_BASE !== urlBase) {
relateUrlInstance = new RelateUrl(urlBase);
if (RelateUrl) {
relateUrlInstance = new RelateUrl(urlBase);
}
STORED_URL_BASE = urlBase;
}

Expand All @@ -156,30 +160,35 @@ export default function minifyUrls(tree, options, moduleOptions) {
if (isJavaScriptUrl(attrValue)) {
promises.push(minifyJavaScriptUrl(node, attrName));
} else {
// FIXME!
// relateurl@1.0.0-alpha only supports URL while stable version (0.2.7) only supports string
// the WHATWG URL API is very strict while attrValue might not be a valid URL
// new URL should be used, and relateUrl#relate should be wrapped in try...catch after relateurl@1 is stable
node.attrs[attrName] = relateUrlInstance.relate(attrValue);
if (relateUrlInstance) {
// FIXME!
// relateurl@1.0.0-alpha only supports URL while stable version (0.2.7) only supports string
// the WHATWG URL API is very strict while attrValue might not be a valid URL
// new URL should be used, and relateUrl#relate should be wrapped in try...catch after relateurl@1 is stable
node.attrs[attrName] = relateUrlInstance.relate(attrValue);
}
}

continue;
}

if (isSrcsetAttribute(node.tag, attrNameLower)) {
try {
const parsedSrcset = srcset.parse(attrValue);

node.attrs[attrName] = srcset.stringify(parsedSrcset.map(srcset => {
srcset.url = relateUrlInstance.relate(srcset.url);

return srcset;
}));
} catch (e) {
// srcset will throw an Error for invalid srcset.
if (srcset) {
try {
const parsedSrcset = srcset.parse(attrValue);

node.attrs[attrName] = srcset.stringify(parsedSrcset.map(srcset => {
if (relateUrlInstance) {
srcset.url = relateUrlInstance.relate(srcset.url);
}

return srcset;
}));
} catch (e) {
// srcset will throw an Error for invalid srcset.
}
}


continue;
}
}
Expand All @@ -196,6 +205,8 @@ function isJavaScriptUrl(url) {
}

function minifyJavaScriptUrl(node, attrName) {
if (!terser) return Promise.resolve();

const jsWrapperStart = 'function a(){';
const jsWrapperEnd = '}a();';

Expand Down
17 changes: 11 additions & 6 deletions lib/modules/removeUnusedCss.es6
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { isStyleNode, extractCssFromStyleNode } from '../helpers';
import uncss from 'uncss';
import Purgecss from 'purgecss';
import { isStyleNode, extractCssFromStyleNode, optionalRequire } from '../helpers';

const uncss = optionalRequire('uncss');
const purgecss = optionalRequire('purgecss');

// These options must be set and shouldn't be overriden to ensure uncss doesn't look at linked stylesheets.
const uncssOptions = {
Expand Down Expand Up @@ -90,7 +91,7 @@ function runPurgecss(tree, css, userOptions) {
}]
};

return new Purgecss()
return new purgecss.PurgeCSS()
.purge(options)
.then((result) => {
return result[0].css;
Expand All @@ -105,9 +106,13 @@ export default function removeUnusedCss(tree, options, userOptions) {
tree.walk(node => {
if (isStyleNode(node)) {
if (userOptions.tool === 'purgeCSS') {
promises.push(processStyleNodePurgeCSS(tree, node, userOptions));
if (purgecss) {
promises.push(processStyleNodePurgeCSS(tree, node, userOptions));
}
} else {
promises.push(processStyleNodeUnCSS(html, node, userOptions));
if (uncss) {
promises.push(processStyleNodeUnCSS(html, node, userOptions));
}
}
}
return node;
Expand Down
Loading