From 99d1e6abcf5c6d4fae4139cca439b0fcd9b9ac90 Mon Sep 17 00:00:00 2001 From: Ludovico Fischer <43557+ludofischer@users.noreply.github.com> Date: Wed, 22 Mar 2023 17:48:32 +0100 Subject: [PATCH] feat!(postcss-normalize-url): inline third-party dep and remove options (#1480) * chore: bump ECMAscript version Support the normalize-url code. * feat!(postcss-normalize-url): inline third-party dep and remove options Inline a simplified version of normalize-url. Reasons: - the two most recent major releases of normalize-url use ES modules, so cssnano cannot use them - most options change the meaning of the URLs, so it is unlikely that turning them on during minification makes sense THe remaining code removes redundant slashes and default ports, so performs the same as the previous default configuration. It does not sort parameters any more because we haven't yet found a method that preserves the correct encoding in all cases. If the user does not like these transforms they can turn the plugin off completely. * docs(postcss-normalize-url): update readme --- .changeset/tame-islands-pump.md | 5 + .eslintrc.json | 2 +- packages/cssnano-preset-default/src/index.js | 2 +- .../cssnano-preset-default/types/index.d.ts | 2 +- packages/postcss-normalize-url/README.md | 7 - packages/postcss-normalize-url/package.json | 1 - packages/postcss-normalize-url/src/index.js | 32 +--- .../postcss-normalize-url/src/normalize.js | 152 ++++++++++++++++++ .../postcss-normalize-url/types/index.d.ts | 11 +- .../types/normalize.d.ts | 6 + pnpm-lock.yaml | 7 - 11 files changed, 177 insertions(+), 50 deletions(-) create mode 100644 .changeset/tame-islands-pump.md create mode 100644 packages/postcss-normalize-url/src/normalize.js create mode 100644 packages/postcss-normalize-url/types/normalize.d.ts diff --git a/.changeset/tame-islands-pump.md b/.changeset/tame-islands-pump.md new file mode 100644 index 000000000..0a5c302f6 --- /dev/null +++ b/.changeset/tame-islands-pump.md @@ -0,0 +1,5 @@ +--- +'postcss-normalize-url': major +--- + +feat!: drop third-party normalize-url and remove options diff --git a/.eslintrc.json b/.eslintrc.json index e711d3f9d..bf0fcd671 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,6 +1,6 @@ { "parserOptions": { - "ecmaVersion": 2018 + "ecmaVersion": 2020 }, "extends": [ "eslint:recommended", diff --git a/packages/cssnano-preset-default/src/index.js b/packages/cssnano-preset-default/src/index.js index 090aa6603..94338d519 100644 --- a/packages/cssnano-preset-default/src/index.js +++ b/packages/cssnano-preset-default/src/index.js @@ -57,7 +57,7 @@ minifySelectors?: false | { exclude?: true}, minifyParams?: false | { exclude?: true}, normalizeCharset?: false | import('postcss-normalize-charset').Options & { exclude?: true}, minifyFontValues?: false | import('postcss-minify-font-values').Options & { exclude?: true}, -normalizeUrl?: false | import('postcss-normalize-url').Options & { exclude?: true}, +normalizeUrl?: false | { exclude?: true}, mergeLonghand?: false | { exclude?: true}, discardDuplicates?: false | { exclude?: true}, discardOverridden?: false | { exclude?: true}, diff --git a/packages/cssnano-preset-default/types/index.d.ts b/packages/cssnano-preset-default/types/index.d.ts index 6009841d5..93ea66b32 100644 --- a/packages/cssnano-preset-default/types/index.d.ts +++ b/packages/cssnano-preset-default/types/index.d.ts @@ -49,7 +49,7 @@ type Options = { minifyFontValues?: false | import('postcss-minify-font-values').Options & { exclude?: true; }; - normalizeUrl?: false | import('postcss-normalize-url').Options & { + normalizeUrl?: false | { exclude?: true; }; mergeLonghand?: false | { diff --git a/packages/postcss-normalize-url/README.md b/packages/postcss-normalize-url/README.md index 7420ce421..23c9d05ab 100644 --- a/packages/postcss-normalize-url/README.md +++ b/packages/postcss-normalize-url/README.md @@ -36,13 +36,6 @@ of stripping unnecessary quotes. For more examples, see the [tests](test.js). See the [PostCSS documentation](https://github.com/postcss/postcss#usage) for examples for your environment. -## API - -### normalize([options]) - -Please see the [normalize-url documentation][docs]. By default, -`normalizeProtocol`, `stripHash` & `stripWWW` are set to `false`. - ## Contributors See [CONTRIBUTORS.md](https://github.com/cssnano/cssnano/blob/master/CONTRIBUTORS.md). diff --git a/packages/postcss-normalize-url/package.json b/packages/postcss-normalize-url/package.json index a74ca13c6..209fc26bb 100644 --- a/packages/postcss-normalize-url/package.json +++ b/packages/postcss-normalize-url/package.json @@ -20,7 +20,6 @@ ], "license": "MIT", "dependencies": { - "normalize-url": "^6.0.1", "postcss-value-parser": "^4.2.0" }, "homepage": "https://github.com/cssnano/cssnano", diff --git a/packages/postcss-normalize-url/src/index.js b/packages/postcss-normalize-url/src/index.js index a6d8ab394..3542dcf8e 100644 --- a/packages/postcss-normalize-url/src/index.js +++ b/packages/postcss-normalize-url/src/index.js @@ -1,7 +1,7 @@ 'use strict'; const path = require('path'); const valueParser = require('postcss-value-parser'); -const normalize = require('normalize-url'); +const normalize = require('./normalize.js'); const multiline = /\\[\r\n]/; // eslint-disable-next-line no-useless-escape @@ -27,15 +27,14 @@ function isAbsolute(url) { /** * @param {string} url - * @param {normalize.Options} options * @return {string} */ -function convert(url, options) { +function convert(url) { if (isAbsolute(url) || url.startsWith('//')) { let normalizedURL; try { - normalizedURL = normalize(url, options); + normalizedURL = normalize(url); } catch (e) { normalizedURL = url; } @@ -74,10 +73,9 @@ function transformNamespace(rule) { /** * @param {import('postcss').Declaration} decl - * @param {normalize.Options} opts * @return {void} */ -function transformDecl(decl, opts) { +function transformDecl(decl) { decl.value = valueParser(decl.value) .walk((node) => { if (node.type !== 'function' || node.value.toLowerCase() !== 'url') { @@ -107,7 +105,7 @@ function transformDecl(decl, opts) { } if (!/^.+-extension:\//i.test(url.value)) { - url.value = convert(url.value, opts); + url.value = convert(url.value); } if (escapeChars.test(url.value) && url.type === 'string') { @@ -126,32 +124,18 @@ function transformDecl(decl, opts) { .toString(); } -/** @typedef {normalize.Options} Options */ /** - * @type {import('postcss').PluginCreator} - * @param {Options} opts + * @type {import('postcss').PluginCreator} * @return {import('postcss').Plugin} */ -function pluginCreator(opts) { - opts = Object.assign( - {}, - { - normalizeProtocol: false, - sortQueryParameters: false, - stripHash: false, - stripWWW: false, - stripTextFragment: false, - }, - opts - ); - +function pluginCreator() { return { postcssPlugin: 'postcss-normalize-url', OnceExit(css) { css.walk((node) => { if (node.type === 'decl') { - return transformDecl(node, opts); + return transformDecl(node); } else if ( node.type === 'atrule' && node.name.toLowerCase() === 'namespace' diff --git a/packages/postcss-normalize-url/src/normalize.js b/packages/postcss-normalize-url/src/normalize.js new file mode 100644 index 000000000..41031843a --- /dev/null +++ b/packages/postcss-normalize-url/src/normalize.js @@ -0,0 +1,152 @@ +/* Derived from normalize-url https://github.com/sindresorhus/normalize-url/main/index.js by Sindre Sorhus */ + +// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs +const DATA_URL_DEFAULT_MIME_TYPE = 'text/plain'; +const DATA_URL_DEFAULT_CHARSET = 'us-ascii'; + +const supportedProtocols = new Set(['https:', 'http:', 'file:']); + +/** + * @param {string} urlString + * @return {boolean} */ +function hasCustomProtocol(urlString) { + try { + const { protocol } = new URL(urlString); + return protocol.endsWith(':') && !supportedProtocols.has(protocol); + } catch { + return false; + } +} + +/** + * @param {string} urlString + * @return {string} */ +function normalizeDataURL(urlString) { + const match = /^data:(?[^,]*?),(?[^#]*?)(?:#(?.*))?$/.exec( + urlString + ); + + if (!match) { + throw new Error(`Invalid URL: ${urlString}`); + } + + let { type, data, hash } = + /** @type {{type: string, data: string, hash: string}} */ (match.groups); + const mediaType = type.split(';'); + + let isBase64 = false; + if (mediaType[mediaType.length - 1] === 'base64') { + mediaType.pop(); + isBase64 = true; + } + + // Lowercase MIME type + const mimeType = mediaType.shift()?.toLowerCase() ?? ''; + const attributes = mediaType + .map( + /** @type {(string: string) => string} */ (attribute) => { + let [key, value = ''] = attribute + .split('=') + .map( + /** @type {(string: string) => string} */ (string) => string.trim() + ); + + // Lowercase `charset` + if (key === 'charset') { + value = value.toLowerCase(); + + if (value === DATA_URL_DEFAULT_CHARSET) { + return ''; + } + } + + return `${key}${value ? `=${value}` : ''}`; + } + ) + .filter(Boolean); + + const normalizedMediaType = [...attributes]; + + if (isBase64) { + normalizedMediaType.push('base64'); + } + + if ( + normalizedMediaType.length > 0 || + (mimeType && mimeType !== DATA_URL_DEFAULT_MIME_TYPE) + ) { + normalizedMediaType.unshift(mimeType); + } + + return `data:${normalizedMediaType.join(';')},${ + isBase64 ? data.trim() : data + }${hash ? `#${hash}` : ''}`; +} + +/** + * @param {string} urlString + * @return {string} + */ +function normalizeUrl(urlString) { + urlString = urlString.trim(); + + // Data URL + if (/^data:/i.test(urlString)) { + return normalizeDataURL(urlString); + } + + if (hasCustomProtocol(urlString)) { + return urlString; + } + + const hasRelativeProtocol = urlString.startsWith('//'); + const isRelativeUrl = !hasRelativeProtocol && /^\.*\//.test(urlString); + + // Prepend protocol + if (!isRelativeUrl) { + urlString = urlString.replace(/^(?!(?:\w+:)?\/\/)|^\/\//, 'http:'); + } + + const urlObject = new URL(urlString); + + // Remove duplicate slashes if not preceded by a protocol + if (urlObject.pathname) { + urlObject.pathname = urlObject.pathname.replace( + /(?} - * @param {Options} opts + * @type {import('postcss').PluginCreator} * @return {import('postcss').Plugin} */ -declare function pluginCreator(opts: Options): import('postcss').Plugin; +declare function pluginCreator(): import('postcss').Plugin; declare namespace pluginCreator { - export { postcss, Options }; + const postcss: true; } -type Options = normalize.Options; -declare var postcss: true; -import normalize = require("normalize-url"); diff --git a/packages/postcss-normalize-url/types/normalize.d.ts b/packages/postcss-normalize-url/types/normalize.d.ts new file mode 100644 index 000000000..e8aae04e0 --- /dev/null +++ b/packages/postcss-normalize-url/types/normalize.d.ts @@ -0,0 +1,6 @@ +export = normalizeUrl; +/** + * @param {string} urlString + * @return {string} + */ +declare function normalizeUrl(urlString: string): string; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 147be71c7..350abda20 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -385,11 +385,9 @@ importers: packages/postcss-normalize-url: specifiers: - normalize-url: ^6.0.1 postcss: ^8.2.15 postcss-value-parser: ^4.2.0 dependencies: - normalize-url: 6.1.0 postcss-value-parser: 4.2.0 devDependencies: postcss: 8.4.21 @@ -2470,11 +2468,6 @@ packages: resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} engines: {node: '>=0.10.0'} - /normalize-url/6.1.0: - resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} - engines: {node: '>=10'} - dev: false - /nth-check/2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} dependencies: