Skip to content

Commit

Permalink
Merge pull request #133 from bholloway/feature/engine-next
Browse files Browse the repository at this point in the history
postcss multi-value engine
  • Loading branch information
bholloway committed Nov 10, 2019
2 parents 17dbe18 + 23cd950 commit 5fcb764
Show file tree
Hide file tree
Showing 13 changed files with 945 additions and 58 deletions.
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
{
"name": "resolve-url-loader",
"description": "monorepo of all things resolve-url-loader",
"private": true,
"author": "bholloway",
"license": "MIT",
"private": true,
"main": "packages/resolve-url-loader",
"workspaces": [
"packages/*"
],
Expand Down
72 changes: 59 additions & 13 deletions packages/resolve-url-loader/lib/engine/postcss.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ var os = require('os'),
postcss = require('postcss');

var fileProtocol = require('../file-protocol');
var algerbra = require('../position-algerbra');

var ORPHAN_CR_REGEX = /\r(?!\n)(.|\n)?/g;

Expand Down Expand Up @@ -49,7 +50,7 @@ function process(sourceFile, sourceContent, params) {
* Plugin for postcss that follows SASS transpilation.
*/
function postcssPlugin() {
return function(styles) {
return function applyPlugin(styles) {
styles.walkDecls(eachDeclaration);
};

Expand All @@ -58,32 +59,77 @@ function process(sourceFile, sourceContent, params) {
* @param declaration
*/
function eachDeclaration(declaration) {
var isValid = declaration.value && (declaration.value.indexOf('url') >= 0);
var prefix,
isValid = declaration.value && (declaration.value.indexOf('url') >= 0);
if (isValid) {
prefix = declaration.prop + declaration.raws.between;
declaration.value = params.transformDeclaration(declaration.value, getPathsAtChar);
}

/**
* Create an iterable 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 {string[]} Iterable of base path strings possibly empty
*/
function getPathsAtChar(index) {
var subString = declaration.value.slice(0, index),
posParent = 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)]);

// reverse the original source-map to find the original source file before transpilation
var startPosApparent = declaration.source.start,
startPosOriginal = params.sourceMapConsumer &&
params.sourceMapConsumer.originalPositionFor(startPosApparent);
var list = [posSubString, posValue, posProperty, posParent]
.map(positionToOriginalDirectory)
.filter(Boolean)
.filter(filterUnique);

// we require a valid directory for the specified file
var directory =
startPosOriginal &&
startPosOriginal.source &&
fileProtocol.remove(path.dirname(startPosOriginal.source));
if (directory) {
declaration.value = params.transformDeclaration(declaration.value, directory);
if (list.length) {
return list;
}
// source-map present but invalid entry
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 {
return [];
}
}
}
}

/**
* 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
*/
function positionToOriginalDirectory(startPosApparent) {
// reverse the original source-map to find the original source file before transpilation
var startPosOriginal =
!!params.sourceMapConsumer &&
params.sourceMapConsumer.originalPositionFor(startPosApparent);

// we require a valid directory for the specified file
var directory =
!!startPosOriginal &&
!!startPosOriginal.source &&
fileProtocol.remove(path.dirname(startPosOriginal.source));

return directory;
}

/**
* Simple Array filter predicate for unique elements.
*/
function filterUnique(element, i, array) {
return array.indexOf(element) === i;
}
}

module.exports = process;
47 changes: 36 additions & 11 deletions packages/resolve-url-loader/lib/engine/rework.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,27 +75,52 @@ function process(sourceFile, sourceContent, params) {
function eachDeclaration(declaration) {
var isValid = declaration.value && (declaration.value.indexOf('url') >= 0);
if (isValid) {
declaration.value = params.transformDeclaration(declaration.value, getPathsAtChar);
}

// reverse the original source-map to find the original source file before transpilation
var startPosApparent = declaration.position.start,
startPosOriginal = params.sourceMapConsumer &&
params.sourceMapConsumer.originalPositionFor(startPosApparent);

// we require a valid directory for the specified file
var directory =
startPosOriginal &&
startPosOriginal.source &&
fileProtocol.remove(path.dirname(startPosOriginal.source));
/**
* Create a list of base path strings.
*
* Position in the declaration is not supported since rework does not refine sourcemaps to this detail.
*
* @throws Error on invalid source map
* @returns {string[]} Iterable of base path strings possibly empty
*/
function getPathsAtChar() {
var directory = positionToOriginalDirectory(declaration.position.start);
if (directory) {
declaration.value = params.transformDeclaration(declaration.value, directory);
return [directory];
}
// source-map present but invalid entry
else if (params.sourceMapConsumer) {
throw new Error('source-map information is not available at url() declaration');
} else {
return [];
}
}
}
}

/**
* 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
*/
function positionToOriginalDirectory(startPosApparent) {
// reverse the original source-map to find the original source file before transpilation
var startPosOriginal =
!!params.sourceMapConsumer &&
params.sourceMapConsumer.originalPositionFor(startPosApparent);

// we require a valid directory for the specified file
var directory =
!!startPosOriginal &&
!!startPosOriginal.source &&
fileProtocol.remove(path.dirname(startPosOriginal.source));

return directory;
}
}

module.exports = process;
11 changes: 4 additions & 7 deletions packages/resolve-url-loader/lib/join-function.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,11 @@ function createJoinForPredicate(predicate, name) {
* For absolute uri only `uri` will be provided. In this case we substitute any `root` given in options.
*
* @param {string} uri A uri path, relative or absolute
* @param {string|Iterator.<string>} [baseOrIteratorOrAbsent] Optional absolute base path or iterator thereof
* @param {Iterator<string>} [maybeIterator] Optional iterator of absolute base path strings
* @return {string} Just the uri where base is empty or the uri appended to the base
*/
return function joinProper(uri, baseOrIteratorOrAbsent) {
var iterator =
(typeof baseOrIteratorOrAbsent === 'undefined') && new Iterator([options.root ]) ||
(typeof baseOrIteratorOrAbsent === 'string' ) && new Iterator([baseOrIteratorOrAbsent]) ||
baseOrIteratorOrAbsent;
return function joinProper(uri, maybeIterator) {
var iterator = typeof maybeIterator === 'undefined' ? new Iterator([options.root]) : maybeIterator;

var result = runIterator([]);
log(createJoinMsg, [filename, uri, result, result.isFound]);
Expand Down Expand Up @@ -128,7 +125,7 @@ exports.createJoinForPredicate = createJoinForPredicate;
*
* @param {string} file The file being processed by webpack
* @param {string} uri A uri path, relative or absolute
* @param {Array.<string>} bases Absolute base paths up to and including the found one
* @param {string[]} bases Absolute base paths up to and including the found one
* @param {boolean} isFound Indicates the last base was correct
* @return {string} Formatted message
*/
Expand Down
62 changes: 62 additions & 0 deletions packages/resolve-url-loader/lib/position-algerbra.js
Original file line number Diff line number Diff line change
@@ -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;
81 changes: 81 additions & 0 deletions packages/resolve-url-loader/lib/position-algerbra.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* MIT License http://opensource.org/licenses/MIT
* Author: Ben Holloway @bholloway
*/
'use strict';

const {basename} = require('path');
const tape = require('blue-tape');

const {sanitise, strToOffset, add} = require('./position-algerbra');

const json = (strings, ...substitutions) =>
String.raw(
strings,
...substitutions.map(v => JSON.stringify(v, (_, vv) => Number.isNaN(vv) ? 'NaN' : vv))
);

tape(
basename(require.resolve('./position-algerbra')),
({name, test, end: end1, equal}) => {
test(`${name} / sanitise()`, ({ end: end2 }) => {
[
[undefined, {line: NaN, column: NaN}],
[null, {line: NaN, column: NaN}],
[false, {line: NaN, column: NaN}],
[true, {line: NaN, column: NaN}],
[12, {line: NaN, column: NaN}],
[{}, {line: NaN, column: NaN}],
[{line: 1}, {line: 1, column: NaN}],
[{column: 1}, {line: NaN, column: 1}],
].forEach(([input, expected]) =>
equal(
json`${sanitise(input)}`,
json`${expected}`,
json`input ${input} should sanitise to ${expected}`
)
);

end2();
});

test(`${name} / strToOffset()`, ({ end: end2 }) => {
[
['', {line: 0, column: 0}],
['something', {line: 0, column: 9}],
['another\nthing', {line: 1, column: 5}],
['a\r\nthird\nthingie', {line: 2, column: 7}],
['a\r\nthird\rthingie', {line: 1, column: 13}],
].forEach(([input, expected]) =>
equal(
json`${strToOffset(input)}`,
json`${expected}`,
json`input ${input} should convert to ${expected}`
)
);

end2();
});

test(`${name} / add()`, ({ end: end2 }) => {
[
[[{line: 0, column: 2}, {line: 0, column: 3}], {line: 0, column: 5}],
[[{line: 0, column: 2}, {line: 1, column: 3}], {line: 1, column: 3}],
[[{line: 1, column: 2}, {line: 0, column: 3}], {line: 1, column: 5}],
[[{line: 1, column: 2}, {line: 2, column: 3}], {line: 3, column: 3}],
[[{line: NaN, column: NaN}, {line: 0, column: 3}], {line: NaN, column: NaN}],
[[{line: NaN, column: NaN}, {line: 2, column: 3}], {line: NaN, column: 3}]
].forEach(([input, expected]) =>
equal(
json`${add(input)}`,
json`${expected}`,
json`input ${input} should add to give ${expected}`
)
);

end2();
});

end1();
}
);
Loading

0 comments on commit 5fcb764

Please sign in to comment.