Skip to content

Commit

Permalink
feat!(postcss-normalize-url): inline third-party dep and remove options
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ludofischer committed Mar 19, 2023
1 parent 697a0e5 commit 2f3ca13
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 40 deletions.
5 changes: 5 additions & 0 deletions .changeset/tame-islands-pump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'postcss-normalize-url': major
---

feat!: drop third-party normalize-url and remove options
1 change: 0 additions & 1 deletion packages/postcss-normalize-url/package.json
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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 2f3ca13

Please sign in to comment.