Skip to content

Commit

Permalink
feat!(postcss-normalize-url): inline third-party dep and remove optio…
Browse files Browse the repository at this point in the history
…ns (#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
  • Loading branch information
ludofischer committed Mar 22, 2023
1 parent 65674d4 commit 99d1e6a
Show file tree
Hide file tree
Showing 11 changed files with 177 additions and 50 deletions.
5 changes: 5 additions & 0 deletions .changeset/tame-islands-pump.md
@@ -0,0 +1,5 @@
---
'postcss-normalize-url': major
---

feat!: drop third-party normalize-url and remove options
2 changes: 1 addition & 1 deletion .eslintrc.json
@@ -1,6 +1,6 @@
{
"parserOptions": {
"ecmaVersion": 2018
"ecmaVersion": 2020
},
"extends": [
"eslint:recommended",
Expand Down
2 changes: 1 addition & 1 deletion packages/cssnano-preset-default/src/index.js
Expand Up @@ -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},
Expand Down
2 changes: 1 addition & 1 deletion packages/cssnano-preset-default/types/index.d.ts
Expand Up @@ -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 | {
Expand Down
7 changes: 0 additions & 7 deletions packages/postcss-normalize-url/README.md
Expand Up @@ -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).
Expand Down
1 change: 0 additions & 1 deletion packages/postcss-normalize-url/package.json
Expand Up @@ -20,7 +20,6 @@
],
"license": "MIT",
"dependencies": {
"normalize-url": "^6.0.1",
"postcss-value-parser": "^4.2.0"
},
"homepage": "https://github.com/cssnano/cssnano",
Expand Down
32 changes: 8 additions & 24 deletions 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
Expand All @@ -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;
}
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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') {
Expand All @@ -126,32 +124,18 @@ function transformDecl(decl, opts) {
.toString();
}

/** @typedef {normalize.Options} Options */
/**
* @type {import('postcss').PluginCreator<Options>}
* @param {Options} opts
* @type {import('postcss').PluginCreator<void>}
* @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'
Expand Down
152 changes: 152 additions & 0 deletions 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:(?<type>[^,]*?),(?<data>[^#]*?)(?:#(?<hash>.*))?$/.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(
/(?<!\b[a-z][a-z\d+\-.]{1,50}:)\/{2,}/g,
'/'
);
}

// Decode URI octets
if (urlObject.pathname) {
try {
urlObject.pathname = decodeURI(urlObject.pathname);
} catch {
/* Do nothing */
}
}

if (urlObject.hostname) {
// Remove trailing dot
urlObject.hostname = urlObject.hostname.replace(/\.$/, '');
}

urlObject.pathname = urlObject.pathname.replace(/\/$/, '');

// Take advantage of many of the Node `url` normalizations
urlString = urlObject.toString();

// Remove ending `/`
if (urlObject.pathname === '/' && urlObject.hash === '') {
urlString = urlString.replace(/\/$/, '');
}

// Restore relative protocol
if (hasRelativeProtocol) {
urlString = urlString.replace(/^http:\/\//, '//');
}

return urlString;
}

module.exports = normalizeUrl;
11 changes: 3 additions & 8 deletions packages/postcss-normalize-url/types/index.d.ts
@@ -1,14 +1,9 @@
export = pluginCreator;
/** @typedef {normalize.Options} Options */
/**
* @type {import('postcss').PluginCreator<Options>}
* @param {Options} opts
* @type {import('postcss').PluginCreator<void>}
* @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");
6 changes: 6 additions & 0 deletions 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;
7 changes: 0 additions & 7 deletions pnpm-lock.yaml

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

0 comments on commit 99d1e6a

Please sign in to comment.