diff --git a/packages/docusaurus-mdx-loader/src/remark/transformImage/index.js b/packages/docusaurus-mdx-loader/src/remark/transformImage/index.js
index 1a017033a7c3..cf2088e424c6 100644
--- a/packages/docusaurus-mdx-loader/src/remark/transformImage/index.js
+++ b/packages/docusaurus-mdx-loader/src/remark/transformImage/index.js
@@ -13,17 +13,13 @@ const escapeHtml = require('escape-html');
const {getFileLoaderUtils} = require('@docusaurus/core/lib/webpack/utils');
const {posixPath, toMessageRelativeFilePath} = require('@docusaurus/utils');
-const {
- loaders: {inlineMarkdownImageFileLoader},
-} = getFileLoaderUtils();
+const {assetQuery} = getFileLoaderUtils();
const createJSX = (node, pathUrl) => {
const jsxNode = node;
jsxNode.type = 'jsx';
jsxNode.value = ``;
if (jsxNode.url) {
diff --git a/packages/docusaurus-mdx-loader/src/remark/transformLinks/index.js b/packages/docusaurus-mdx-loader/src/remark/transformLinks/index.js
index 1ac3a0890098..ad6dfd33247d 100644
--- a/packages/docusaurus-mdx-loader/src/remark/transformLinks/index.js
+++ b/packages/docusaurus-mdx-loader/src/remark/transformLinks/index.js
@@ -15,9 +15,7 @@ const escapeHtml = require('escape-html');
const {toValue} = require('../utils');
const {getFileLoaderUtils} = require('@docusaurus/core/lib/webpack/utils');
-const {
- loaders: {inlineMarkdownLinkFileLoader},
-} = getFileLoaderUtils();
+const {assetQuery} = getFileLoaderUtils();
async function ensureAssetFileExist(fileSystemAssetPath, sourceFilePath) {
const assetExists = await fs.pathExists(fileSystemAssetPath);
@@ -43,7 +41,7 @@ function toAssetRequireNode({node, filePath, requireAssetPath}) {
? relativeRequireAssetPath
: `./${relativeRequireAssetPath}`;
- const href = `require('${inlineMarkdownLinkFileLoader}${relativeRequireAssetPath}').default`;
+ const href = `require('${relativeRequireAssetPath}?${assetQuery}').default`;
const children = (node.children || []).map((n) => toValue(n)).join('');
const title = node.title ? `title="${escapeHtml(node.title)}"` : '';
diff --git a/packages/docusaurus-plugin-ideal-image/src/index.ts b/packages/docusaurus-plugin-ideal-image/src/index.ts
index c3ddf74b9311..b1e91b0628c0 100644
--- a/packages/docusaurus-plugin-ideal-image/src/index.ts
+++ b/packages/docusaurus-plugin-ideal-image/src/index.ts
@@ -31,7 +31,11 @@ export default function (
module: {
rules: [
{
- test: /\.(png|jpe?g|gif)$/i,
+ test: /\.(png|jpe?g)$/i,
+ type: 'javascript/auto',
+ generator: {
+ emit: !isServer,
+ },
use: [
require.resolve('@docusaurus/lqip-loader'),
{
diff --git a/packages/docusaurus/src/commands/build.ts b/packages/docusaurus/src/commands/build.ts
index 7074f706b6da..0a5c1c8fb7d4 100644
--- a/packages/docusaurus/src/commands/build.ts
+++ b/packages/docusaurus/src/commands/build.ts
@@ -24,6 +24,7 @@ import {
applyConfigurePostCss,
applyConfigureWebpack,
compile,
+ getFileLoaderUtils,
} from '../webpack/utils';
import CleanWebpackPlugin from '../webpack/plugins/CleanWebpackPlugin';
import {loadI18n} from '../server/i18n';
@@ -197,6 +198,11 @@ async function buildLocale({
}
});
+ // Add the very high-priority rules triggered by using a resourceQuery like ?asset
+ const {prependAssetQueryRules} = getFileLoaderUtils();
+ clientConfig = prependAssetQueryRules(clientConfig);
+ serverConfig = prependAssetQueryRules(serverConfig);
+
// Make sure generated client-manifest is cleaned first so we don't reuse
// the one from previous builds.
if (await fs.pathExists(clientManifestPath)) {
diff --git a/packages/docusaurus/src/commands/start.ts b/packages/docusaurus/src/commands/start.ts
index 2f045b48f101..48f71d61f137 100644
--- a/packages/docusaurus/src/commands/start.ts
+++ b/packages/docusaurus/src/commands/start.ts
@@ -29,6 +29,7 @@ import {
applyConfigureWebpack,
applyConfigurePostCss,
getHttpsConfig,
+ getFileLoaderUtils,
} from '../webpack/utils';
import {getCLIOptionHost, getCLIOptionPort} from './commandUtils';
import {getTranslationsLocaleDirPath} from '../server/translations/translations';
@@ -157,6 +158,10 @@ export default async function start(
}
});
+ // Add the very high-priority rules triggered by using a resourceQuery like ?asset
+ const {prependAssetQueryRules} = getFileLoaderUtils();
+ config = prependAssetQueryRules(config);
+
// https://webpack.js.org/configuration/dev-server
const devServerConfig: WebpackDevServer.Configuration = {
...{
diff --git a/packages/docusaurus/src/webpack/base.ts b/packages/docusaurus/src/webpack/base.ts
index 12637483dc67..7547685a401f 100644
--- a/packages/docusaurus/src/webpack/base.ts
+++ b/packages/docusaurus/src/webpack/base.ts
@@ -104,6 +104,7 @@ export function createBaseConfig(
chunkFilename: isProd
? 'assets/js/[name].[contenthash:8].js'
: '[name].js',
+ assetModuleFilename: 'assets/[hash][ext][query]',
publicPath: baseUrl,
},
// Don't throw warning when asset created is over 250kb
@@ -187,7 +188,7 @@ export function createBaseConfig(
fileLoaderUtils.rules.fonts(),
fileLoaderUtils.rules.media(),
fileLoaderUtils.rules.svg(),
- fileLoaderUtils.rules.otherAssets(),
+ fileLoaderUtils.rules.files(),
{
test: /\.(j|t)sx?$/,
exclude: excludeJS,
diff --git a/packages/docusaurus/src/webpack/utils.ts b/packages/docusaurus/src/webpack/utils.ts
index f9eda08d13d7..fa6a3b61d243 100644
--- a/packages/docusaurus/src/webpack/utils.ts
+++ b/packages/docusaurus/src/webpack/utils.ts
@@ -284,9 +284,11 @@ export function compile(config: Configuration[]): Promise {
});
}
-type AssetFolder = 'images' | 'files' | 'fonts' | 'medias';
+type AssetFolder = 'images' | 'files' | 'fonts' | 'medias' | 'svgs' | 'other';
type FileLoaderUtils = {
+ assetQuery: string;
+ prependAssetQueryRules: (configuration: Configuration) => Configuration;
loaders: {
file: (options: {folder: AssetFolder}) => RuleSetRule;
url: (options: {folder: AssetFolder}) => RuleSetRule;
@@ -298,34 +300,66 @@ type FileLoaderUtils = {
fonts: () => RuleSetRule;
media: () => RuleSetRule;
svg: () => RuleSetRule;
- otherAssets: () => RuleSetRule;
+ files: () => RuleSetRule;
};
};
// Inspired by https://github.com/gatsbyjs/gatsby/blob/8e6e021014da310b9cc7d02e58c9b3efe938c665/packages/gatsby/src/utils/webpack-utils.ts#L447
export function getFileLoaderUtils(): FileLoaderUtils {
- // files/images < 10kb will be inlined as base64 strings directly in the html
- const urlLoaderLimit = 10000;
+ // Asset queries are used to force the usage of the file as an asset
+ // In some case we want to opt-out o
+ // - converting an image to an ideal-image
+ // - converting an SVG to a React component
+ // - other cases
+ const assetQuery = 'asset';
+ const assetQueryRegex = /asset/;
+
+ // Threshold for datauri/file (previously set on url-loader)
+ // files/images < 10kb will be inlined as base64 strings directly in the JS bundle
+ // See https://webpack.js.org/guides/asset-modules/#general-asset-type
+ const dataUrlMaxSize = 10000;
// defines the path/pattern of the assets handled by webpack
- const fileLoaderFileName = (folder: AssetFolder) =>
+ const generatedFileName = (folder: AssetFolder) =>
`${OUTPUT_STATIC_ASSETS_DIR_NAME}/${folder}/[name]-[hash].[ext]`;
+ function fileNameGenerator(folder: AssetFolder) {
+ return {
+ filename: generatedFileName(folder),
+ };
+ }
+
+ function baseAssetRule(folder: AssetFolder): RuleSetRule {
+ return {
+ parser: {
+ dataUrlCondition: {
+ // Threshold for datauri/file (previously set on url-loader)
+ // files/images < 10kb will be inlined as base64 strings directly in the JS bundle
+ // See https://webpack.js.org/guides/asset-modules/#general-asset-type
+ maxSize: dataUrlMaxSize,
+ },
+ },
+ generator: fileNameGenerator(folder),
+ };
+ }
+
const loaders: FileLoaderUtils['loaders'] = {
+ // TODO deprecated
file: (options: {folder: AssetFolder}) => {
return {
loader: require.resolve(`file-loader`),
options: {
- name: fileLoaderFileName(options.folder),
+ name: generatedFileName(options.folder),
},
};
},
url: (options: {folder: AssetFolder}) => {
+ // TODO deprecated
return {
loader: require.resolve(`url-loader`),
options: {
- limit: urlLoaderLimit,
- name: fileLoaderFileName(options.folder),
+ limit: dataUrlMaxSize,
+ name: generatedFileName(options.folder),
fallback: require.resolve(`file-loader`),
},
};
@@ -336,85 +370,115 @@ export function getFileLoaderUtils(): FileLoaderUtils {
// Maybe with the ideal image plugin, all md images should be "ideal"?
// This is used to force url-loader+file-loader on markdown images
// https://webpack.js.org/concepts/loaders/#inline
- inlineMarkdownImageFileLoader: `!url-loader?limit=${urlLoaderLimit}&name=${fileLoaderFileName(
+ inlineMarkdownImageFileLoader: `!url-loader?limit=${dataUrlMaxSize}&name=${generatedFileName(
'images',
)}&fallback=file-loader!`,
- inlineMarkdownLinkFileLoader: `!file-loader?name=${fileLoaderFileName(
+ inlineMarkdownLinkFileLoader: `!file-loader?name=${generatedFileName(
'files',
)}!`,
};
- const rules: FileLoaderUtils['rules'] = {
- /**
- * Loads image assets, inlines images via a data URI if they are below
- * the size threshold
- */
- images: () => {
- return {
- use: [loaders.url({folder: 'images'})],
- test: /\.(ico|jpg|jpeg|png|gif|webp)(\?.*)?$/,
- };
- },
+ function imageAssetRule(): RuleSetRule {
+ return {
+ ...baseAssetRule('images'),
+ test: /\.(ico|jpg|jpeg|png|gif|webp)(\?.*)?$/,
+ };
+ }
- fonts: () => {
- return {
- use: [loaders.url({folder: 'fonts'})],
- test: /\.(woff|woff2|eot|ttf|otf)$/,
- };
- },
+ function fontAssetRule(): RuleSetRule {
+ return {
+ ...baseAssetRule('fonts'),
+ test: /\.(woff|woff2|eot|ttf|otf)$/,
+ };
+ }
- /**
- * Loads audio and video and inlines them via a data URI if they are below
- * the size threshold
- */
- media: () => {
- return {
- use: [loaders.url({folder: 'medias'})],
- test: /\.(mp4|webm|ogv|wav|mp3|m4a|aac|oga|flac)$/,
- };
- },
+ function mediaAssetRule(): RuleSetRule {
+ return {
+ ...baseAssetRule('medias'),
+ test: /\.(mp4|webm|ogv|wav|mp3|m4a|aac|oga|flac)$/,
+ };
+ }
- svg: () => {
- return {
- test: /\.svg?$/,
- oneOf: [
- {
- use: [
- {
- loader: '@svgr/webpack',
- options: {
- prettier: false,
- svgo: true,
- svgoConfig: {
- plugins: [{removeViewBox: false}],
- },
- titleProp: true,
- ref: ![path],
+ function fileAssetRule(): RuleSetRule {
+ return {
+ ...baseAssetRule('files'),
+ test: /\.(pdf|doc|docx|xls|xlsx|zip|rar)$/,
+ type: 'asset/resource',
+ };
+ }
+
+ function svgAssetRule(): RuleSetRule {
+ return {
+ ...baseAssetRule('svgs'),
+ test: /\.svg?$/,
+ };
+ }
+
+ // We convert SVG to React component when required from code only
+ // We don't convert SVG to React components when referenced in CSS
+ function svgComponentOrAssetRule(): RuleSetRule {
+ return {
+ test: /\.svg?$/,
+ oneOf: [
+ {
+ // only convert for those extensions
+ issuer: /\.(ts|tsx|js|jsx|md|mdx)$/,
+ use: [
+ {
+ loader: '@svgr/webpack',
+ options: {
+ prettier: false,
+ svgo: true,
+ svgoConfig: {
+ plugins: [{removeViewBox: false}],
},
+ titleProp: true,
+ ref: ![path],
},
- ],
- // We don't want to use SVGR loader for non-React source code
- // ie we don't want to use SVGR for CSS files...
- issuer: {
- and: [/\.(ts|tsx|js|jsx|md|mdx)$/],
},
- },
+ ],
+ },
+ svgAssetRule(),
+ ],
+ };
+ }
+
+ const rules: FileLoaderUtils['rules'] = {
+ images: imageAssetRule,
+ fonts: fontAssetRule,
+ media: mediaAssetRule,
+ svg: svgComponentOrAssetRule,
+ files: fileAssetRule,
+ };
+
+ // Those rules are triggered conditionally when using ?asset
+ // They must be added at the very beginning of the rules array
+ // Even before the rules prepended by other plugins
+ // This is a replacement for Webpack 4 file/url-loader webpack queries
+ function prependAssetQueryRules(configuration: Configuration): Configuration {
+ return mergeWithCustomize({
+ customizeArray: customizeArray({
+ 'module.rules': CustomizeRule.Prepend,
+ }),
+ })(configuration, {
+ module: {
+ rules: [
+ {...imageAssetRule(), resourceQuery: assetQueryRegex},
+ {...fontAssetRule(), resourceQuery: assetQueryRegex},
+ {...mediaAssetRule(), resourceQuery: assetQueryRegex},
+ {...svgAssetRule(), resourceQuery: assetQueryRegex},
+ // Fallback when ?asset is used but the file is unknown
{
- use: [loaders.url({folder: 'images'})],
+ type: 'asset/resource',
+ resourceQuery: assetQueryRegex,
+ generator: fileNameGenerator('files'),
},
],
- };
- },
-
- otherAssets: () => {
- return {
- use: [loaders.file({folder: 'files'})],
- test: /\.(pdf|doc|docx|xls|xlsx|zip|rar)$/,
- };
- },
- };
+ },
+ } as Configuration);
+ }
- return {loaders, rules};
+ return {loaders, rules, assetQuery, prependAssetQueryRules};
}
// Ensure the certificate and key provided are valid and if not