diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d9ba2b8..fd8d9ac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ - node-version: ['12', '14', '16'] + node-version: ['14', '16'] steps: - uses: actions/checkout@v2 @@ -27,5 +27,6 @@ jobs: working-directory: docs - name: Test docs build + if: always() run: npm run build working-directory: docs diff --git a/CHANGELOG.md b/CHANGELOG.md index bb87872..f1dbadd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [2.0.0] - 2022-01-12 +*The major version has to be released because of vulnerability in PostCSS (see [#165])* + +### Changed +- Support optional dependencies [#168] (`minifyUrl`, ` minifyJs`, `removeUnusedCss`, `minifyCss`). *This might be a breaking change for you*. Check the docs: https://github.com/posthtml/htmlnano/pull/168/files +- Disable `mergeScripts` & `mergeStyles` in the safe preset [#170]. + + ## [1.1.1] - 2021-09-19 This version fixes fatal errors introduced in [1.1.0]. @@ -225,6 +233,7 @@ Otherwise, you have to adapt the config according to the new [PurgeCSS@3](https: - Remove attributes that contains only white spaces. +[2.0.0]: https://github.com/posthtml/htmlnano/compare/1.1.1...2.0.0 [1.1.1]: https://github.com/posthtml/htmlnano/compare/1.1.0...1.1.1 [1.1.0]: https://github.com/posthtml/htmlnano/compare/1.0.1...1.1.0 [1.0.1]: https://github.com/posthtml/htmlnano/compare/1.0.0...1.0.1 @@ -250,6 +259,9 @@ Otherwise, you have to adapt the config according to the new [PurgeCSS@3](https: [0.1.2]: https://github.com/posthtml/htmlnano/compare/0.1.1...0.1.2 [0.1.1]: https://github.com/posthtml/htmlnano/compare/0.1.0...0.1.1 +[#170]: https://github.com/posthtml/htmlnano/issues/170 +[#168]: https://github.com/posthtml/htmlnano/issues/168 +[#165]: https://github.com/posthtml/htmlnano/issues/165 [#163]: https://github.com/posthtml/htmlnano/issues/163 [#161]: https://github.com/posthtml/htmlnano/issues/161 [#159]: https://github.com/posthtml/htmlnano/issues/159 diff --git a/README.md b/README.md index b03ea3d..b638266 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,6 @@ Modular HTML minifier, built on top of the [PostHTML](https://github.com/posthtml/posthtml). Inspired by [cssnano](http://cssnano.co/). -To switch to the optional fork of uncss, apply `uncss-fork.patch`. - -Reasons you may want to do this are: uncss uses outdated dependencies with security issues - -Reasons you may want to avoid this are: long term support, stability - ## [Benchmark](https://github.com/maltsev/html-minifiers-benchmark/blob/master/README.md) [html-minifier-terser@6.0.2]: https://www.npmjs.com/package/html-minifier-terser [htmlnano@1.1.1]: https://www.npmjs.com/package/htmlnano diff --git a/docs/docs/050-modules.md b/docs/docs/050-modules.md index 7ffe992..44bf650 100644 --- a/docs/docs/050-modules.md +++ b/docs/docs/050-modules.md @@ -203,6 +203,19 @@ 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 +``` + +You can also use a mainted fork [@novaatwarren/uncss](https://www.npmjs.com/package/@novaatwarren/uncss) instead. + + ##### Options See [the documentation of uncss](https://github.com/uncss/uncss) for all supported options. @@ -225,6 +238,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. @@ -275,6 +298,16 @@ Optimized: ### minifyCss Minifies CSS with [cssnano](http://cssnano.co/) inside ` +
+ +``` + +Minified (with `all`): +```html +
hello world!answer
+``` + +Minified (with `aggressive`): +```html +
hello world! answer
+``` + +Minified (with `conservative`): +```html +
hello world! answer
+``` + + +### deduplicateAttributeValues +Remove duplicate values from list-like attributes (`class`, `rel`, `ping`). + +#### Example +Source: +```html + +``` + +Minified: +```html + +``` + + +### removeComments +#### Options +- `safe` – removes all HTML comments except the conditional comments and [``](https://yandex.com/support/webmaster/controlling-robot/html.xml) (default) +- `all` — removes all HTML comments +- A `RegExp` — only HTML comments matching the given regexp will be removed. +- A `Function` that returns boolean — removes HTML comments that can make the given callback function returns truthy value. + +#### Example + +Source: + +```js +{ + removeComments: 'all' +} +``` + +```html +
+``` + +Minified: + +```html +
+``` + +Source: + +```js +{ + removeComments: // +} +``` + +```html +
this text will not be indexedLorem ipsum dolor sit ametLorem ipsum dolor sit amet
+``` + +Minified: + +```html +
this text will not be indexedLorem ipsum dolor sit ametLorem ipsum dolor sit amet
+``` + +Source: + +```js +{ + removeComments: (comments) => { + if (comments.includes('noindex')) return true; + return false; + } +} +``` + +```html +
this text will not be indexedLorem ipsum dolor sit ametLorem ipsum dolor sit amet
+``` + +Minified: + +```html +
this text will not be indexedLorem ipsum dolor sit ametLorem ipsum dolor sit amet
+``` + + +### removeEmptyAttributes +Removes empty [safe-to-remove](https://github.com/posthtml/htmlnano/blob/master/lib/modules/removeEmptyAttributes.es6) attributes. + +#### Side effects +This module could break your styles or JS if you use selectors with attributes: +```CSS +img[style=""] { + margin: 10px; +} +``` + +#### Example +Source: +```html + +``` + +Minified: +```html + +``` + +### removeAttributeQuotes +Remove quotes around attributes when possible, see [HTML Standard - 12.1.2.3 Attributes - Unquoted attribute value syntax](https://html.spec.whatwg.org/multipage/syntax.html#attributes-2). + +#### Example +Source: +```html +
+``` + +Minified: +```html +
+``` + +#### Notice +The feature is implemented by [posthtml-render's `quoteAllAttributes`](https://github.com/posthtml/posthtml-render#options), which is one of the PostHTML's option. So `removeAttributeQuotes` could be overriden by other PostHTML's plugins and PostHTML's configuration. + +For example: + +```js +posthtml([ + htmlnano({ + removeAttributeQuotes: true + }) +]).process(html, { + quoteAllAttributes: true +}) +``` + +`removeAttributeQuotes` will not work because PostHTML's `quoteAllAttributes` takes the priority. + +### removeUnusedCss + +Removes unused CSS inside ` + +``` + +Optimized: +```html +
+ +
+``` + + +### minifyCss +Minifies CSS with [cssnano](http://cssnano.co/) inside ` + +``` + +Minified: +```html +
+ +
+``` + + +### minifyJs +Minifies JS using [Terser](https://github.com/fabiosantoscode/terser) inside ` + +``` + +Minified: +```html +
+ +
+``` + + +### minifyJson +Minifies JSON inside ``. + +#### Example +Source: +```html + +``` + +Minified: +```html + +``` + + +### minifySvg +Minifies SVG inside `` tags using [SVGO](https://github.com/svg/svgo/). + +#### Options +See [the documentation of SVGO](https://github.com/svg/svgo/blob/master/README.md) for all supported options. +SVGO options can be passed directly to the `minifySvg` module: +```js +htmlnano.process(html, { + minifySvg: { + plugins: [ + { + name: 'preset-default', + params: { + overrides: { + builtinPluginName: { + optionName: 'optionValue' + }, + }, + }, + } + ] + } +}); +``` + +#### Example +Source: +```html + + + + + + SVG +` +``` + +Minified: +```html +SVG +``` + +### minifyConditionalComments + +Minify content inside conditional comments. + +#### Example + +Source: + +```html + +``` + +Minified: + +```html + +``` + +### removeRedundantAttributes +Removes redundant attributes from tags if they contain default values: +- `method="get"` from `
` +- `type="text"` from `
click
+``` + +Processed: +```html +
click
+``` + +**frequency** + +Source: +```html +
+``` + +Processed: +```html +
+``` + +### sortAttributes +Sort attributes inside elements. + +The module won't impact the plain-text size of the output. However it will improve the compression ratio of gzip/brotli used in HTTP compression. + +#### Options + +- `alphabetical`: Default option. Sort attributes in alphabetical order. +- `frequency`: Sort attributes by frequency. + +#### Example + +**alphabetical** + +Source: +```html + +``` + +Processed: +```html + +``` + +**frequency** + +Source: +```html + + +image +``` + +Processed: +```html + + +image +``` + +### 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`. + +```js +htmlnano.process(html, { + minifyUrls: 'https://example.com' // Valid configuration +}); +``` + +```js +htmlnano.process(html, { + minifyUrls: new URL('https://example.com') // Valid configuration +}); +``` + +```js +htmlnano.process(html, { + minifyUrls: false // The module will be disabled +}); +``` + +```js +htmlnano.process(html, { + minifyUrls: true // Invalid configuration, the module will be disabled +}); +``` + +#### Example + +**Basic Usage** + +Configuration: + +```js +htmlnano.process(html, { + minifyUrls: 'https://example.com' +}); +``` + +Source: + +```html +bar +``` + +Minified: + +```html +bar +``` + +**With sub-directory** + +Configuration: + +```js +htmlnano.process(html, { + minifyUrls: 'https://example.com/foo/baz/' +}); +``` + +Source: + +```html +bar +``` + +Minified: + +```html +bar +``` + +### removeOptionalTags +Remove certain tags that can be omitted, see [HTML Standard - 13.1.2.4 Optional tags](https://html.spec.whatwg.org/multipage/syntax.html#optional-tags). + +#### Example + +Source: + +```html +Title

Hi

+``` + +Minified: + +```html +Title

Hi

+``` + +#### Notice +Due to [the limitation of PostHTML](https://github.com/posthtml/htmlnano/issues/99), htmlnano can't remove only the start tag or the end tag of an element. Currently, htmlnano only supports removing the following optional tags, as htmlnano can remove their start tag and end tag at the same time: + +- `html` +- `head` +- `body` +- `colgroup` +- `tbody` + +### normalizeAttributeValues + +Normalize casing of attribute values. + +The module won't impact the plain-text size of the output. However it will improve the compression ratio of gzip/brotli used in HTTP compression. + +#### Example + +Source: + +```html +
+``` + +Minified: + +```html +
+``` diff --git a/docs/versioned_docs/version-2.0.0/060-contribute.md b/docs/versioned_docs/version-2.0.0/060-contribute.md new file mode 100644 index 0000000..c70093e --- /dev/null +++ b/docs/versioned_docs/version-2.0.0/060-contribute.md @@ -0,0 +1,16 @@ +# Contribute + +Since the minifier is modular, it's very easy to add new modules: + +1. Create a ES6-file inside `lib/modules/` with a function that does some minification. For example you can check [`lib/modules/example.es6`](https://github.com/posthtml/htmlnano/blob/master/lib/modules/example.es6). + +2. Add the module's name into one of those [presets](https://github.com/posthtml/htmlnano/tree/master/lib/presets). You can choose either `ampSafe`, `max`, or `safe`. + +3. Create a JS-file inside `test/modules/` with some unit-tests. + +4. Describe your module in the section "[Modules](https://github.com/posthtml/htmlnano/blob/master/README.md#modules)". + +5. Send me a pull request. + +Other types of contribution (bug fixes, documentation improves, etc) are also welcome! +Would like to contribute, but don't have any ideas what to do? Check out [our issues](https://github.com/posthtml/htmlnano/labels/help%20wanted). diff --git a/docs/versioned_sidebars/version-2.0.0-sidebars.json b/docs/versioned_sidebars/version-2.0.0-sidebars.json new file mode 100644 index 0000000..94ca8dd --- /dev/null +++ b/docs/versioned_sidebars/version-2.0.0-sidebars.json @@ -0,0 +1,8 @@ +{ + "version-2.0.0/tutorialSidebar": [ + { + "type": "autogenerated", + "dirName": "." + } + ] +} diff --git a/docs/versions.json b/docs/versions.json index 1cadd53..cbe532c 100644 --- a/docs/versions.json +++ b/docs/versions.json @@ -1,3 +1,4 @@ [ + "2.0.0", "1.1.1" ] diff --git a/lib/helpers.es6 b/lib/helpers.es6 index 576bcc0..de2c177 100644 --- a/lib/helpers.es6 +++ b/lib/helpers.es6 @@ -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; + } +} diff --git a/lib/htmlnano.es6 b/lib/htmlnano.es6 index b280d96..40b9bf5 100644 --- a/lib/htmlnano.es6 +++ b/lib/htmlnano.es6 @@ -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; } } @@ -37,6 +37,13 @@ export function loadConfig(options, preset, configPath) { ]; } +const optionalDependencies = { + minifyCss: ['cssnano', 'postcss'], + minifyJs: ['terser'], + minifyUrl: ['relateurl', 'srcset', 'terser'], + minifySvg: ['svgo'], +}; + function htmlnano(optionsRun, presetRun) { let [options, preset] = loadConfig(optionsRun, presetRun); @@ -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; } @@ -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; + } + } + }); + let module = require('./modules/' + moduleName); promise = promise.then(tree => module.default(tree, options, moduleOptions)); } @@ -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())]; +}; + htmlnano.process = function (html, options, preset, postHtmlOptions) { return posthtml([htmlnano(options, preset)]) diff --git a/lib/modules/minifyCss.es6 b/lib/modules/minifyCss.es6 index 9b23014..209b541 100644 --- a/lib/modules/minifyCss.es6 +++ b/lib/modules/minifyCss.es6 @@ -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: @@ -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)) { diff --git a/lib/modules/minifyJs.es6 b/lib/modules/minifyJs.es6 index 548a575..a73b7b7 100644 --- a/lib/modules/minifyJs.es6 +++ b/lib/modules/minifyJs.es6 @@ -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') { diff --git a/lib/modules/minifySvg.es6 b/lib/modules/minifySvg.es6 index 9ea33c8..6a63a9c 100644 --- a/lib/modules/minifySvg.es6 +++ b/lib/modules/minifySvg.es6 @@ -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; diff --git a/lib/modules/minifyUrls.es6 b/lib/modules/minifyUrls.es6 index 2fe452d..a06c348 100644 --- a/lib/modules/minifyUrls.es6 +++ b/lib/modules/minifyUrls.es6 @@ -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([ @@ -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; } @@ -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; } } @@ -196,6 +205,8 @@ function isJavaScriptUrl(url) { } function minifyJavaScriptUrl(node, attrName) { + if (!terser) return Promise.resolve(); + const jsWrapperStart = 'function a(){'; const jsWrapperEnd = '}a();'; diff --git a/lib/modules/removeUnusedCss.es6 b/lib/modules/removeUnusedCss.es6 index 77d9aaa..a968a9b 100644 --- a/lib/modules/removeUnusedCss.es6 +++ b/lib/modules/removeUnusedCss.es6 @@ -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 = { @@ -90,7 +91,7 @@ function runPurgecss(tree, css, userOptions) { }] }; - return new Purgecss() + return new purgecss.PurgeCSS() .purge(options) .then((result) => { return result[0].css; @@ -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; diff --git a/lib/presets/max.es6 b/lib/presets/max.es6 index 5fea58b..4515874 100644 --- a/lib/presets/max.es6 +++ b/lib/presets/max.es6 @@ -8,6 +8,8 @@ export default { ...safePreset, removeComments: 'all', removeAttributeQuotes: true, removeRedundantAttributes: true, + mergeScripts: true, + mergeStyles: true, removeUnusedCss: {}, minifyCss: { preset: 'default', diff --git a/lib/presets/safe.es6 b/lib/presets/safe.es6 index b7e2b6a..ee312fc 100644 --- a/lib/presets/safe.es6 +++ b/lib/presets/safe.es6 @@ -10,8 +10,8 @@ export default { collapseWhitespace: 'conservative', custom: [], deduplicateAttributeValues: true, - mergeScripts: true, - mergeStyles: true, + mergeScripts: false, + mergeStyles: false, removeUnusedCss: false, minifyCss: { preset: 'default', diff --git a/package.json b/package.json index f67bd1c..faa3c9b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "htmlnano", - "version": "1.1.1", + "version": "2.0.0", "description": "Modular HTML minifier, built on top of the PostHTML", "main": "index.js", "author": "Kirill Maltsev ", @@ -40,16 +40,8 @@ }, "dependencies": { "cosmiconfig": "^7.0.1", - "cssnano": "^5.0.8", - "postcss": "^8.3.6", "posthtml": "^0.16.5", - "purgecss": "^4.0.0", - "relateurl": "^0.2.7", - "srcset": "^4.0.0", - "svgo": "^2.6.1", - "terser": "^5.8.0", - "timsort": "^0.3.0", - "uncss": "^0.17.3" + "timsort": "^0.3.0" }, "devDependencies": { "@babel/cli": "^7.15.7", @@ -57,11 +49,55 @@ "@babel/preset-env": "^7.15.6", "@babel/register": "^7.15.3", "babel-eslint": "^10.1.0", + "cssnano": "^5.0.11", "eslint": "^7.32.0", "expect": "^27.2.0", "mocha": "^9.1.0", + "postcss": "^8.3.11", + "purgecss": "^4.0.3", + "relateurl": "^0.2.7", "release-it": "^14.11.5", - "rimraf": "^3.0.2" + "rimraf": "^3.0.2", + "srcset": "^5.0.0", + "svgo": "^2.8.0", + "terser": "^5.10.0", + "uncss": "^0.17.3" + }, + "peerDependencies": { + "cssnano": "^5.0.11", + "postcss": "^8.3.11", + "purgecss": "^4.0.3", + "relateurl": "^0.2.7", + "srcset": "^5.0.0", + "svgo": "^2.8.0", + "terser": "^5.10.0", + "uncss": "^0.17.3" + }, + "peerDependenciesMeta": { + "cssnano": { + "optional": true + }, + "postcss": { + "optional": true + }, + "purgecss": { + "optional": true + }, + "relateurl": { + "optional": true + }, + "srcset": { + "optional": true + }, + "svgo": { + "optional": true + }, + "terser": { + "optional": true + }, + "uncss": { + "optional": true + } }, "repository": { "type": "git", diff --git a/test/helpers.js b/test/helpers.js index 36e0602..c417ec0 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -1,5 +1,5 @@ import expect from 'expect'; -import { isAmpBoilerplate, isComment, isConditionalComment, isStyleNode, extractCssFromStyleNode } from '../lib/helpers'; +import { isAmpBoilerplate, isComment, isConditionalComment, isStyleNode, extractCssFromStyleNode, optionalRequire } from '../lib/helpers'; describe('[helpers]', () => { context('isAmpBoilerplate()', () => { @@ -57,4 +57,14 @@ describe('[helpers]', () => { })).toBe('abc def'); }); }); + + context('optionalRequire()', () => { + it('should return the dependency when resolved', () => { + expect(optionalRequire('expect')).toBe(expect); + }); + + it('should return null when module not found', () => { + expect(optionalRequire('null')).toBe(null); + }); + }); }); diff --git a/test/htmlnano.js b/test/htmlnano.js index 39f5aea..7a15409 100644 --- a/test/htmlnano.js +++ b/test/htmlnano.js @@ -24,6 +24,13 @@ describe('[htmlnano]', () => { expect(error.message).toBe('Module "notDefinedModule" is not defined'); }); }); + + it('getRequiredOptionalDependencies', () => { + expect(htmlnano.getRequiredOptionalDependencies({ + minifyUrl: true, + minifyJs: {} + })).toStrictEqual(['relateurl', 'srcset', 'terser']); + }); }); diff --git a/test/modules/mergeScripts.js b/test/modules/mergeScripts.js index 7fab1f5..4d373e5 100644 --- a/test/modules/mergeScripts.js +++ b/test/modules/mergeScripts.js @@ -1,10 +1,10 @@ import { init } from '../htmlnano'; -import safePreset from '../../lib/presets/safe'; +import maxPreset from '../../lib/presets/max'; describe('mergeScripts', () => { const options = { - mergeScripts: safePreset.mergeScripts, + mergeScripts: maxPreset.mergeScripts, }; it('should merge