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: