UNPKG

@vitbokisch/next-optimized-images

Version:

Automatically optimize images used in next.js projects (jpeg, png, gif, svg).

1,024 lines (921 loc) 28 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var module$1 = require('module'); var path = require('path'); var fs = require('fs'); var url = require('url'); var chalk = require('chalk'); var figures = require('figures'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var path__default = /*#__PURE__*/_interopDefaultLegacy(path); var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs); var chalk__default = /*#__PURE__*/_interopDefaultLegacy(chalk); var figures__default = /*#__PURE__*/_interopDefaultLegacy(figures); /** * Enriches the next.js configuration object with default config values for * next-optimized-iamges and returns it * * @param {object} nextConfig - next.js configuration object * @returns {object} enriched config */ const getConfig = (nextConfig) => ({ optimizeImages: true, optimizeImagesInDev: false, handleImages: ['jpeg', 'png', 'svg', 'webp', 'gif'], imagesFolder: 'images', imagesName: '[name]-[hash].[ext]', removeOriginalExtension: false, inlineImageLimit: 8192, defaultImageLoader: 'img-loader', mozjpeg: {}, optipng: {}, pngquant: {}, gifsicle: { interlaced: true, optimizationLevel: 3, }, svgo: {}, svgSpriteLoader: { symbolId: '[name]-[hash:8]', }, webp: {}, ...nextConfig, }); const __filename$1 = url.fileURLToPath((typeof document === 'undefined' ? new (require('u' + 'rl').URL)('file:' + __filename).href : (document.currentScript && document.currentScript.src || new URL('next-optimized-images.js.js', document.baseURI).href))); /** * Build options for the webpack file loader * * @param {object} nextConfig - next.js configuration * @param {boolean} isServer - if the build is for the server * @returns {object} */ const getFileLoaderOptions = ( { assetPrefix, imagesPublicPath, imagesOutputPath, imagesFolder, imagesName }, isServer ) => { let publicPath = `/_next/static/${imagesFolder}/`; if (imagesPublicPath) { publicPath = imagesPublicPath; } else if (assetPrefix) { publicPath = `${assetPrefix}${ assetPrefix.endsWith('/') ? '' : '/' }_next/static/${imagesFolder}/`; } return { publicPath, outputPath: imagesOutputPath || `${isServer ? '../' : ''}static/${imagesFolder}/`, name: imagesName, } }; /** * Get the file-loader path * * @returns {string} */ const getFileLoaderPath = () => { const absolutePath = path__default["default"].resolve( path__default["default"].dirname(__filename$1), '..', '..', 'node_modules', 'file-loader', 'dist', 'cjs.js' ); if (fs__default["default"].existsSync(absolutePath)) { return absolutePath } return 'file-loader' }; /** * Apply the file loader to the webpack configuration * * @param {object} webpackConfig - webpack configuration * @param {object} nextConfig - next.js configuration * @param {boolean} isServer - if the build is for the server * @param {RegExp} fileRegex - regex for files to handle * @returns {object} */ const applyFileLoader = (webpackConfig, nextConfig, isServer, fileRegex) => { webpackConfig.module.rules.push({ test: fileRegex, oneOf: [ { use: { loader: getFileLoaderPath(), options: getFileLoaderOptions(nextConfig, isServer), }, }, ], }); return webpackConfig }; /** * Build options for the webpack url loader * * @param {object} nextConfig - next.js configuration * @param {boolean} isServer - if the build is for the server * @returns {object} */ const getUrlLoaderOptions = ({ inlineImageLimit, ...config }, isServer) => ({ ...getFileLoaderOptions(config, isServer), limit: inlineImageLimit, fallback: getFileLoaderPath(), }); /** * Build options for the webpack lqip loader * * @param {object} nextConfig - next.js configuration * @param {boolean} isServer - if the build is for the server * @returns {object} */ const getLqipLoaderOptions = (nextConfig, isServer) => ({ ...getFileLoaderOptions(nextConfig, isServer), ...(nextConfig.lqip || {}), }); const require$5 = module$1.createRequire((typeof document === 'undefined' ? new (require('u' + 'rl').URL)('file:' + __filename).href : (document.currentScript && document.currentScript.src || new URL('next-optimized-images.js.js', document.baseURI).href))); /** * Build options for the webpack responsive loader * * @param {object} nextConfig - next.js configuration * @param {object} detectedLoaders - all detected and installed loaders * @returns {object} */ const getResponsiveLoaderOptions = ( { responsive, ...nextConfig }, isServer, detectedLoaders ) => { let adapter = responsive ? responsive.adapter : undefined; if (!adapter && detectedLoaders.responsiveAdapter === 'sharp') { adapter = require$5(`${detectedLoaders.responsive}${path__default["default"].sep}sharp`); // eslint-disable-line } return { ...getFileLoaderOptions(nextConfig, isServer), name: '[name]-[width]-[hash].[ext]', ...(responsive || {}), adapter, } }; /** * Apply the responsive loader to the webpack configuration * * @param {object} webpackConfig - webpack configuration * @param {object} nextConfig - next.js configuration * @param {boolean} isServer - if the build is for the server * @param {object} detectedLoaders - all detected and installed loaders * @returns {object} */ const applyResponsiveLoader = ( webpackConfig, nextConfig, isServer, detectedLoaders ) => { webpackConfig.module.rules.push({ test: /\.(jpe?g|png)$/i, oneOf: [ { use: { loader: 'responsive-loader', options: getResponsiveLoaderOptions( nextConfig, isServer, detectedLoaders ), }, }, ], }); return webpackConfig }; /** * Build options for the webpack image trace loader * * @param {object} nextConfig - next.js configuration * @returns {object} */ const getImageTraceLoaderOptions = ({ imageTrace }) => ({ ...(imageTrace || {}), }); const require$4 = module$1.createRequire((typeof document === 'undefined' ? new (require('u' + 'rl').URL)('file:' + __filename).href : (document.currentScript && document.currentScript.src || new URL('next-optimized-images.js.js', document.baseURI).href))); /** * Configure the common resource queries */ const queries = [ // ?url: force a file url/reference, never use inlining { test: 'url', loaders: [getFileLoaderPath()], optimize: true, combinations: ['original'], }, // ?inline: force inlining an image regardless of the defined limit { test: 'inline', loaders: ['url-loader'], options: [ { limit: undefined, }, ], optimize: true, combinations: ['original'], }, // ?include: include the image directly, no data uri or external file { test: 'include', loaders: [ require$4.resolve('./loaders/raw-loader/export-loader.js'), 'raw-loader', ], optimize: true, combinations: ['original'], }, // ?original: use the original image and don't optimize it { test: 'original', loaders: ['url-loader'], optimize: false, }, // ?lqip: low quality image placeholder { test: 'lqip(&|$)', loaders: [ require$4.resolve('./loaders/lqip-loader/picture-export-loader.js'), 'lqip-loader', 'url-loader', ], optimize: false, }, // ?lqip: low quality image placeholder { test: 'lqip-colors', loaders: [ require$4.resolve('./loaders/lqip-loader/colors-export-loader.js'), 'lqip-loader', 'url-loader', ], options: [ {}, { base64: false, palette: true, }, ], optimize: false, }, // ?resize: resize images { test: 'size', loaders: ['responsive-loader'], optimize: false, }, // ?trace: generate svg image traces for placeholders { test: 'trace', loaders: ['image-trace-loader', 'url-loader'], optimize: true, combinations: ['original'], }, ] /** * Add combinations */ ;[].concat(queries).forEach((queryConfig) => { if (queryConfig.combinations) { queryConfig.combinations.forEach((combination) => { if (combination === 'original') { queries.unshift({ ...queryConfig, test: `(${queryConfig.test}.*original|original.*${queryConfig.test})`, optimize: false, }); } }); } }); /** * Returns all common resource queries for the given optimization loader * * @param {object} nextConfig - next.js configuration object * @param {boolean} isServer - if the current build is for a server * @param {string} optimizerLoaderName - name of the loader used to optimize the images * @param {object} optimizerLoaderOptions - config for the optimization loader * @returns {array} */ const getResourceQueries = ( nextConfig, isServer, optimizerLoaderName, optimizerLoaderOptions, detectLoaders ) => { const loaderOptions = { 'url-loader': getUrlLoaderOptions(nextConfig, isServer), 'file-loader': getFileLoaderOptions(nextConfig, isServer), [getFileLoaderPath()]: getFileLoaderOptions(nextConfig, isServer), 'lqip-loader': getLqipLoaderOptions(nextConfig, isServer), 'responsive-loader': getResponsiveLoaderOptions( nextConfig, isServer, detectLoaders ), 'image-trace-loader': getImageTraceLoaderOptions(nextConfig), }; return queries.map((queryConfig) => { const loaders = []; queryConfig.loaders.forEach((loader, index) => { const loaderConfig = { loader, }; if (loaderOptions[loader]) { loaderConfig.options = loaderOptions[loader]; } if (queryConfig.options) { loaderConfig.options = { ...(loaderConfig.options || {}), ...(queryConfig.options[index] || {}), }; } loaders.push(loaderConfig); }); return { resourceQuery: new RegExp(queryConfig.test), use: loaders.concat( queryConfig.optimize && optimizerLoaderName !== null ? [ { loader: optimizerLoaderName, options: optimizerLoaderOptions, }, ] : [] ), } }) }; /** * Build options for the webp loader * * @param {object} nextConfig - next.js configuration * @returns {object} */ const getWebpLoaderOptions = ({ webp }) => webp || {}; /** * Apply the webp loader to the webpack configuration * * @param {object} webpackConfig - webpack configuration * @param {object} nextConfig - next.js configuration * @param {boolean} optimize - if images should get optimized * @param {boolean} isServer - if the build is for the server * @param {object} detectedLoaders - all detected and installed loaders * @returns {object} */ const applyWebpLoader = ( webpackConfig, nextConfig, optimize, isServer, detectLoaders ) => { const webpLoaders = [ { loader: 'url-loader', options: getUrlLoaderOptions(nextConfig, isServer), }, ]; if (optimize) { webpLoaders.push({ loader: 'webp-loader', options: getWebpLoaderOptions(nextConfig), }); } webpackConfig.module.rules.push({ test: /\.webp$/i, oneOf: [ // add all resource queries ...getResourceQueries( nextConfig, isServer, !optimize ? null : 'webp-loader', getWebpLoaderOptions(nextConfig), detectLoaders ), // default behavior: inline if below the definied limit, external file if above { use: webpLoaders, }, ], }); return webpackConfig }; /** * Returns the resource query definition for converting a jpeg/png image to webp * * @param {object} nextConfig - next.js configuration * @param {boolean} isServer - if the build is for the server * @returns {object} */ const getWebpResourceQuery = (nextConfig, isServer) => { const urlLoaderOptions = getUrlLoaderOptions(nextConfig, isServer); const imageName = urlLoaderOptions.name.indexOf('[ext]') >= 0 ? urlLoaderOptions.name.replace( '[ext]', nextConfig.removeOriginalExtension ? 'webp' : '[ext].webp' ) : `${urlLoaderOptions.name}.webp`; return { resourceQuery: /webp/, use: [ { loader: 'url-loader', options: Object.assign({}, urlLoaderOptions, { name: imageName, mimetype: 'image/webp', }), }, { loader: 'webp-loader', options: getWebpLoaderOptions(nextConfig), }, ], } }; const require$3 = module$1.createRequire((typeof document === 'undefined' ? new (require('u' + 'rl').URL)('file:' + __filename).href : (document.currentScript && document.currentScript.src || new URL('next-optimized-images.js.js', document.baseURI).href))); /** * Returns the resource query definition for an svg sprite image * * @param {object} nextConfig - next.js configuration * @param {object} detectedLoaders - detected loaders * @param {object} imgLoaderOptions - img loader options * @param {boolean} optimize - if the svg image should get optimized * @returns {object} */ const getSvgSpriteLoaderResourceQuery = ( nextConfig, detectedLoaders, imgLoaderOptions, optimize ) => ({ resourceQuery: /sprite/, use: [ { loader: 'svg-sprite-loader', options: { runtimeGenerator: require$3.resolve( path__default["default"].resolve(__dirname, 'svg-runtime-generator.js') ), ...(nextConfig.svgSpriteLoader || {}), }, }, ].concat( detectedLoaders.svg && optimize ? [ { loader: 'img-loader', options: imgLoaderOptions, }, ] : [] ), }); const require$2 = module$1.createRequire((typeof document === 'undefined' ? new (require('u' + 'rl').URL)('file:' + __filename).href : (document.currentScript && document.currentScript.src || new URL('next-optimized-images.js.js', document.baseURI).href))); /** * Requires an imagemin plugin and configures it * * @param {string} plugin - plugin name * @param {*} nextConfig - next.js configuration * @return {function} */ const requireImageminPlugin = (plugin, nextConfig) => { let moduleName = plugin; if (nextConfig.overwriteImageLoaderPaths) { moduleName = require$2.resolve(plugin, { paths: [nextConfig.overwriteImageLoaderPaths], }); } console.log(moduleName); /* eslint global-require: "off", import/no-dynamic-require: "off" */ return require$2(moduleName)(nextConfig[plugin.replace('imagemin-', '')] || {}) }; /** * Build options for the img loader * * @param {object} nextConfig - next.js configuration * @param {object} detectedLoaders - detected loaders * @param {boolean} optimize - if images should get optimized * @return {object} */ const getImgLoaderOptions = (nextConfig, detectedLoaders, optimize) => { if (!optimize) { return { plugins: [], } } return { plugins: [ detectedLoaders.jpeg ? requireImageminPlugin(detectedLoaders.jpeg, nextConfig) : undefined, detectedLoaders.png ? requireImageminPlugin(detectedLoaders.png, nextConfig) : undefined, detectedLoaders.svg ? requireImageminPlugin(detectedLoaders.svg, nextConfig) : undefined, detectedLoaders.gif ? requireImageminPlugin(detectedLoaders.gif, nextConfig) : undefined, ].filter(Boolean), } }; /** * Build the regex for all handled image types * * @param {object} handledImageTypes - handled image types * @return {RegExp} */ const getHandledFilesRegex = (handledImageTypes) => { const handledFiles = [ handledImageTypes.jpeg ? 'jpe?g' : null, handledImageTypes.png ? 'png' : null, handledImageTypes.svg ? 'svg' : null, handledImageTypes.gif ? 'gif' : null, ]; return new RegExp(`\\.(${handledFiles.filter(Boolean).join('|')})$`, 'i') }; /** * Apply the img loader to the webpack configuration * * @param {object} webpackConfig - webpack configuration * @param {object} nextConfig - next.js configuration * @param {boolean} optimize - if images should get optimized * @param {boolean} isServer - if the build is for the server * @param {object} detectedLoaders - detected loaders * @param {object} handledImageTypes - detected image types * @returns {object} */ const applyImgLoader = ( webpackConfig, nextConfig, optimize, isServer, detectedLoaders, handledImageTypes ) => { const imgLoaderOptions = getImgLoaderOptions( nextConfig, detectedLoaders, optimize ); webpackConfig.module.rules.push({ test: getHandledFilesRegex(handledImageTypes), oneOf: [ // add all resource queries ...getResourceQueries( nextConfig, isServer, optimize ? 'img-loader' : null, imgLoaderOptions, detectedLoaders ), // ?webp: convert an image to webp handledImageTypes.webp ? getWebpResourceQuery(nextConfig, isServer) : undefined, // ?sprite: add icon to sprite detectedLoaders.svgSprite ? getSvgSpriteLoaderResourceQuery( nextConfig, detectedLoaders, imgLoaderOptions, optimize ) : undefined, // default behavior: inline if below the definied limit, external file if above { use: [ { loader: 'url-loader', options: getUrlLoaderOptions(nextConfig, isServer), }, { loader: 'img-loader', options: imgLoaderOptions, }, ], }, ].filter(Boolean), }); return webpackConfig }; const require$1 = module$1.createRequire((typeof document === 'undefined' ? new (require('u' + 'rl').URL)('file:' + __filename).href : (document.currentScript && document.currentScript.src || new URL('next-optimized-images.js.js', document.baseURI).href))); /** * Checks if a node module is installed in the current context * * @param {string} name - module name * @param {string} resolvePath - optional resolve path * @returns {boolean} */ const isModuleInstalled = (name, resolvePath) => { try { require$1.resolve(name, resolvePath ? { paths: [resolvePath] } : undefined); return true } catch (e) { return false } }; /** * Detects all currently installed image optimization loaders * * @param {string} resolvePath - optional resolve path * @returns {object} */ const detectLoaders = (resolvePath) => { const jpeg = isModuleInstalled('imagemin-mozjpeg', resolvePath) ? 'imagemin-mozjpeg' : false; const gif = isModuleInstalled('imagemin-gifsicle', resolvePath) ? 'imagemin-gifsicle' : false; const svg = isModuleInstalled('imagemin-svgo', resolvePath) ? 'imagemin-svgo' : false; const svgSprite = isModuleInstalled('svg-sprite-loader', resolvePath) ? 'svg-sprite-loader' : false; const webp = isModuleInstalled('webp-loader', resolvePath) ? 'webp-loader' : false; const lqip = isModuleInstalled('lqip-loader', resolvePath) ? 'lqip-loader' : false; let png = false; let responsive = false; let responsiveAdapter = false; if (isModuleInstalled('imagemin-optipng', resolvePath)) { png = 'imagemin-optipng'; } else if (isModuleInstalled('imagemin-pngquant', resolvePath)) { png = 'imagemin-pngquant'; } if (isModuleInstalled('responsive-loader', resolvePath)) { responsive = require$1 .resolve( 'responsive-loader', resolvePath ? { paths: [resolvePath] } : undefined ) .replace(/(\/|\\)lib(\/|\\)index.js$/g, ''); if (isModuleInstalled('sharp', resolvePath)) { responsiveAdapter = 'sharp'; } else if (isModuleInstalled('jimp', resolvePath)) { responsiveAdapter = 'jimp'; } } return { jpeg, gif, svg, svgSprite, webp, png, lqip, responsive, responsiveAdapter, } }; /** * Checks which image types should by handled by this plugin * * @param {object} nextConfig - next.js configuration object * @returns {object} */ const getHandledImageTypes = (nextConfig) => { const { handleImages } = nextConfig; return { jpeg: handleImages.indexOf('jpeg') >= 0 || handleImages.indexOf('jpg') >= 0, png: handleImages.indexOf('png') >= 0, svg: handleImages.indexOf('svg') >= 0, webp: handleImages.indexOf('webp') >= 0, gif: handleImages.indexOf('gif') >= 0, ico: handleImages.indexOf('ico') >= 0, } }; /** * Returns the number of image optimization loaders installed * * @param {object} loaders - detected loaders * @returns {number} */ const getNumOptimizationLoadersInstalled = (loaders) => Object.values(loaders).filter( (loader) => loader && (loader.startsWith('imagemin-') || loader.startsWith('webp-') || loader.startsWith('lqip-')) ).length; /** * Appends all loaders to the webpack configuration * * @param {object} webpackConfig - webpack configuration * @param {object} nextConfig - next.js configuration * @param {object} detectedLoaders - detected loaders * @param {boolean} isServer - if the build is for the server * @param {boolean} optimize - if images should get optimized or just copied * @returns {object} */ const appendLoaders = ( webpackConfig, nextConfig, detectedLoaders, isServer, optimize ) => { let config = webpackConfig; const handledImageTypes = getHandledImageTypes(nextConfig); let imgLoaderHandledTypes = handledImageTypes; // check if responsive-loader should be the default loader and apply it if so if ( nextConfig.defaultImageLoader && nextConfig.defaultImageLoader === 'responsive-loader' ) { // img-loader no longer has to handle jpeg and png images imgLoaderHandledTypes = { ...imgLoaderHandledTypes, jpeg: false, png: false, }; config = applyResponsiveLoader( webpackConfig, nextConfig, isServer, detectLoaders ); } // apply img loader const shouldApplyImgLoader = imgLoaderHandledTypes.jpeg || imgLoaderHandledTypes.png || imgLoaderHandledTypes.gif || imgLoaderHandledTypes.svg; if ( (detectedLoaders.jpeg || detectedLoaders.png || detectedLoaders.gif || detectedLoaders.svg) && shouldApplyImgLoader ) { config = applyImgLoader( webpackConfig, nextConfig, optimize, isServer, detectedLoaders, imgLoaderHandledTypes ); } else if (shouldApplyImgLoader) { config = applyImgLoader( webpackConfig, nextConfig, false, isServer, detectedLoaders, imgLoaderHandledTypes ); } // apply webp loader if (detectedLoaders.webp && handledImageTypes.webp) { config = applyWebpLoader( webpackConfig, nextConfig, optimize, isServer, detectLoaders ); } else if (handledImageTypes.webp) { config = applyWebpLoader( webpackConfig, nextConfig, false, isServer, detectLoaders ); } // apply file loader for non optimizable image types if (handledImageTypes.ico) { config = applyFileLoader(webpackConfig, nextConfig, isServer, /\.(ico)$/i); } return config }; const prefix = `${chalk__default["default"].gray('next-optimized-images')} ${chalk__default["default"].red( figures__default["default"].pointer )}`; /** * Output a warning when images should get optimized (prod build) but no optimization * package is installed. */ const showWarning = () => console.log( // eslint-disable-line no-console `${prefix} ${chalk__default["default"].red('WARNING!')} ${prefix} ${chalk__default["default"].red('No package found which can optimize images.')} ${prefix} Starting from version ${chalk__default["default"].cyan('2')} of ${chalk__default["default"].cyan( 'next-optimized-images' )}, all optimization is optional and you can choose which ones you want to use. ${prefix} For help during the setup and installation, please read ${chalk__default["default"].underline( 'https://github.com/cyrilwanner/next-optimized-images#optimization-packages' )} ${prefix} If you recently ${chalk__default["default"].cyan( 'updated from v1 to v2' )}, please read ${chalk__default["default"].underline( 'https://github.com/cyrilwanner/next-optimized-images/blob/master/UPGRADING.md' )} ${prefix} If this is on purpose and you don't want this plugin to optimize the images, set the option ${chalk__default["default"].cyan( '`optimizeImages: false`' )} to hide this warning. ` ); /** * Configure webpack and next.js to handle and optimize images with this plugin. * * @param {object} nextConfig - configuration, see the readme for possible values * @param {object} nextComposePlugins - additional information when loaded with next-compose-plugins * @returns {object} */ const withOptimizedImages = (nextConfig = {}, nextComposePlugins = {}) => { const { optimizeImages, optimizeImagesInDev, overwriteImageLoaderPaths } = getConfig(nextConfig); return Object.assign({}, nextConfig, { webpack(config, options) { if (!options.defaultLoaders) { throw new Error( 'This plugin is not compatible with Next.js versions below 5.0.0 https://err.sh/next-plugins/upgrade' ) } const { dev, isServer } = options; let enrichedConfig = config; // detect all installed loaders const detectedLoaders = detectLoaders(overwriteImageLoaderPaths); // check if it should optimize images in the current step const optimizeInCurrentStep = nextComposePlugins && typeof nextComposePlugins.phase === 'string' ? (nextComposePlugins.phase === 'phase-production-build' && optimizeImages) || (nextComposePlugins.phase === 'phase-export' && optimizeImages) || (nextComposePlugins.phase === 'phase-development-server' && optimizeImagesInDev) : (!dev && optimizeImages) || (dev && optimizeImagesInDev); // show a warning if images should get optimized but no loader is installed if ( optimizeImages && getNumOptimizationLoadersInstalled(detectedLoaders) === 0 && isServer ) { showWarning(); } // remove (unoptimized) builtin image processing introduced in next.js 9.2 if (enrichedConfig.module.rules) { enrichedConfig.module.rules.forEach((rule) => { if (rule.oneOf) { rule.oneOf.forEach((subRule) => { if ( subRule.issuer && !subRule.test && !subRule.include && subRule.exclude && subRule.use && subRule.use.options && subRule.use.options.name ) { if ( (String(subRule.issuer.test) === '/\\.(css|scss|sass)$/' || String(subRule.issuer) === '/\\.(css|scss|sass)$/') && subRule.use.options.name.startsWith('static/media/') ) { subRule.exclude.push(/\.(jpg|jpeg|png|svg|webp|gif|ico)$/); } } }); } }); } // append loaders enrichedConfig = appendLoaders( enrichedConfig, getConfig(nextConfig), detectedLoaders, isServer, optimizeInCurrentStep ); if (typeof nextConfig.webpack === 'function') { return nextConfig.webpack(enrichedConfig, options) } return enrichedConfig }, }) }; exports["default"] = withOptimizedImages; //# sourceMappingURL=next-optimized-images.js.js.map