Last commit july 5th

This commit is contained in:
2024-07-05 13:46:23 +02:00
parent dad0d86e8c
commit b0e4dfbb76
24982 changed files with 2621219 additions and 413 deletions

View File

@@ -0,0 +1,132 @@
/*
* MIT License http://opensource.org/licenses/MIT
* Author: Ben Holloway @bholloway
*/
'use strict';
const os = require('os');
const path = require('path');
const postcss = require('postcss');
const fileProtocol = require('../file-protocol');
const algerbra = require('../position-algerbra');
const ORPHAN_CR_REGEX = /\r(?!\n)(.|\n)?/g;
/**
* Process the given CSS content into reworked CSS content.
*
* @param {string} sourceFile The absolute path of the file being processed
* @param {string} sourceContent CSS content without source-map
* @param {{outputSourceMap: boolean, transformDeclaration:function, absSourceMap:object,
* sourceMapConsumer:object, removeCR:boolean}} params Named parameters
* @return {{content: string, map: object}} Reworked CSS and optional source-map
*/
function process(sourceFile, sourceContent, params) {
// #107 libsass emits orphan CR not considered newline, postcss does consider newline (content vs source-map mismatch)
const correctedContent = params.removeCR && (os.EOL !== '\r') ?
sourceContent.replace(ORPHAN_CR_REGEX, ' $1') :
sourceContent;
// IMPORTANT - prepend file protocol to all sources to avoid problems with source map
const plugin = Object.assign(
() => ({
postcssPlugin: 'postcss-resolve-url',
prepare: () => {
const visited = new Set();
/**
* Given an apparent position find the directory of the original file.
*
* @param startPosApparent {{line: number, column: number}}
* @returns {false|string} Directory of original file or false on invalid
*/
const positionToOriginalDirectory = (startPosApparent) => {
// reverse the original source-map to find the original source file before transpilation
const startPosOriginal =
!!params.sourceMapConsumer &&
params.sourceMapConsumer.originalPositionFor(startPosApparent);
// we require a valid directory for the specified file
const directory =
!!startPosOriginal &&
!!startPosOriginal.source &&
fileProtocol.remove(path.dirname(startPosOriginal.source));
return directory;
};
return {
Declaration: (declaration) => {
var prefix,
isValid = declaration.value && (declaration.value.indexOf('url') >= 0) && !visited.has(declaration);
if (isValid) {
prefix = declaration.prop + declaration.raws.between;
declaration.value = params.transformDeclaration(declaration.value, getPathsAtChar);
visited.add(declaration);
}
/**
* Create a hash of base path strings.
*
* Position in the declaration is supported by postcss at the position of the url() statement.
*
* @param {number} index Index in the declaration value at which to evaluate
* @throws Error on invalid source map
* @returns {{subString:string, value:string, property:string, selector:string}} Hash of base path strings
*/
function getPathsAtChar(index) {
var subString = declaration.value.slice(0, index),
posSelector = algerbra.sanitise(declaration.parent.source.start),
posProperty = algerbra.sanitise(declaration.source.start),
posValue = algerbra.add([posProperty, algerbra.strToOffset(prefix)]),
posSubString = algerbra.add([posValue, algerbra.strToOffset(subString)]);
var result = {
subString: positionToOriginalDirectory(posSubString),
value : positionToOriginalDirectory(posValue),
property : positionToOriginalDirectory(posProperty),
selector : positionToOriginalDirectory(posSelector)
};
var isValid = [result.subString, result.value, result.property, result.selector].every(Boolean);
if (isValid) {
return result;
}
else if (params.sourceMapConsumer) {
throw new Error(
'source-map information is not available at url() declaration ' + (
ORPHAN_CR_REGEX.test(sourceContent) ?
'(found orphan CR, try removeCR option)' :
'(no orphan CR found)'
)
);
} else {
throw new Error('a valid source-map is not present (ensure preceding loaders output a source-map)');
}
}
}
};
}
}),
{ postcss: true }
);
// IMPORTANT - prepend file protocol to all sources to avoid problems with source map
return postcss([plugin])
.process(correctedContent, {
from: fileProtocol.prepend(sourceFile),
map : params.outputSourceMap && {
prev : !!params.absSourceMap && fileProtocol.prepend(params.absSourceMap),
inline : false,
annotation : false,
sourcesContent: true // #98 sourcesContent missing from output map
}
})
.then(({css, map}) => ({
content: css,
map : params.outputSourceMap ? fileProtocol.remove(map.toJSON()) : null
}));
}
module.exports = process;

View File

@@ -0,0 +1,39 @@
/*
* MIT License http://opensource.org/licenses/MIT
* Author: Ben Holloway @bholloway
*/
'use strict';
/**
* Prepend file:// protocol to source path string or source-map sources.
*/
function prepend(candidate) {
if (typeof candidate === 'string') {
return 'file://' + candidate;
} else if (candidate && (typeof candidate === 'object') && Array.isArray(candidate.sources)) {
return Object.assign({}, candidate, {
sources: candidate.sources.map(prepend)
});
} else {
throw new Error('expected string|object');
}
}
exports.prepend = prepend;
/**
* Remove file:// protocol from source path string or source-map sources.
*/
function remove(candidate) {
if (typeof candidate === 'string') {
return candidate.replace(/^file:\/{2}/, '');
} else if (candidate && (typeof candidate === 'object') && Array.isArray(candidate.sources)) {
return Object.assign({}, candidate, {
sources: candidate.sources.map(remove)
});
} else {
throw new Error('expected string|object');
}
}
exports.remove = remove;

View File

@@ -0,0 +1,86 @@
/*
* MIT License http://opensource.org/licenses/MIT
* Author: Ben Holloway @bholloway
*/
'use strict';
const path = require('path');
const PACKAGE_NAME = require('../../package.json').name;
/**
* Paths are formatted to have posix style path separators and those within the CWD are made relative to CWD.
*
* @param {string} absolutePath An absolute path to format
* @returns {string} the formatted path
*/
const pathToString = (absolutePath) => {
if (absolutePath === '') {
return '-empty-';
} else {
const relative = path.relative(process.cwd(), absolutePath).split(path.sep);
const segments =
(relative[0] !== '..') ? ['.'].concat(relative).filter(Boolean) :
(relative.lastIndexOf('..') < 2) ? relative :
absolutePath.replace(/^[A-Z]\:/, '').split(path.sep);
return segments.join('/');
}
};
exports.pathToString = pathToString;
/**
* Format a debug message.
*
* @param {string} filename The file being processed by webpack
* @param {string} uri A uri path, relative or absolute
* @param {Array<{base:string,joined:string,isSuccess:boolean}>} attempts An array of attempts, possibly empty
* @return {string} Formatted message
*/
const formatJoinMessage = (filename, uri, attempts) => {
const attemptToCells = (_, i, array) => {
const { base: prev } = (i === 0) ? {} : array[i-1];
const { base: curr, joined } = array[i];
return [(curr === prev) ? '' : pathToString(curr), pathToString(joined)];
};
const formatCells = (lines) => {
const maxWidth = lines.reduce((max, [cellA]) => Math.max(max, cellA.length), 0);
return lines.map(([cellA, cellB]) => [cellA.padEnd(maxWidth), cellB]).map((cells) => cells.join(' --> '));
};
return [PACKAGE_NAME + ': ' + pathToString(filename) + ': ' + uri]
.concat(attempts.length === 0 ? '-empty-' : formatCells(attempts.map(attemptToCells)))
.concat(attempts.some(({ isSuccess }) => isSuccess) ? 'FOUND' : 'NOT FOUND')
.join('\n ');
};
exports.formatJoinMessage = formatJoinMessage;
/**
* A factory for a log function predicated on the given debug parameter.
*
* The logging function created accepts a function that formats a message and parameters that the function utilises.
* Presuming the message function may be expensive we only call it if logging is enabled.
*
* The log messages are de-duplicated based on the parameters, so it is assumed they are simple types that stringify
* well.
*
* @param {function|boolean} debug A boolean or debug function
* @return {function(function, array):void} A logging function possibly degenerate
*/
const createDebugLogger = (debug) => {
const log = !!debug && ((typeof debug === 'function') ? debug : console.log);
const cache = {};
return log ?
((msgFn, params) => {
const key = Function.prototype.toString.call(msgFn) + JSON.stringify(params);
if (!cache[key]) {
cache[key] = true;
log(msgFn.apply(null, params));
}
}) :
(() => undefined);
};
exports.createDebugLogger = createDebugLogger;

View File

@@ -0,0 +1,24 @@
/*
* MIT License http://opensource.org/licenses/MIT
* Author: Ben Holloway @bholloway
*/
'use strict';
const fsUtils = (fs) => {
// fs from enhanced-resolver doesn't include fs.existsSync so we need to use fs.statsSync instead
const withStats = (fn) => (absolutePath) => {
try {
return fn(fs.statSync(absolutePath));
} catch (e) {
return false;
}
};
return {
isFileSync: withStats((stats) => stats.isFile()),
isDirectorySync: withStats((stats) => stats.isDirectory()),
existsSync: withStats((stats) => stats.isFile() || stats.isDirectory())
};
};
module.exports = fsUtils;

View File

@@ -0,0 +1,235 @@
/*
* MIT License http://opensource.org/licenses/MIT
* Author: Ben Holloway @bholloway
*/
'use strict';
const path = require('path');
const { createDebugLogger, formatJoinMessage } = require('./debug');
const fsUtils = require('./fs-utils');
const ITERATION_SAFETY_LIMIT = 100e3;
/**
* Wrap a function such that it always returns a generator of tuple elements.
*
* @param {function({uri:string},...):(Array|Iterator)<[string,string]|string>} fn The function to wrap
* @returns {function({uri:string},...):(Array|Iterator)<[string,string]>} A function that always returns tuple elements
*/
const asGenerator = (fn) => {
const toTuple = (defaults) => (value) => {
const partial = [].concat(value);
return [...partial, ...defaults.slice(partial.length)];
};
const isTupleUnique = (v, i, a) => {
const required = v.join(',');
return a.findIndex((vv) => vv.join(',') === required) === i;
};
return (item, ...rest) => {
const {uri} = item;
const mapTuple = toTuple([null, uri]);
const pending = fn(item, ...rest);
if (Array.isArray(pending)) {
return pending.map(mapTuple).filter(isTupleUnique)[Symbol.iterator]();
} else if (
pending &&
(typeof pending === 'object') &&
(typeof pending.next === 'function') &&
(pending.next.length === 0)
) {
return pending;
} else {
throw new TypeError(`in "join" function expected "generator" to return Array|Iterator`);
}
};
};
exports.asGenerator = asGenerator;
/**
* A high-level utility to create a join function.
*
* The `generator` is responsible for ordering possible base paths. The `operation` is responsible for joining a single
* `base` path with the given `uri`. The `predicate` is responsible for reporting whether the single joined value is
* successful as the overall result.
*
* Both the `generator` and `operation` may be `function*()` or simply `function(...):Array<string>`.
*
* @param {function({uri:string, isAbsolute:boolean, bases:{subString:string, value:string, property:string,
* selector:string}}, {filename:string, fs:Object, debug:function|boolean, root:string}):
* (Array<string>|Iterator<string>)} generator A function that takes the hash of base paths from the `engine` and
* returns ordered iterable of paths to consider
* @returns {function({filename:string, fs:Object, debug:function|boolean, root:string}):
* (function({uri:string, isAbsolute:boolean, bases:{subString:string, value:string, property:string,
* selector:string}}):string)} join implementation
*/
const createJoinImplementation = (generator) => (item, options, loader) => {
const { isAbsolute } = item;
const { root } = options;
const { fs } = loader;
// generate the iterator
const iterator = generator(item, options, loader);
const isValidIterator = iterator && typeof iterator === 'object' && typeof iterator.next === 'function';
if (!isValidIterator) {
throw new Error('expected generator to return Iterator');
}
// run the iterator lazily and record attempts
const { isFileSync, isDirectorySync } = fsUtils(fs);
const attempts = [];
for (let i = 0; i < ITERATION_SAFETY_LIMIT; i++) {
const { value, done } = iterator.next();
if (done) {
break;
} else if (value) {
const tuple = Array.isArray(value) && value.length === 2 ? value : null;
if (!tuple) {
throw new Error('expected Iterator values to be tuple of [string,string], do you need asGenerator utility?');
}
// skip elements where base or uri is non-string
// noting that we need to support base="" when root=""
const [base, uri] = value;
if ((typeof base === 'string') && (typeof uri === 'string')) {
// validate
const isValidBase = (isAbsolute && base === root) || (path.isAbsolute(base) && isDirectorySync(base));
if (!isValidBase) {
throw new Error(`expected "base" to be absolute path to a valid directory, got "${base}"`);
}
// make the attempt
const joined = path.normalize(path.join(base, uri));
const isFallback = true;
const isSuccess = isFileSync(joined);
attempts.push({base, uri, joined, isFallback, isSuccess});
if (isSuccess) {
break;
}
// validate any non-strings are falsey
} else {
const isValidTuple = value.every((v) => (typeof v === 'string') || !v);
if (!isValidTuple) {
throw new Error('expected Iterator values to be tuple of [string,string]');
}
}
}
}
return attempts;
};
exports.createJoinImplementation = createJoinImplementation;
/**
* A low-level utility to create a join function.
*
* The `implementation` function processes an individual `item` and returns an Array of attempts. Each attempt consists
* of a `base` and a `joined` value with `isSuccessful` and `isFallback` flags.
*
* In the case that any attempt `isSuccessful` then its `joined` value is the outcome. Otherwise the first `isFallback`
* attempt is used. If there is no successful or fallback attempts then `null` is returned indicating no change to the
* original URI in the CSS.
*
* The `attempts` Array is logged to console when in `debug` mode.
*
* @param {string} name Name for the resulting join function
* @param {function({uri:string, query:string, isAbsolute:boolean, bases:{subString:string, value:string,
* property:string, selector:string}}, {filename:string, fs:Object, debug:function|boolean, root:string}):
* Array<{base:string,joined:string,fallback?:string,result?:string}>} implementation A function accepts an item and
* returns a list of attempts
* @returns {function({filename:string, fs:Object, debug:function|boolean, root:string}):
* (function({uri:string, isAbsolute:boolean, bases:{subString:string, value:string, property:string,
* selector:string}}):string)} join function
*/
const createJoinFunction = (name, implementation) => {
const assertAttempts = (value) => {
const isValid =
Array.isArray(value) && value.every((v) =>
v &&
(typeof v === 'object') &&
(typeof v.base === 'string') &&
(typeof v.uri === 'string') &&
(typeof v.joined === 'string') &&
(typeof v.isSuccess === 'boolean') &&
(typeof v.isFallback === 'boolean')
);
if (!isValid) {
throw new Error(`expected implementation to return Array of {base, uri, joined, isSuccess, isFallback}`);
} else {
return value;
}
};
const assertJoined = (value) => {
const isValid = value && (typeof value === 'string') && path.isAbsolute(value) || (value === null);
if (!isValid) {
throw new Error(`expected "joined" to be absolute path, got "${value}"`);
} else {
return value;
}
};
const join = (options, loader) => {
const { debug } = options;
const { resourcePath } = loader;
const log = createDebugLogger(debug);
return (item) => {
const { uri } = item;
const attempts = implementation(item, options, loader);
assertAttempts(attempts, !!debug);
const { joined: fallback } = attempts.find(({ isFallback }) => isFallback) || {};
const { joined: result } = attempts.find(({ isSuccess }) => isSuccess) || {};
log(formatJoinMessage, [resourcePath, uri, attempts]);
return assertJoined(result || fallback || null);
};
};
const toString = () => '[Function ' + name + ']';
return Object.assign(join, !!name && {
toString,
toJSON: toString
});
};
exports.createJoinFunction = createJoinFunction;
/**
* The default iterable factory will order `subString` then `value` then `property` then `selector`.
*
* @param {string} uri The uri given in the file webpack is processing
* @param {boolean} isAbsolute True for absolute URIs, false for relative URIs
* @param {string} subString A possible base path
* @param {string} value A possible base path
* @param {string} property A possible base path
* @param {string} selector A possible base path
* @param {string} root The loader options.root value where given
* @returns {Array<string>} An iterable of possible base paths in preference order
*/
const defaultJoinGenerator = asGenerator(
({ uri, isAbsolute, bases: { subString, value, property, selector } }, { root }) =>
isAbsolute ? [root] : [subString, value, property, selector]
);
exports.defaultJoinGenerator = defaultJoinGenerator;
/**
* @type {function({filename:string, fs:Object, debug:function|boolean, root:string}):
* (function({uri:string, isAbsolute:boolean, bases:{subString:string, value:string, property:string,
* selector:string}}):string)} join function
*/
exports.defaultJoin = createJoinFunction(
'defaultJoin',
createJoinImplementation(defaultJoinGenerator)
);

View File

@@ -0,0 +1,36 @@
/*
* MIT License http://opensource.org/licenses/MIT
* Author: Ben Holloway @bholloway
*/
'use strict';
var stream = require('stream');
var hasLogged = false;
function logToTestHarness(maybeStream, options) {
var doLogging =
!hasLogged &&
!!maybeStream &&
(typeof maybeStream === 'object') &&
(maybeStream instanceof stream.Writable);
if (doLogging) {
hasLogged = true; // ensure we log only once
Object.keys(options).forEach(eachOptionKey);
}
function eachOptionKey(key) {
maybeStream.write(key + ': ' + stringify(options[key]) + '\n');
}
function stringify(value) {
try {
return JSON.stringify(value) || String(value);
} catch (e) {
return '-unstringifyable-';
}
}
}
module.exports = logToTestHarness;

View File

@@ -0,0 +1,62 @@
/*
* MIT License http://opensource.org/licenses/MIT
* Author: Ben Holloway @bholloway
*/
'use strict';
/**
* Given a sourcemap position create a new maybeObject with only line and column properties.
*
* @param {*|{line: number, column: number}} maybeObj Possible location hash
* @returns {{line: number, column: number}} Location hash with possible NaN values
*/
function sanitise(maybeObj) {
var obj = !!maybeObj && typeof maybeObj === 'object' && maybeObj || {};
return {
line: isNaN(obj.line) ? NaN : obj.line,
column: isNaN(obj.column) ? NaN : obj.column
};
}
exports.sanitise = sanitise;
/**
* Infer a line and position delta based on the linebreaks in the given string.
*
* @param candidate {string} A string with possible linebreaks
* @returns {{line: number, column: number}} A position object where line and column are deltas
*/
function strToOffset(candidate) {
var split = candidate.split(/\r\n|\n/g);
var last = split[split.length - 1];
return {
line: split.length - 1,
column: last.length
};
}
exports.strToOffset = strToOffset;
/**
* Add together a list of position elements.
*
* Lines are added. If the new line is zero the column is added otherwise it is overwritten.
*
* @param {{line: number, column: number}[]} list One or more sourcemap position elements to add
* @returns {{line: number, column: number}} Resultant position element
*/
function add(list) {
return list
.slice(1)
.reduce(
function (accumulator, element) {
return {
line: accumulator.line + element.line,
column: element.line > 0 ? element.column : accumulator.column + element.column,
};
},
list[0]
);
}
exports.add = add;

View File

@@ -0,0 +1,136 @@
/*
* MIT License http://opensource.org/licenses/MIT
* Author: Ben Holloway @bholloway
*/
'use strict';
var path = require('path'),
loaderUtils = require('loader-utils');
/**
* Create a value processing function for a given file path.
*
* @param {function(Object):string} join The inner join function
* @param {string} root The loader options.root value where given
* @param {string} directory The directory of the file webpack is currently processing
* @return {function} value processing function
*/
function valueProcessor({ join, root, directory }) {
var URL_STATEMENT_REGEX = /(url\s*\(\s*)(?:(['"])((?:(?!\2).)*)(\2)|([^'"](?:(?!\)).)*[^'"]))(\s*\))/g,
QUERY_REGEX = /([?#])/g;
/**
* Process the given CSS declaration value.
*
* @param {string} value A declaration value that may or may not contain a url() statement
* @param {function(number):Object} getPathsAtChar Given an offset in the declaration value get a
* list of possible absolute path strings
*/
return function transformValue(value, getPathsAtChar) {
// allow multiple url() values in the declaration
// split by url statements and process the content
// additional capture groups are needed to match quotations correctly
// escaped quotations are not considered
return value
.split(URL_STATEMENT_REGEX)
.map(initialise)
.map(eachSplitOrGroup)
.join('');
/**
* Ensure all capture group tokens are a valid string.
*
* @param {string|void} token A capture group or uncaptured token
* @returns {string}
*/
function initialise(token) {
return typeof token === 'string' ? token : '';
}
/**
* An Array reduce function that accumulates string length.
*/
function accumulateLength(accumulator, element) {
return accumulator + element.length;
}
/**
* Encode the content portion of <code>url()</code> statements.
* There are 6 capture groups in the split making every 7th unmatched.
*
* @param {string} element A single split item
* @param {number} i The index of the item in the split
* @param {Array} arr The array of split values
* @returns {string} Every 3 or 5 items is an encoded url everything else is as is
*/
function eachSplitOrGroup(element, i, arr) {
// the content of the url() statement is either in group 3 or group 5
var mod = i % 7;
// only one of the capture groups 3 or 5 will match the other will be falsey
if (element && ((mod === 3) || (mod === 5))) {
// calculate the offset of the match from the front of the string
var position = arr.slice(0, i - mod + 1).reduce(accumulateLength, 0);
// detect quoted url and unescape backslashes
var before = arr[i - 1],
after = arr[i + 1],
isQuoted = (before === after) && ((before === '\'') || (before === '"')),
unescaped = isQuoted ? element.replace(/\\{2}/g, '\\') : element;
// split into uri and query/hash and then determine if the uri is some type of file
var split = unescaped.split(QUERY_REGEX),
uri = split[0],
query = split.slice(1).join(''),
isRelative = testIsRelative(uri),
isAbsolute = testIsAbsolute(uri);
// file like URIs are processed but not all URIs are files
if (isRelative || isAbsolute) {
var bases = getPathsAtChar(position), // construct iterator as late as possible in case sourcemap invalid
absolute = join({ uri, query, isAbsolute, bases });
if (typeof absolute === 'string') {
var relative = path.relative(directory, absolute)
.replace(/\\/g, '/'); // #6 - backslashes are not legal in URI
return loaderUtils.urlToRequest(relative + query);
}
}
}
// everything else, including parentheses and quotation (where present) and media statements
return element;
}
};
/**
* The loaderUtils.isUrlRequest() doesn't support windows absolute paths on principle. We do not subscribe to that
* dogma so we add path.isAbsolute() check to allow them.
*
* We also eliminate module relative (~) paths.
*
* @param {string|undefined} uri A uri string possibly empty or undefined
* @return {boolean} True for relative uri
*/
function testIsRelative(uri) {
return !!uri && loaderUtils.isUrlRequest(uri, false) && !path.isAbsolute(uri) && (uri.indexOf('~') !== 0);
}
/**
* The loaderUtils.isUrlRequest() doesn't support windows absolute paths on principle. We do not subscribe to that
* dogma so we add path.isAbsolute() check to allow them.
*
* @param {string|undefined} uri A uri string possibly empty or undefined
* @return {boolean} True for absolute uri
*/
function testIsAbsolute(uri) {
return !!uri && (typeof root === 'string') && loaderUtils.isUrlRequest(uri, root) &&
(/^\//.test(uri) || path.isAbsolute(uri));
}
}
module.exports = valueProcessor;