Skip to content

Commit

Permalink
Add pathTransform option
Browse files Browse the repository at this point in the history
Closes #3
  • Loading branch information
alexmchardy committed Oct 23, 2015
1 parent 0ee2a1f commit 20ab8a1
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 22 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
### [0.3.0] - 2015-10-23
#### Added
- "pathTransform" option

### [0.2.0] - 2015-10-12
#### Changed
- "base" option now optional, defaults to PostCSS "to" option
Expand Down
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,35 @@ dist/
### Plugin options

#### `base`
Type: `string`
Default: PostCSS `to` option

Optional base path where the plugin will copy images, fonts, and other assets it finds in CSS `url()` declarations. Only `url()` declarations with relative paths are processed. Each asset's sub-directory hierarchy will be maintained under the base path. Basically, sub-directories after the last `../` in the path will be kept (or the whole path if no `../` exists). For example, if the plugin is called with `{ base: 'dist' }`, the image referred to by `url("../../images/icons/icon.jpg")` will be copied to `dist/images/icons/icon.jpg`.

By using a single `base` path, a build pipeline can output several built CSS files (each with its own PostCSS `to` destination) while organizing all their assets under one directory (e.g. under `dist/` in `dist/images/`, `dist/fonts/`, etc.).

If `base` is not specified assets will be copied by default to the base directory given to the PostCSS `to` option while still maintaining the assets' sub-directory hierarchy. For example, if PostCSS is told to ouput to `dist/css/foo.css` and `base` is not specified the image referred to by `url("../../images/icons/icon.jpg")` will be copied to `dist/css/images/icons/icon.jpg`.

#### `pathTransform`
Type: `function`
Default: `undefined`

Optional function that returns a transformed absolute filesystem path to an asset file. Useful for adding revision hashes to filenames for cachebusting (e.g. `image-a7f234e8d4.jpg`), or handling special cases. The function is expected to be of the form given below:
```js
/**
* Transforms the paths to which asset files will be copied
*
* @param {string} newPath - Absolute filesystem path to which asset would be copied by default
* @param {string} origPath - Absolute filesystem path to original asset file
* @param {object} contents - Buffer containing asset file contents
* @returns {string} - Transformed absolute filesystem path to which asset will be copied
*/
pathTransform: function(newPath, origPath, contents) {
// ... transform newPath ...
return newPath;
}
```

### PostCSS options

#### `to`
Expand Down
64 changes: 43 additions & 21 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,21 +45,23 @@ function getCommonBaseDir(a, b) {
* Processes a Declaration containing 'url()'
*
* @param {object} decl - PostCSS Declaration
* @param {string} assetBase - Base path to copy assets to
* @param {object} copyOpts - options passed to this plugin
* @param {string} copyOpts.base - Base path to copy assets to
* @param {function} copyOpts.pathTransform - user defined path transform
* @param {object} opts - options passed to PostCSS
* @param {object} postCssResult - PostCSS Result
* @returns {void}
*/
function handleUrlDecl(decl, assetBase, opts, postCssResult) {
function handleUrlDecl(decl, copyOpts, postCssOpts, postCssResult) {
// Replace 'url()' parts of Declaration
decl.value = decl.value.replace(/url\((.*?)\)/g,
function (fullMatch, urlMatch) {
// Example:
// decl.value = 'background: url("../../images/foo.png?a=123");'
// urlMatch = '"../../images/foo.png?a=123"'
// assetBase = 'dist/assets'
// opts.from = 'src/css/page/home.css'
// opts.to = 'dist/assets/css/home.min.css'
// urlMatch = '"../../images/foo.png?a=123"'
// copyOpts.base = 'dist/assets'
// postCssOpts.from = 'src/css/page/home.css'
// postCssOpts.to = 'dist/assets/css/home.min.css'

// "../../images/foo.png?a=123" -> ../../images/foo.png?a=123
urlMatch = trimUrlValue(urlMatch);
Expand All @@ -73,9 +75,9 @@ function handleUrlDecl(decl, assetBase, opts, postCssResult) {
}

// '/path/to/project/src/css/page/'
var cssFromDirAbs = path.dirname(path.resolve(opts.from));
var cssFromDirAbs = path.dirname(path.resolve(postCssOpts.from));
// '/path/to/project/dist/assets/css/page/'
var cssToDir = path.dirname(opts.to);
var cssToDir = path.dirname(postCssOpts.to);
// parsed.pathname = '../../images/foo.png'
var assetUrlParsed = url.parse(urlMatch);
// '/path/to/project/src/images/foo.png'
Expand All @@ -91,7 +93,28 @@ function handleUrlDecl(decl, assetBase, opts, postCssResult) {
// 'images'
var assetPathPart = path.relative(fromBaseDirAbs, assetFromDirAbs);
// '/path/to/project/dist/assets/images'
var newAssetPath = path.join(assetBase, assetPathPart);
var newAssetPath = path.join(copyOpts.base, assetPathPart);
// '/path/to/project/dist/assets/images/foo.png'
var newAssetFile = path.join(newAssetPath, assetBasename);

// Read the original file
var contents = null;
try {
contents = fs.readFileSync(assetFromAbs);
} catch(e) {
postCssResult.warn('Can\'t read asset file "' +
assetFromAbs + '". Ignoring.', { node: decl });
contents = null;
}

// Call user-defined function
if (copyOpts.pathTransform) {
newAssetFile = copyOpts.pathTransform(newAssetFile,
assetFromAbs, contents);
newAssetPath = path.dirname(newAssetFile);
assetBasename = path.basename(newAssetFile);
}

// 'foo.png?a=123'
var urlBasename = assetBasename +
(assetUrlParsed.search ? assetUrlParsed.search : '') +
Expand All @@ -101,13 +124,8 @@ function handleUrlDecl(decl, assetBase, opts, postCssResult) {
path.join(path.relative(cssToDir, newAssetPath), urlBasename) +
'")';

// Read the original file
var contents;
try {
contents = fs.readFileSync(assetFromAbs);
} catch(e) {
postCssResult.warn('Can\'t read asset file "' +
assetFromAbs + '". Ignoring.', { node: decl });
// Return early with new url() string if original file is unreadable
if (contents === null) {
return newUrl;
}

Expand All @@ -120,10 +138,8 @@ function handleUrlDecl(decl, assetBase, opts, postCssResult) {
return newUrl;
}

// '/path/to/project/dist/assets/images/foo.png'
var newAssetFile = path.join(newAssetPath, assetBasename);
try {
// Write new asset file to assetBase
// Write new asset file into base dir
fs.writeFileSync(newAssetFile, contents);
} catch(e) {
postCssResult.warn('Can\'t write new asset file "' +
Expand All @@ -149,13 +165,19 @@ module.exports = postcss.plugin('postcss-copy-assets', function (copyOpts) {
result.warn('postcss-copy-assets requires postcss "to" option.');
return;
}
if (copyOpts.pathTransform &&
typeof copyOpts.pathTransform !== 'function') {
result.warn('postcss-copy-assets "pathTransform" option ' +
'must be a function.');
return;
}
if (!copyOpts.base) {
copyOpts.base = path.dirname(postCssOpts.to);
}
var assetBase = path.resolve(copyOpts.base);
copyOpts.base = path.resolve(copyOpts.base);
css.walkDecls(function (decl) {
if (decl.value && decl.value.indexOf('url(') > -1) {
handleUrlDecl(decl, assetBase, postCssOpts, result);
handleUrlDecl(decl, copyOpts, postCssOpts, result);
}
});
};
Expand Down
52 changes: 51 additions & 1 deletion test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ var test = function (input, output, outputFiles, opts, warningsCount, done) {
postcss([ plugin(opts.plugin) ]).process(input, opts.postcss)
.then(function (result) {
var warnings = result.warnings();
expect(result.css).to.eql(output);
expect(result.css).to.eql(output, 'transformed css');
if (warnings.length && warnings.length !== warningsCount) {
console.log(warnings.join('\n'));
}
Expand Down Expand Up @@ -331,4 +331,54 @@ describe('postcss-copy-assets', function () {
opts, 0, done);
});

describe('pathTransform option', function () {
var cwd = process.cwd();

it('handles not a function', function (done) {
opts.plugin.pathTransform = 'not-a-function';
test('a{ background: url("test1.png") }',
'a{ background: url("test1.png") }',
null,
opts, 1, done);
});

it('receives correct arguments', function (done) {
opts.plugin.pathTransform = function (newPath, origPath, contents) {
expect(newPath).to.eql(
path.resolve(cwd, 'test/dist/assets/test1.png'),
'newPath');
expect(contents.toString()).to.eql('1', 'file contents');
expect(origPath).to.eql(
path.resolve(cwd, 'test/fixtures/src/css/test1.png'),
'origPath');
return newPath;
};
test('a{ background: url("test1.png") }',
'a{ background: url("../test1.png") }',
['test/dist/assets/test1.png'],
opts, 0, done);
});

it('can change the filename', function (done) {
opts.plugin.pathTransform = function (newPath) {
return newPath.replace(/\./, '-abc.');
};
test('a{ background: url("test1.png") }',
'a{ background: url("../test1-abc.png") }',
['test/dist/assets/test1-abc.png'],
opts, 0, done);
});

it('can change the path', function (done) {
opts.plugin.pathTransform = function (newPath) {
var i = newPath.lastIndexOf('/');
return newPath.slice(0, i) + '/abc' + newPath.slice(i);
};
test('a{ background: url("test1.png") }',
'a{ background: url("../abc/test1.png") }',
['test/dist/assets/abc/test1.png'],
opts, 0, done);
});
});

});

0 comments on commit 20ab8a1

Please sign in to comment.