diff --git a/src/index.js b/src/index.js index 9d09dd0..5eeaf1c 100644 --- a/src/index.js +++ b/src/index.js @@ -13,13 +13,16 @@ module.exports = postcss.plugin('postcss-url', (options) => { options = options || {}; return function(styles, result) { + const promises = []; const opts = result.opts; const from = opts.from ? path.dirname(opts.from) : '.'; const to = opts.to ? path.dirname(opts.to) : from; styles.walkDecls((decl) => - declProcessor(from, to, options, result, decl) + promises.push(declProcessor(from, to, options, result, decl)) ); + + return Promise.all(promises); }; }); diff --git a/src/lib/decl-processor.js b/src/lib/decl-processor.js index 0a7cbc0..80dce9b 100644 --- a/src/lib/decl-processor.js +++ b/src/lib/decl-processor.js @@ -64,7 +64,7 @@ const wrapUrlProcessor = (urlProcessor, result, decl) => { }); return (asset, dir, option) => - urlProcessor(asset, dir, option, decl, warn, result, addDependency); + urlProcessor(asset, dir, option, decl, warn, addDependency); }; /** @@ -80,14 +80,14 @@ const getPattern = (decl) => * @param {Options} options * @param {Result} result * @param {Decl} decl - * @returns {String|undefined} + * @returns {Promise} */ const replaceUrl = (url, dir, options, result, decl) => { const asset = prepareAsset(url, dir, decl); const matchedOptions = matchOptions(asset, options); - if (!matchedOptions) return; + if (!matchedOptions) return Promise.resolve(); const process = (option) => { const wrappedUrlProcessor = wrapUrlProcessor(getUrlProcessor(option.url), result, decl); @@ -95,13 +95,21 @@ const replaceUrl = (url, dir, options, result, decl) => { return wrappedUrlProcessor(asset, dir, option); }; + const resultPromise; if (Array.isArray(matchedOptions)) { - matchedOptions.forEach((option) => asset.url = process(option)); + resultPromise = Promise.resolve(); + matchedOptions.forEach((option) => { + resultPromise = resultPromise.then(() => { + return process(option); + }); + }); } else { - asset.url = process(matchedOptions); + resultPromise = process(matchedOptions); } - return asset.url; + return resultPromise.then((url) => { + asset.url = url; + }); }; /** @@ -110,31 +118,44 @@ const replaceUrl = (url, dir, options, result, decl) => { * @param {PostcssUrl~Options} options * @param {Result} result * @param {Decl} decl - * @returns {PostcssUrl~DeclProcessor} + * @returns {Promise} */ const declProcessor = (from, to, options, result, decl) => { const dir = { from, to, file: getDirDeclFile(decl) }; const pattern = getPattern(decl); - if (!pattern) return; + if (!pattern) return Promise.resolve(); + + let id = 0; + const promises = []; decl.value = decl.value - .replace(pattern, (matched, before, url, after) => { - const newUrl = replaceUrl(url, dir, options, result, decl); + .replace(pattern, (matched, before, url, after) => { + const newUrlPromise = replaceUrl(url, dir, options, result, decl);; + + const marker = `::id${id++}`; - if (!newUrl) return matched; + promises.push( + newUrlPromise + .then((newUrl) => { + if (!newUrl) return matched; - if (WITH_QUOTES.test(newUrl) && WITH_QUOTES.test(after)) { + if (WITH_QUOTES.test(newUrl) && WITH_QUOTES.test(after)) { before = before.slice(0, -1); after = after.slice(1); - } + } - return `${before}${newUrl}${after}`; - }); + decl.value = decl.value.replace(marker, `${before}${newUrl}${after}`); + }) + ); + + return marker; + }); + + return Promise.all(promises); }; module.exports = { - replaceUrl, declProcessor }; diff --git a/src/lib/get-file.js b/src/lib/get-file.js index 7bb4b02..bcadaa7 100644 --- a/src/lib/get-file.js +++ b/src/lib/get-file.js @@ -5,31 +5,64 @@ const mime = require('mime'); const getPathByBasePath = require('./paths').getPathByBasePath; +const readFileAsync = (filePath) => { + return new Promise((resolse, reject) => { + fs.readFile(filePath, (err, data) => { + if (err) { + reject(err); + } + resolve(data); + }); + }); +}; + +const existFileAsync = (filePath) => { + new Promise((resolve, reject) => + fs.access(filePath, (err) => { + if (err) { + reject(); + } + resolve(path); + }) + ) +}; + +const oneSuccess = (promises) => { + return Promise.all(promises.map(p => { + return p.then( + val => Promise.reject(val), + err => Promise.resolve(err) + ); + })).then( + errors => Promise.reject(errors), + val => Promise.resolve(val) + ); +} + /** * * @param {PostcssUrl~Asset} asset * @param {PostcssUrl~Options} options * @param {PostcssUrl~Dir} dir * @param {Function} warn - * @returns {PostcssUrl~File} + * @returns {Promise} */ const getFile = (asset, options, dir, warn) => { - const paths = options.basePath - ? getPathByBasePath(options.basePath, dir.from, asset.pathname) - : [asset.absolutePath]; - const filePath = paths.find(fs.existsSync); - - if (!filePath) { - warn(`Can't read file '${paths.join()}', ignoring`); - - return; - } - - return { - path: filePath, - contents: fs.readFileSync(filePath), - mimeType: mime.getType(filePath) - }; + const paths = options.basePath ? + getPathByBasePath(options.basePath, dir.from, asset.pathname) : + [asset.absolutePath]; + + return oneSuccess(paths.map(path => existFileAsync(path))) + .then(path => readFileAsync(path)) + .then(contents => ({ + path: filePath, + contents: contents, + mimeType: mime.getType(filePath) + })) + .catch(() => { + warn(`Can't read file '${paths.join()}', ignoring`); + return; + }); }; module.exports = getFile; diff --git a/src/type/copy.js b/src/type/copy.js index 2f1da06..dd8a1d4 100644 --- a/src/type/copy.js +++ b/src/type/copy.js @@ -13,9 +13,46 @@ const getAssetsPath = paths.getAssetsPath; const normalize = paths.normalize; const getHashName = (file, options) => - (options && options.append ? (`${path.basename(file.path, path.extname(file.path))}_`) : '') - + calcHash(file.contents, options) - + path.extname(file.path); + (options && options.append ? (`${path.basename(file.path, path.extname(file.path))}_`) : '') + + calcHash(file.contents, options) + + path.extname(file.path); + +const createDirAsync = (path) => { + return Promise((resolve, reject) => { + mkdirp(path, (err) => { + if (err) reject(err); + resolve(); + }) + }) +}; + +const writeFileAsync = (file, src) => { + return new Promise((resolve, reject) => { + fs.open(dest, 'wx', (err, fd) => { + if (err) { + if (err.code === 'EEXIST') { + resolve(); + } + + reject(err); + } + + resolve(fd); + }) + }) + .then(fd => { + if (!fd) return; + + return new Promise((resolve, reject) => { + fs.writeFile(newAssetPath, file.contents, (err) => { + if (err) { + reject(err); + } + resolve(); + }); + }) + }); +}; /** * Copy images from readed from url() to an specific assets destination @@ -33,37 +70,35 @@ const getHashName = (file, options) => * @param {Result} result * @param {Function} addDependency * - * @returns {String|Undefined} + * @returns {Promise} */ -module.exports = function processCopy(asset, dir, options, decl, warn, result, addDependency) { - if (!options.assetsPath && dir.from === dir.to) { - warn('Option `to` of postcss is required, ignoring'); - - return; - } - - const file = getFile(asset, options, dir, warn); - - if (!file) return; - const assetRelativePath = options.useHash - ? getHashName(file, options.hashOptions) - : asset.relativePath; +module.exports = function processCopy(asset, dir, options, decl, warn, addDependency) { + if (!options.assetsPath && dir.from === dir.to) { + warn('Option `to` of postcss is required, ignoring'); - const targetDir = getTargetDir(dir); - const newAssetBaseDir = getAssetsPath(targetDir, options.assetsPath); - const newAssetPath = path.join(newAssetBaseDir, assetRelativePath); - const newRelativeAssetPath = normalize( - path.relative(targetDir, newAssetPath) - ); + return Promise.resolve(); + } - mkdirp.sync(path.dirname(newAssetPath)); + const newRelativeAssetPath = generateNewPath(file, dir, options); - if (!fs.existsSync(newAssetPath)) { - fs.writeFileSync(newAssetPath, file.contents); - } + return getFile(asset, options, dir, warn) + .then(file => { + const assetRelativePath = options.useHash ? + getHashName(file, options.hashOptions) : + asset.relativePath; - addDependency(file.path); + const targetDir = getTargetDir(dir); + const newAssetBaseDir = getAssetsPath(targetDir, options.assetsPath); + const newAssetPath = path.join(newAssetBaseDir, assetRelativePath); + const newRelativeAssetPath = normalize(path.relative(targetDir, newAssetPath)); - return `${newRelativeAssetPath}${asset.search}${asset.hash}`; + return createDirAsync(path.dirname(newAssetPath)) + .then(() => writeFileAsync(file, newAssetPath)) + .then(() => { + addDependency(file.path); + return `${newRelativeAssetPath}${asset.search}${asset.hash}`; + }); + } + ); }; diff --git a/src/type/custom.js b/src/type/custom.js index 4c610d4..68338dd 100644 --- a/src/type/custom.js +++ b/src/type/custom.js @@ -6,7 +6,7 @@ * @param {PostcssUrl~Dir} dir * @param {PostcssUrl~Option} options * - * @returns {String|Undefined} + * @returns {Promise} */ module.exports = function getCustomProcessor(asset, dir, options) { return options.url.apply(null, arguments); diff --git a/src/type/inline.js b/src/type/inline.js index d25c29c..c7f405d 100644 --- a/src/type/inline.js +++ b/src/type/inline.js @@ -15,7 +15,7 @@ const getFile = require('../lib/get-file'); * * @returns {String|Undefined} */ -function processFallback(originUrl, dir, options) { +const processFallback = (originUrl, dir, options) => { if (typeof options.fallback === 'function') { return options.fallback.apply(null, arguments); } @@ -25,10 +25,34 @@ function processFallback(originUrl, dir, options) { case 'rebase': return processRebase.apply(null, arguments); default: - return; + return Promise.resolve(); } } +const inlineProcess = (file, asset, warn, options) => { + const isSvg = file.mimeType === 'image/svg+xml'; + const defaultEncodeType = isSvg ? 'encodeURIComponent' : 'base64'; + const encodeType = options.encodeType || defaultEncodeType; + + // Warn for svg with hashes/fragments + if (isSvg && asset.hash && !options.ignoreFragmentWarning) { + // eslint-disable-next-line max-len + warn(`Image type is svg and link contains #. Postcss-url cant handle svg fragments. SVG file fully inlined. ${file.path}`); + } + + addDependency(file.path); + + const optimizeSvgEncode = isSvg && options.optimizeSvgEncode; + const encodedStr = encodeFile(file, encodeType, optimizeSvgEncode); + const resultValue = options.includeUriFragment && asset.hash + ? encodedStr + asset.hash + : encodedStr; + + // wrap url by quotes if percent-encoded svg + return isSvg && encodeType !== 'base64' ? `"${resultValue}"` : resultValue; +}; + + /** * Inline image in url() * @@ -41,48 +65,38 @@ function processFallback(originUrl, dir, options) { * @param {Result} result * @param {Function} addDependency * - * @returns {String|Undefined} + * @returns {Promise} */ // eslint-disable-next-line complexity -module.exports = function(asset, dir, options, decl, warn, result, addDependency) { - const file = getFile(asset, options, dir, warn); - - if (!file) return; +module.exports = function(asset, dir, options, decl, warn, addDependency) { + return getFile(asset, options, dir, warn) + .then(file => { + if (!file) return; - if (!file.mimeType) { - warn(`Unable to find asset mime-type for ${file.path}`); + if (!file.mimeType) { + warn(`Unable to find asset mime-type for ${file.path}`); - return; - } + return; + } - const maxSize = (options.maxSize || 0) * 1024; + const maxSize = (options.maxSize || 0) * 1024; - if (maxSize) { - const stats = fs.statSync(file.path); + if (maxSize) { + const size = Buffer.byteLength(file.contents); - if (stats.size >= maxSize) { - return processFallback.apply(this, arguments); + if (stats.size >= maxSize) { + return processFallback.apply(this, arguments); + } } - } - const isSvg = file.mimeType === 'image/svg+xml'; - const defaultEncodeType = isSvg ? 'encodeURIComponent' : 'base64'; - const encodeType = options.encodeType || defaultEncodeType; + return inlineProcess(file, asset, warn, options); + }); + + + + - // Warn for svg with hashes/fragments - if (isSvg && asset.hash && !options.ignoreFragmentWarning) { - // eslint-disable-next-line max-len - warn(`Image type is svg and link contains #. Postcss-url cant handle svg fragments. SVG file fully inlined. ${file.path}`); - } - addDependency(file.path); - const optimizeSvgEncode = isSvg && options.optimizeSvgEncode; - const encodedStr = encodeFile(file, encodeType, optimizeSvgEncode); - const resultValue = options.includeUriFragment && asset.hash - ? encodedStr + asset.hash - : encodedStr; - // wrap url by quotes if percent-encoded svg - return isSvg && encodeType !== 'base64' ? `"${resultValue}"` : resultValue; }; diff --git a/src/type/rebase.js b/src/type/rebase.js index d327618..658c2bf 100644 --- a/src/type/rebase.js +++ b/src/type/rebase.js @@ -13,7 +13,7 @@ const getAssetsPath = paths.getAssetsPath; * @param {PostcssUrl~Dir} dir * @param {PostcssUrl~Option} options * - * @returns {String|Undefined} + * @returns {Promise} */ module.exports = function(asset, dir, options) { const dest = getAssetsPath(dir.to, options && options.assetsPath || ''); @@ -21,5 +21,5 @@ module.exports = function(asset, dir, options) { path.relative(dest, asset.absolutePath) ); - return `${rebasedUrl}${asset.search}${asset.hash}`; + return Promise.resolve(`${rebasedUrl}${asset.search}${asset.hash}`); };