From c49793cd3dca4c4e60d8646a55298c1c391d37fa Mon Sep 17 00:00:00 2001 From: QWp6t Date: Sun, 6 Nov 2016 03:49:41 -0800 Subject: [PATCH] Add images to assets manifest --- assets/build/config.js | 15 +- .../build/util/assetsPluginProcessOutput.js | 7 +- assets/build/util/interpolateName.js | 50 ++++++ assets/build/util/promisify.js | 21 +++ assets/build/webpack.config.js | 33 ++-- assets/build/webpack.config.production.js | 4 +- assets/build/webpack.plugin.copyglobs.js | 143 ++++++++++++++++++ package.json | 7 +- yarn.lock | 69 ++++----- 9 files changed, 270 insertions(+), 79 deletions(-) create mode 100644 assets/build/util/interpolateName.js create mode 100644 assets/build/util/promisify.js create mode 100644 assets/build/webpack.plugin.copyglobs.js diff --git a/assets/build/config.js b/assets/build/config.js index 5de0b80f12..5725b08571 100644 --- a/assets/build/config.js +++ b/assets/build/config.js @@ -1,6 +1,6 @@ const path = require('path'); const argv = require('minimist')(process.argv.slice(2)); -const glob = require('glob-all'); +const uniq = require('lodash/uniq'); const mergeWithConcat = require('./util/mergeWithConcat'); const userConfig = require('../config'); @@ -11,7 +11,7 @@ const rootPath = (userConfig.paths && userConfig.paths.root) : process.cwd(); const config = mergeWithConcat({ - copy: ['images/**/*'], + copy: 'images/**/*', proxyUrl: 'http://localhost:3000', cacheBusting: '[name]_[hash]', paths: { @@ -29,11 +29,8 @@ const config = mergeWithConcat({ watch: [], }, userConfig); -const files = glob.sync(config.copy, { - cwd: config.paths.assets, - mark: true, -}).filter(file => !((file.slice(-1) === '/') || (!file.indexOf('*') === -1))) - .map(file => path.join(config.paths.assets, file)); +config.watch.push(config.copy); +config.watch = uniq(config.watch); Object.keys(config.entry).forEach(id => config.entry[id].unshift(path.join(__dirname, 'public-path.js'))); @@ -41,8 +38,6 @@ Object.keys(config.entry).forEach(id => module.exports = mergeWithConcat(config, { env: Object.assign({ production: isProduction, development: !isProduction }, argv.env), publicPath: `${config.publicPath}/${path.basename(config.paths.dist)}/`, + manifest: {}, }); -if (files.length) { - module.exports = mergeWithConcat(module.exports, { entry: { files } }); -} diff --git a/assets/build/util/assetsPluginProcessOutput.js b/assets/build/util/assetsPluginProcessOutput.js index d894d5cff1..246efd7a20 100644 --- a/assets/build/util/assetsPluginProcessOutput.js +++ b/assets/build/util/assetsPluginProcessOutput.js @@ -1,3 +1,4 @@ + const path = require('path'); /** @@ -7,12 +8,12 @@ const path = require('path'); * @return {String} JSON */ module.exports = (assets) => { - const results = {}; + const manifest = {}; Object.keys(assets).forEach((name) => { Object.keys(assets[name]).forEach((ext) => { const filename = `${path.dirname(assets[name][ext])}/${path.basename(`${name}.${ext}`)}`; - results[filename] = assets[name][ext]; + manifest[filename] = assets[name][ext]; }); }); - return JSON.stringify(results); + return manifest; }; diff --git a/assets/build/util/interpolateName.js b/assets/build/util/interpolateName.js new file mode 100644 index 0000000000..d96490f5b6 --- /dev/null +++ b/assets/build/util/interpolateName.js @@ -0,0 +1,50 @@ +'use strict'; // eslint-disable-line + +const path = require('path'); +const utils = require('loader-utils'); + +/** + * Generate output name from output pattern + * + * @link https://github.com/kevlened/copy-webpack-plugin/blob/323b1d74ef35ed2221637d8028b1bef854deb523/src/writeFile.js#L31-L65 + * @param {string} pattern + * @param {string} relativeFrom + * @param {binary} content + * @return {string} + */ +module.exports = (pattern, relativeFrom, content) => { + let webpackTo = pattern; + let resourcePath = relativeFrom; + + /* A hack so .dotted files don't get parsed as extensions */ + const basename = path.basename(resourcePath); + let dotRemoved = false; + if (basename[0] === '.') { + dotRemoved = true; + resourcePath = path.join(path.dirname(resourcePath), basename.slice(1)); + } + + /** + * If it doesn't have an extension, remove it from the pattern + * ie. [name].[ext] or [name][ext] both become [name] + */ + if (!path.extname(resourcePath)) { + webpackTo = webpackTo.replace(/\.?\[ext]/g, ''); + } + + /** + * A hack because loaderUtils.interpolateName doesn't + * find the right path if no directory is defined + * ie. [path] applied to 'file.txt' would return 'file' + */ + if (resourcePath.indexOf('/') < 0) { + resourcePath = `/${resourcePath}`; + } + + webpackTo = utils.interpolateName({ resourcePath }, webpackTo, { content }); + + if (dotRemoved) { + webpackTo = path.join(path.dirname(webpackTo), `.${path.basename(webpackTo)}`); + } + return webpackTo; +}; diff --git a/assets/build/util/promisify.js b/assets/build/util/promisify.js new file mode 100644 index 0000000000..65a64551a3 --- /dev/null +++ b/assets/build/util/promisify.js @@ -0,0 +1,21 @@ +/** + * Node-style asynchronous function. + * + * @callback nodeAsyncCallback + * @param {string|null} err + * @param {*} data + */ +/** + * Promisify node-style asynchronous functions + * + * @param {nodeAsyncCallback} fn - Function with node-style callback + * @param {this} [scope] - Scope to which the function should be bound. Default: fn + * @returns {Promise} - An instance of Promise + */ +module.exports = (fn, scope) => function callback() { + const args = [].slice.call(arguments); + return new Promise((resolve, reject) => { + args.push((err, data) => (err === null ? resolve(data) : reject(err))); + return fn.apply(scope || fn, args); + }); +}; diff --git a/assets/build/webpack.config.js b/assets/build/webpack.config.js index 07fb4a990e..4d17fa184d 100644 --- a/assets/build/webpack.config.js +++ b/assets/build/webpack.config.js @@ -6,6 +6,7 @@ const ExtractTextPlugin = require('extract-text-webpack-plugin'); const ImageminPlugin = require('imagemin-webpack-plugin').default; const imageminMozjpeg = require('imagemin-mozjpeg'); +const CopyGlobsPlugin = require('./webpack.plugin.copyglobs'); const mergeWithConcat = require('./util/mergeWithConcat'); const addHotMiddleware = require('./util/addHotMiddleware'); const webpackConfigProduction = require('./webpack.config.production'); @@ -75,7 +76,7 @@ const webpackConfig = { include: config.paths.assets, loaders: [ `file?${qs.stringify({ - name: '[path][name].[ext]', + name: `[path]${assetsFilenames}.[ext]`, })}`, ], }, @@ -118,25 +119,19 @@ const webpackConfig = { }, plugins: [ new CleanPlugin([config.paths.dist], config.paths.root), + new CopyGlobsPlugin({ + // It would be nice to switch to copy-webpack-plugin, but unfortunately it doesn't + // provide a reliable way of tracking the before/after file names + pattern: config.copy, + output: `[path]${assetsFilenames}.[ext]`, + manifest: config.manifest, + }), new ImageminPlugin({ - optipng: { - optimizationLevel: 7, - }, - gifsicle: { - optimizationLevel: 3, - }, - pngquant: { - quality: '65-90', - speed: 4, - }, - svgo: { - removeUnknownsAndDefaults: false, - cleanupIDs: false, - }, - jpegtran: null, - plugins: [imageminMozjpeg({ - quality: 75, - })], + optipng: { optimizationLevel: 7 }, + gifsicle: { optimizationLevel: 3 }, + pngquant: { quality: '65-90', speed: 4 }, + svgo: { removeUnknownsAndDefaults: false, cleanupIDs: false }, + plugins: [imageminMozjpeg({ quality: 75 })], disable: (config.enabled.watcher), }), new ExtractTextPlugin({ diff --git a/assets/build/webpack.config.production.js b/assets/build/webpack.config.production.js index ab8de65d13..e3f23ec59f 100644 --- a/assets/build/webpack.config.production.js +++ b/assets/build/webpack.config.production.js @@ -11,7 +11,9 @@ module.exports = { path: config.paths.dist, filename: 'assets.json', fullPath: false, - processOutput, + processOutput(assets) { + return JSON.stringify(Object.assign(processOutput(assets), config.manifest)); + }, }), new OptimizeCssAssetsPlugin({ cssProcessor: cssnano, diff --git a/assets/build/webpack.plugin.copyglobs.js b/assets/build/webpack.plugin.copyglobs.js new file mode 100644 index 0000000000..76202d00ae --- /dev/null +++ b/assets/build/webpack.plugin.copyglobs.js @@ -0,0 +1,143 @@ +'use strict'; // eslint-disable-line + +const fs = require('fs'); +const path = require('path'); +const glob = require('glob'); +const utils = require('loader-utils'); +const includes = require('lodash/includes'); + +const interpolateName = require('./util/interpolateName'); +const promisify = require('./util/promisify'); + +const fixPath = v => v.replace(/\\/g, '/'); +const errorMsg = msg => `\x1b[31m${msg}\x1b[0m`; + +const GLOB_CWD_AUTO = null; + +const globAsync = promisify(glob); +const statAsync = promisify(fs.stat); +const readFileAsync = promisify(fs.readFile); + +class PatternUndefinedError extends Error { + constructor() { + super(errorMsg('[copy-globs] You must provide glob pattern.')); + } +} + +class ArgsArrayError extends TypeError { + constructor() { + super(errorMsg( + '[copy-globs] pattern cannot be an array.\n' + + 'For multiple folders, use something like:\n\n' + + ' +(images|fonts)/**/*\n\n' + + 'See also: https://github.com/isaacs/node-glob#glob-primer\n' + )); + } +} + +/** + * Throws an error if pattern is an array or undefined + * + * @param pattern + */ +const testPattern = (pattern) => { + if (pattern === undefined) { + throw new PatternUndefinedError(); + } + if (Array.isArray(pattern)) { + throw new ArgsArrayError(); + } +}; + +const normalizeArguments = (input) => { + testPattern(input); + const options = {}; + if (typeof input === 'string') { + options.pattern = input; + } else { + testPattern(input.pattern); + return input; + } + return options; +}; + +module.exports = class { + constructor(o) { + const options = normalizeArguments(o); + this.pattern = options.pattern; + this.disable = options.disable; + this.output = options.output || '[path][name].[ext]'; + this.globOptions = Object.assign(options.globOptions || {}, { cwd: GLOB_CWD_AUTO }); + this.globOptions.nodir = true; + this.manifest = options.manifest || {}; + this.files = []; + } + apply(compiler) { + if (this.disable) { + return; + } + this.compiler = compiler; + this.resolveWorkingDirectory(); + compiler.plugin('emit', this.emitHandler.bind(this)); + compiler.plugin('after-emit', this.afterEmitHandler.bind(this)); + } + emitHandler(compilation, callback) { + this.compilation = compilation; + globAsync(this.pattern, this.globOptions) + .then( + paths => Promise.all(paths.map(this.processAsset.bind(this))), + err => compilation.errors.push(err) + ) + .then(() => { + Object.keys(this.files).forEach((absoluteFrom) => { + const file = this.files[absoluteFrom]; + this.manifest[file.relativeFrom] = file.webpackTo; + this.compilation.assets[file.webpackTo] = { + size: () => file.stat.size, + source: () => file.content, + }; + }); + }) + .then(callback); + } + afterEmitHandler(compilation, callback) { + Object.keys(this.files) + .filter(absoluteFrom => !includes(compilation.fileDependencies, absoluteFrom)) + .forEach(absoluteFrom => compilation.fileDependencies.push(absoluteFrom)); + callback(); + } + resolveWorkingDirectory() { + if (this.globOptions.cwd === GLOB_CWD_AUTO) { + this.globOptions.cwd = this.compiler.options.context; + } + this.context = this.globOptions.cwd || this.compiler.options.context; + } + processAsset(relativeFrom) { + if (this.compilation.assets[relativeFrom]) { + return Promise.resolve(); + } + const absoluteFrom = path.resolve(this.context, relativeFrom); + return statAsync(absoluteFrom) + .then(stat => this.buildFileObject(relativeFrom, absoluteFrom, stat)) + .then(this.addAsset.bind(this)); + } + buildFileObject(relativeFrom, absoluteFrom, stat) { + return readFileAsync(absoluteFrom) + .then((content) => { + const hash = utils.getHashDigest(content); + const webpackTo = fixPath(interpolateName(this.output, relativeFrom, content)); + return { relativeFrom, absoluteFrom, stat, content, hash, webpackTo }; + }); + } + addAsset(file) { + const asset = this.getAsset(file.absoluteFrom); + if (asset && asset.hash === file.hash) { + return null; + } + this.files[file.absoluteFrom] = file; + return file; + } + getAsset(absoluteFrom) { + return this.files[absoluteFrom]; + } +}; diff --git a/package.json b/package.json index c120a4feab..bfdeb6662f 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "body-parser": "^1.15.2", "browser-sync": "^2.17.5", "buble": "^0.14.2", - "buble-loader": "^0.3.1", + "buble-loader": "^0.3.2", "clean-webpack-plugin": "^0.1.13", "css-loader": "^0.25.0", "cssnano": "^3.8.0", @@ -46,14 +46,15 @@ "eslint-plugin-react": "^6.5.0", "extract-text-webpack-plugin": "^2.0.0-beta.4", "file-loader": "^0.9.0", - "glob-all": "^3.1.0", + "glob": "^7.1.1", "imagemin-mozjpeg": "^6.0.0", "imagemin-webpack-plugin": "^1.2.1", "imports-loader": "^0.6.5", + "loader-utils": "^0.2.16", "lodash": "^4.16.6", "minimist": "^1.2.0", "monkey-hot-loader": "github:rmarscher/monkey-hot-loader#webpack2-import", - "node-sass": "^3.11.0", + "node-sass": "^3.11.2", "optimize-css-assets-webpack-plugin": "^1.3.0", "postcss": "^5.2.5", "postcss-loader": "^1.1.0", diff --git a/yarn.lock b/yarn.lock index dc37242d24..0f74c70d0f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -189,7 +189,7 @@ assert@^1.1.1: dependencies: util "0.10.3" -assets-webpack-plugin@^3.5.0: +assets-webpack-plugin: version "3.5.0" resolved "https://registry.yarnpkg.com/assets-webpack-plugin/-/assets-webpack-plugin-3.5.0.tgz#933b16bf679c7510dd3475e4df9ba495d9dc0368" dependencies: @@ -215,7 +215,7 @@ async-foreach@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542" -"async-throttle@github:zeit/async-throttle#596dc23b4ac2598cfed4d30289fb75c29f8d57fe": +async-throttle@zeit/async-throttle#596dc23b4ac2598cfed4d30289fb75c29f8d57fe: version "0.0.1" resolved "https://codeload.github.com/zeit/async-throttle/tar.gz/596dc23b4ac2598cfed4d30289fb75c29f8d57fe" @@ -571,9 +571,9 @@ bs-recipes@1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/bs-recipes/-/bs-recipes-1.2.3.tgz#0e4d17bb1cff92ef6c36608b8487d9a07571ac54" -buble-loader@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/buble-loader/-/buble-loader-0.3.1.tgz#2cab8c4b5578ff0dbcbc51c97f2f919d79cdbe66" +buble-loader@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/buble-loader/-/buble-loader-0.3.2.tgz#b228ead153f7d5c46bef2150d6d5c86c9c27bf4a" dependencies: loader-utils "^0.2.15" @@ -744,8 +744,8 @@ clap@^1.0.9: chalk "^1.1.3" clean-webpack-plugin@^0.1.13: - version "0.1.13" - resolved "https://registry.yarnpkg.com/clean-webpack-plugin/-/clean-webpack-plugin-0.1.13.tgz#c2bae9bc4dd44348123ad98e19fa55e7a9b07600" + version "0.1.14" + resolved "https://registry.yarnpkg.com/clean-webpack-plugin/-/clean-webpack-plugin-0.1.14.tgz#72e577e90d38a5b8a42a2d3a9c055c45b84e2194" dependencies: rimraf "~2.5.1" @@ -2044,12 +2044,16 @@ gifsicle@^3.0.0: bin-wrapper "^3.0.0" logalot "^2.0.0" -glob-all@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/glob-all/-/glob-all-3.1.0.tgz#8913ddfb5ee1ac7812656241b03d5217c64b02ab" +glob, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@~7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" dependencies: - glob "^7.0.5" - yargs "~1.2.6" + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.2" + once "^1.3.0" + path-is-absolute "^1.0.0" glob-base@^0.3.0: version "0.3.0" @@ -2094,17 +2098,6 @@ glob@^5.0.3: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@~7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.2" - once "^1.3.0" - path-is-absolute "^1.0.0" - globals@^9.2.0: version "9.12.0" resolved "https://registry.yarnpkg.com/globals/-/globals-9.12.0.tgz#992ce90828c3a55fa8f16fada177adb64664cf9d" @@ -2691,8 +2684,8 @@ is-stream@^1.0.0, is-stream@^1.0.1, is-stream@^1.1.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" is-svg@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-2.0.1.tgz#f93ab3bf1d6bbca30e9753cd3485b1300eebc013" + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-2.1.0.tgz#cf61090da0d9efbcab8722deba6f032208dbb0e9" dependencies: html-comment-regex "^1.1.0" @@ -2895,7 +2888,7 @@ loader-runner@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.2.0.tgz#824c1b699c4e7a2b6501b85902d5b862bf45b3fa" -loader-utils@^0.2.11, loader-utils@^0.2.12, loader-utils@^0.2.15, loader-utils@^0.2.16, loader-utils@^0.2.3, loader-utils@^0.2.7, loader-utils@~0.2.2, loader-utils@~0.2.5, loader-utils@0.2.x: +loader-utils, loader-utils@^0.2.11, loader-utils@^0.2.12, loader-utils@^0.2.15, loader-utils@^0.2.16, loader-utils@^0.2.3, loader-utils@^0.2.7, loader-utils@~0.2.2, loader-utils@~0.2.5, loader-utils@0.2.x: version "0.2.16" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.16.tgz#f08632066ed8282835dff88dfb52704765adee6d" dependencies: @@ -3292,10 +3285,6 @@ minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@~3.0.2, "minimat dependencies: brace-expansion "^1.0.0" -minimist@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.1.0.tgz#99df657a52574c21c9057497df742790b2b4c0de" - minimist@^1.1.0, minimist@^1.1.3, minimist@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" @@ -3417,9 +3406,9 @@ node-pre-gyp@^0.6.29: tar "~2.2.1" tar-pack "~3.3.0" -node-sass@^3.11.0: - version "3.11.1" - resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-3.11.1.tgz#0b25699663cc9d616e8c6fb874e7d9b25e5a8e20" +node-sass@^3.11.2: + version "3.11.2" + resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-3.11.2.tgz#ba3bef8cd46bd1698c2bdb2b6d2af1f9e7d7878f" dependencies: async-foreach "^0.1.3" chalk "^1.1.1" @@ -4296,8 +4285,8 @@ regenerate@^1.2.1: resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.1.tgz#0300203a5d2fdcf89116dce84275d011f5903f33" regenerator-runtime@^0.9.5: - version "0.9.5" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.9.5.tgz#403d6d40a4bdff9c330dd9392dcbb2d9a8bba1fc" + version "0.9.6" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.9.6.tgz#d33eb95d0d2001a4be39659707c51b0cb71ce029" regex-cache@^0.4.2: version "0.4.3" @@ -4404,8 +4393,8 @@ require-main-filename@^1.0.1: resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" require-uncached@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.2.tgz#67dad3b733089e77030124678a459589faf6a7ec" + version "1.0.3" + resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" dependencies: caller-path "^0.1.0" resolve-from "^1.0.0" @@ -5461,12 +5450,6 @@ yargs@^4.7.1: y18n "^3.2.1" yargs-parser "^2.4.1" -yargs@~1.2.6: - version "1.2.6" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-1.2.6.tgz#9c7b4a82fd5d595b2bf17ab6dcc43135432fe34b" - dependencies: - minimist "^0.1.0" - yargs@~3.10.0: version "3.10.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1"