diff --git a/packages/docusaurus-init/templates/bootstrap/package.json b/packages/docusaurus-init/templates/bootstrap/package.json index 53cbf5284e0e..f6ec5419e43f 100644 --- a/packages/docusaurus-init/templates/bootstrap/package.json +++ b/packages/docusaurus-init/templates/bootstrap/package.json @@ -19,10 +19,8 @@ "@mdx-js/react": "^1.6.21", "@svgr/webpack": "^5.5.0", "clsx": "^1.1.1", - "file-loader": "^6.2.0", "react": "^17.0.1", - "react-dom": "^17.0.1", - "url-loader": "^4.1.1" + "react-dom": "^17.0.1" }, "browserslist": { "production": [ diff --git a/packages/docusaurus-init/templates/classic/package.json b/packages/docusaurus-init/templates/classic/package.json index 684d3cc213f9..83cceccf87dd 100644 --- a/packages/docusaurus-init/templates/classic/package.json +++ b/packages/docusaurus-init/templates/classic/package.json @@ -19,10 +19,8 @@ "@mdx-js/react": "^1.6.21", "@svgr/webpack": "^5.5.0", "clsx": "^1.1.1", - "file-loader": "^6.2.0", "react": "^17.0.1", - "react-dom": "^17.0.1", - "url-loader": "^4.1.1" + "react-dom": "^17.0.1" }, "browserslist": { "production": [ diff --git a/packages/docusaurus-init/templates/facebook/package.json b/packages/docusaurus-init/templates/facebook/package.json index 69ea8e118b49..12c97e76b45d 100644 --- a/packages/docusaurus-init/templates/facebook/package.json +++ b/packages/docusaurus-init/templates/facebook/package.json @@ -23,10 +23,8 @@ "@mdx-js/react": "^1.6.21", "@svgr/webpack": "^5.5.0", "clsx": "^1.1.1", - "file-loader": "^6.2.0", "react": "^17.0.1", - "react-dom": "^17.0.1", - "url-loader": "^4.1.1" + "react-dom": "^17.0.1" }, "devDependencies": { "@babel/eslint-parser": "^7.13.10", diff --git a/packages/docusaurus-mdx-loader/src/remark/transformImage/index.js b/packages/docusaurus-mdx-loader/src/remark/transformImage/index.js index 42122ab4a32b..4286a6338d7e 100644 --- a/packages/docusaurus-mdx-loader/src/remark/transformImage/index.js +++ b/packages/docusaurus-mdx-loader/src/remark/transformImage/index.js @@ -17,18 +17,15 @@ const { toMessageRelativeFilePath, } = require('@docusaurus/utils'); -const { - loaders: {inlineMarkdownImageFileLoader}, -} = getFileLoaderUtils(); +const {assetQuery} = getFileLoaderUtils(); const createJSX = (node, pathUrl) => { const jsxNode = node; jsxNode.type = 'jsx'; jsxNode.value = ``; diff --git a/packages/docusaurus-mdx-loader/src/remark/transformLinks/index.js b/packages/docusaurus-mdx-loader/src/remark/transformLinks/index.js index 50bc4d7f713d..51e3e583d048 100644 --- a/packages/docusaurus-mdx-loader/src/remark/transformLinks/index.js +++ b/packages/docusaurus-mdx-loader/src/remark/transformLinks/index.js @@ -19,9 +19,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); @@ -47,9 +45,9 @@ function toAssetRequireNode({node, filePath, requireAssetPath}) { ? relativeRequireAssetPath : `./${relativeRequireAssetPath}`; - const href = `require('${inlineMarkdownLinkFileLoader}${escapePath( + const href = `new URL('${escapePath( relativeRequireAssetPath, - )}').default`; + )}?${assetQuery}', import.meta.url).toString()`; 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..d3fc2fb0362c 100644 --- a/packages/docusaurus-plugin-ideal-image/src/index.ts +++ b/packages/docusaurus-plugin-ideal-image/src/index.ts @@ -31,7 +31,14 @@ export default function ( module: { rules: [ { - test: /\.(png|jpe?g|gif)$/i, + test: /\.(png|jpe?g)$/i, + resourceQuery: { + not: [/asset/], + }, + type: 'javascript/auto', + generator: { + emit: !isServer, + }, use: [ require.resolve('@docusaurus/lqip-loader'), { @@ -40,9 +47,8 @@ export default function ( emitFile: !isServer, // don't emit for server-side rendering disable: !isProd, adapter: require('@docusaurus/responsive-loader/sharp'), - name: isProd - ? 'assets/ideal-img/[name].[hash:hex:7].[width].[ext]' - : 'assets/ideal-img/[name].[width].[ext]', + name: + 'assets/ideal-img/[name]-[contenthash:8].[width].[ext]', ...options, }, }, 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 a43019b69ef9..4767221b67f1 100644 --- a/packages/docusaurus/src/webpack/base.ts +++ b/packages/docusaurus/src/webpack/base.ts @@ -108,6 +108,7 @@ export function createBaseConfig( chunkFilename: isProd ? 'assets/js/[name].[contenthash:8].js' : '[name].js', + assetModuleFilename: 'assets/[name]-[hash][ext]', publicPath: baseUrl, }, // Don't throw warning when asset created is over 250kb @@ -191,7 +192,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..2e892b590a1e 100644 --- a/packages/docusaurus/src/webpack/utils.ts +++ b/packages/docusaurus/src/webpack/utils.ts @@ -284,137 +284,157 @@ export function compile(config: Configuration[]): Promise { }); } -type AssetFolder = 'images' | 'files' | 'fonts' | 'medias'; +type AssetFolder = 'images' | 'files' | 'fonts' | 'medias' | 'svgs'; type FileLoaderUtils = { - loaders: { - file: (options: {folder: AssetFolder}) => RuleSetRule; - url: (options: {folder: AssetFolder}) => RuleSetRule; - inlineMarkdownImageFileLoader: string; - inlineMarkdownLinkFileLoader: string; - }; + assetQuery: string; + prependAssetQueryRules: (configuration: Configuration) => Configuration; rules: { images: () => RuleSetRule; 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 assetResourceQuery = /asset/; + // Can this be removed? see https://github.com/facebook/docusaurus/commit/2f21d306bdd4d286cc5d25c81adaea2fc77f0474#commitcomment-50223144) + const notAssetResourceQuery: RuleSetRule['resourceQuery'] = {not: [/asset/]}; // defines the path/pattern of the assets handled by webpack - const fileLoaderFileName = (folder: AssetFolder) => - `${OUTPUT_STATIC_ASSETS_DIR_NAME}/${folder}/[name]-[hash].[ext]`; - - const loaders: FileLoaderUtils['loaders'] = { - file: (options: {folder: AssetFolder}) => { - return { - loader: require.resolve(`file-loader`), - options: { - name: fileLoaderFileName(options.folder), - }, - }; - }, - url: (options: {folder: AssetFolder}) => { - return { - loader: require.resolve(`url-loader`), - options: { - limit: urlLoaderLimit, - name: fileLoaderFileName(options.folder), - fallback: require.resolve(`file-loader`), + function fileNameGenerator(folder: AssetFolder) { + return { + filename: `${OUTPUT_STATIC_ASSETS_DIR_NAME}/${folder}/[name]-[hash][ext]`, + }; + } + + function baseAssetRule(folder: AssetFolder): RuleSetRule { + return { + type: 'asset', + 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: 10 * 1024, }, - }; - }, + }, + generator: fileNameGenerator(folder), + resourceQuery: notAssetResourceQuery, + }; + } - // TODO find a better solution to avoid conflicts with the ideal-image plugin - // TODO this may require a little breaking change for ideal-image users? - // 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( - 'images', - )}&fallback=file-loader!`, - inlineMarkdownLinkFileLoader: `!file-loader?name=${fileLoaderFileName( - 'files', - )}!`, - }; + function imageAssetRule(): RuleSetRule { + return { + ...baseAssetRule('images'), + test: /\.(ico|jpg|jpeg|png|gif|webp)(\?.*)?$/i, + }; + } - 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 fontAssetRule(): RuleSetRule { + return { + ...baseAssetRule('fonts'), + test: /\.(woff|woff2|eot|ttf|otf)$/i, + }; + } - fonts: () => { - return { - use: [loaders.url({folder: 'fonts'})], - test: /\.(woff|woff2|eot|ttf|otf)$/, - }; - }, + function mediaAssetRule(): RuleSetRule { + return { + ...baseAssetRule('medias'), + test: /\.(mp4|webm|ogv|wav|mp3|m4a|aac|oga|flac)$/i, + }; + } - /** - * 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 fileAssetRule(): RuleSetRule { + return { + ...baseAssetRule('files'), + test: /\.(pdf|doc|docx|xls|xlsx|zip|rar)$/i, + type: 'asset/resource', + }; + } - svg: () => { - return { - test: /\.svg?$/, - oneOf: [ - { - use: [ - { - loader: '@svgr/webpack', - options: { - prettier: false, - svgo: true, - svgoConfig: { - plugins: [{removeViewBox: false}], - }, - titleProp: true, - ref: ![path], + function svgAssetRule(): RuleSetRule { + return { + ...baseAssetRule('svgs'), + test: /\.svg?$/i, + }; + } + + // 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?$/i, + resourceQuery: notAssetResourceQuery, + 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: assetResourceQuery}, + {...fontAssetRule(), resourceQuery: assetResourceQuery}, + {...mediaAssetRule(), resourceQuery: assetResourceQuery}, + {...svgAssetRule(), resourceQuery: assetResourceQuery}, + // Fallback when ?asset is used but the file is unknown { - use: [loaders.url({folder: 'images'})], + type: 'asset/resource', + resourceQuery: assetResourceQuery, + generator: fileNameGenerator('files'), }, ], - }; - }, - - otherAssets: () => { - return { - use: [loaders.file({folder: 'files'})], - test: /\.(pdf|doc|docx|xls|xlsx|zip|rar)$/, - }; - }, - }; + }, + } as Configuration); + } - return {loaders, rules}; + return {rules, assetQuery, prependAssetQueryRules}; } // Ensure the certificate and key provided are valid and if not diff --git a/website/docs/docusaurus-core.md b/website/docs/docusaurus-core.md index 6344bfcf9a3e..2e38ad249f25 100644 --- a/website/docs/docusaurus-core.md +++ b/website/docs/docusaurus-core.md @@ -303,7 +303,7 @@ In most cases, you don't need `useBaseUrl`. Prefer a `require()` call for [assets](./guides/markdown-features/markdown-features-assets.mdx): ```jsx - + ``` ::: diff --git a/website/docs/guides/markdown-features/markdown-features-assets.mdx b/website/docs/guides/markdown-features/markdown-features-assets.mdx index 83d6514c7741..b9398ca9499b 100644 --- a/website/docs/guides/markdown-features/markdown-features-assets.mdx +++ b/website/docs/guides/markdown-features/markdown-features-assets.mdx @@ -28,7 +28,7 @@ You can use images in Markdown, or by requiring them and using a JSX image tag: # My Markdown page Example banner @@ -64,9 +64,7 @@ In the same way, you can link to existing assets by requiring them and using the ```mdx # My Markdown page - + Download this PDF @@ -77,7 +75,10 @@ or + href={new URL( + '../../assets/docusaurus-asset-example-pdf.pdf', + import.meta.url, + ).toString()}> Download this PDF diff --git a/website/docs/static-assets.md b/website/docs/static-assets.md index 9224e655776c..07e8f3a2f5f9 100644 --- a/website/docs/static-assets.md +++ b/website/docs/static-assets.md @@ -27,7 +27,7 @@ import DocusaurusImageUrl from '@site/static/img/docusaurus.png'; ``` ```jsx title="MyComponent.js" - + ``` ```jsx title="MyComponent.js"