diff --git a/packages/docusaurus-plugin-ideal-image/src/index.ts b/packages/docusaurus-plugin-ideal-image/src/index.ts index 2921208e9792..187e168526b4 100644 --- a/packages/docusaurus-plugin-ideal-image/src/index.ts +++ b/packages/docusaurus-plugin-ideal-image/src/index.ts @@ -25,6 +25,9 @@ export default function ( configureWebpack(_config: Configuration, isServer: boolean) { return { + mergeStrategy: { + 'module.rules': 'prepend', + }, module: { rules: [ { diff --git a/packages/docusaurus-types/package.json b/packages/docusaurus-types/package.json index c143aea97ae9..1352c439a104 100644 --- a/packages/docusaurus-types/package.json +++ b/packages/docusaurus-types/package.json @@ -11,6 +11,7 @@ "dependencies": { "@types/webpack": "^4.41.0", "commander": "^4.0.1", - "querystring": "0.2.0" + "querystring": "0.2.0", + "webpack-merge": "^4.2.2" } } diff --git a/packages/docusaurus-types/src/index.d.ts b/packages/docusaurus-types/src/index.d.ts index 140f0f0438e4..e95737a2bcc6 100644 --- a/packages/docusaurus-types/src/index.d.ts +++ b/packages/docusaurus-types/src/index.d.ts @@ -8,6 +8,7 @@ import {Loader, Configuration} from 'webpack'; import {Command} from 'commander'; import {ParsedUrlQueryInput} from 'querystring'; +import {MergeStrategy} from 'webpack-merge'; export interface DocusaurusConfig { baseUrl: string; @@ -118,7 +119,7 @@ export interface Plugin { config: Configuration, isServer: boolean, utils: ConfigureWebpackUtils, - ): Configuration; + ): Configuration & {mergeStrategy?: ConfigureWebpackFnMergeStrategy}; getThemePath?(): string; getTypeScriptThemePath?(): string; getPathsToWatch?(): string[]; @@ -131,6 +132,9 @@ export interface Plugin { }; } +export type ConfigureWebpackFn = Plugin['configureWebpack']; +export type ConfigureWebpackFnMergeStrategy = Record; + export type PluginConfig = | [string, Record] | [string] diff --git a/packages/docusaurus/package.json b/packages/docusaurus/package.json index 7faf83f049eb..a8954b855cdb 100644 --- a/packages/docusaurus/package.json +++ b/packages/docusaurus/package.json @@ -56,6 +56,7 @@ "del": "^5.1.0", "eta": "^1.1.1", "express": "^4.17.1", + "file-loader": "^6.0.0", "fs-extra": "^8.1.0", "globby": "^10.0.1", "html-minifier-terser": "^5.0.5", @@ -84,6 +85,7 @@ "shelljs": "^0.8.4", "std-env": "^2.2.1", "terser-webpack-plugin": "^2.3.5", + "url-loader": "^4.1.0", "wait-file": "^1.0.5", "webpack": "^4.41.2", "webpack-bundle-analyzer": "^3.6.1", diff --git a/packages/docusaurus/src/webpack/__tests__/utils.test.ts b/packages/docusaurus/src/webpack/__tests__/utils.test.ts index c6b5f189f1e3..993acd35af9d 100644 --- a/packages/docusaurus/src/webpack/__tests__/utils.test.ts +++ b/packages/docusaurus/src/webpack/__tests__/utils.test.ts @@ -5,23 +5,33 @@ * LICENSE file in the root directory of this source tree. */ -import {validate} from 'webpack'; +import { + // @ts-expect-error: seems it's not in the typedefs??? + validate, + Configuration, +} from 'webpack'; import path from 'path'; import {applyConfigureWebpack} from '../utils'; +import { + ConfigureWebpackFn, + ConfigureWebpackFnMergeStrategy, +} from '@docusaurus/types'; describe('extending generated webpack config', () => { test('direct mutation on generated webpack config object', async () => { // fake generated webpack config - let config = { + let config: Configuration = { output: { path: __dirname, filename: 'bundle.js', }, }; - /* eslint-disable */ - const configureWebpack = (generatedConfig, isServer) => { + const configureWebpack: ConfigureWebpackFn = ( + generatedConfig, + isServer, + ) => { if (!isServer) { generatedConfig.entry = 'entry.js'; generatedConfig.output = { @@ -29,8 +39,8 @@ describe('extending generated webpack config', () => { filename: 'new.bundle.js', }; } + return {}; }; - /* eslint-enable */ config = applyConfigureWebpack(configureWebpack, config, false); expect(config).toEqual({ @@ -45,23 +55,20 @@ describe('extending generated webpack config', () => { }); test('webpack-merge with user webpack config object', async () => { - // fake generated webpack config - let config = { + let config: Configuration = { output: { path: __dirname, filename: 'bundle.js', }, }; - /* eslint-disable */ - const configureWebpack = { + const configureWebpack: ConfigureWebpackFn = () => ({ entry: 'entry.js', output: { path: path.join(__dirname, 'dist'), filename: 'new.bundle.js', }, - }; - /* eslint-enable */ + }); config = applyConfigureWebpack(configureWebpack, config, false); expect(config).toEqual({ @@ -74,4 +81,54 @@ describe('extending generated webpack config', () => { const errors = validate(config); expect(errors.length).toBe(0); }); + + test('webpack-merge with custom strategy', async () => { + const config: Configuration = { + module: { + rules: [{use: 'xxx'}, {use: 'yyy'}], + }, + }; + + const createConfigureWebpack: ( + mergeStrategy?: ConfigureWebpackFnMergeStrategy, + ) => ConfigureWebpackFn = (mergeStrategy) => () => ({ + module: { + rules: [{use: 'zzz'}], + }, + mergeStrategy, + }); + + const defaultStrategyMergeConfig = applyConfigureWebpack( + createConfigureWebpack(), + config, + false, + ); + expect(defaultStrategyMergeConfig).toEqual({ + module: { + rules: [{use: 'xxx'}, {use: 'yyy'}, {use: 'zzz'}], + }, + }); + + const prependRulesStrategyConfig = applyConfigureWebpack( + createConfigureWebpack({'module.rules': 'prepend'}), + config, + false, + ); + expect(prependRulesStrategyConfig).toEqual({ + module: { + rules: [{use: 'zzz'}, {use: 'xxx'}, {use: 'yyy'}], + }, + }); + + const uselessMergeStrategyConfig = applyConfigureWebpack( + createConfigureWebpack({uselessAttributeName: 'append'}), + config, + false, + ); + expect(uselessMergeStrategyConfig).toEqual({ + module: { + rules: [{use: 'xxx'}, {use: 'yyy'}, {use: 'zzz'}], + }, + }); + }); }); diff --git a/packages/docusaurus/src/webpack/base.ts b/packages/docusaurus/src/webpack/base.ts index c69c2d20cfc9..7ce0b7720a3d 100644 --- a/packages/docusaurus/src/webpack/base.ts +++ b/packages/docusaurus/src/webpack/base.ts @@ -14,7 +14,12 @@ import TerserPlugin from 'terser-webpack-plugin'; import {Configuration, Loader} from 'webpack'; import {Props} from '@docusaurus/types'; -import {getBabelLoader, getCacheLoader, getStyleLoaders} from './utils'; +import { + getBabelLoader, + getCacheLoader, + getStyleLoaders, + getFileLoaderUtils, +} from './utils'; import {BABEL_CONFIG_FILE_NAME} from '../constants'; const CSS_REGEX = /\.css$/; @@ -48,6 +53,8 @@ export function createBaseConfig( BABEL_CONFIG_FILE_NAME, ); + const fileLoaderUtils = getFileLoaderUtils(); + return { mode: isProd ? 'production' : 'development', output: { @@ -158,6 +165,9 @@ export function createBaseConfig( }, module: { rules: [ + fileLoaderUtils.rules.images(), + fileLoaderUtils.rules.media(), + fileLoaderUtils.rules.otherAssets(), { test: /\.(j|t)sx?$/, exclude: excludeJS, diff --git a/packages/docusaurus/src/webpack/utils.ts b/packages/docusaurus/src/webpack/utils.ts index 8caf06b96bd7..e8f6e3f95ad9 100644 --- a/packages/docusaurus/src/webpack/utils.ts +++ b/packages/docusaurus/src/webpack/utils.ts @@ -8,10 +8,9 @@ import MiniCssExtractPlugin from 'mini-css-extract-plugin'; import env from 'std-env'; import merge from 'webpack-merge'; -import {Configuration, Loader} from 'webpack'; +import {Configuration, Loader, RuleSetRule} from 'webpack'; import {TransformOptions} from '@babel/core'; -import {ConfigureWebpackUtils} from '@docusaurus/types'; - +import {ConfigureWebpackFn} from '@docusaurus/types'; import {version as cacheLoaderVersion} from 'cache-loader/package.json'; // Utility method to get style loaders @@ -120,20 +119,10 @@ export function getBabelLoader( * @returns final/ modified webpack config */ export function applyConfigureWebpack( - configureWebpack: - | Configuration - | (( - config: Configuration, - isServer: boolean, - utils: ConfigureWebpackUtils, - ) => Configuration), + configureWebpack: ConfigureWebpackFn, config: Configuration, isServer: boolean, ): Configuration { - if (typeof configureWebpack === 'object') { - return merge(config, configureWebpack); - } - // Export some utility functions const utils = { getStyleLoaders, @@ -141,10 +130,71 @@ export function applyConfigureWebpack( getBabelLoader, }; if (typeof configureWebpack === 'function') { - const res = configureWebpack(config, isServer, utils); + const {mergeStrategy, ...res} = configureWebpack(config, isServer, utils); if (res && typeof res === 'object') { - return merge(config, res); + return merge.strategy(mergeStrategy ?? {})(config, res); } } return config; } + +// Inspired by https://github.com/gatsbyjs/gatsby/blob/8e6e021014da310b9cc7d02e58c9b3efe938c665/packages/gatsby/src/utils/webpack-utils.ts#L447 +export function getFileLoaderUtils() { + const assetsRelativeRoot = 'assets/'; + + const loaders = { + file: (options = {}) => { + return { + loader: require.resolve(`file-loader`), + options: { + name: `${assetsRelativeRoot}[name]-[hash].[ext]`, + ...options, + }, + }; + }, + url: (options = {}) => { + return { + loader: require.resolve(`url-loader`), + options: { + limit: 10000, + name: `${assetsRelativeRoot}[name]-[hash].[ext]`, + fallback: require.resolve(`file-loader`), + ...options, + }, + }; + }, + }; + + const rules = { + /** + * Loads image assets, inlines images via a data URI if they are below + * the size threshold + */ + images: (): RuleSetRule => { + return { + use: [loaders.url()], + test: /\.(ico|svg|jpg|jpeg|png|gif|webp)(\?.*)?$/, + }; + }, + + /** + * Loads audio and video and inlines them via a data URI if they are below + * the size threshold + */ + media: (): RuleSetRule => { + return { + use: [loaders.url()], + test: /\.(mp4|webm|ogv|wav|mp3|m4a|aac|oga|flac)$/, + }; + }, + + otherAssets: (): RuleSetRule => { + return { + use: [loaders.file()], + test: /\.(pdf|doc|docx|xls|xlsx|zip|rar)$/, + }; + }, + }; + + return {loaders, rules}; +} diff --git a/website/docs/assets/docusaurus-asset-example-banner.png b/website/docs/assets/docusaurus-asset-example-banner.png new file mode 100644 index 000000000000..ed29058b3e5c Binary files /dev/null and b/website/docs/assets/docusaurus-asset-example-banner.png differ diff --git a/website/docs/assets/docusaurus-asset-example-pdf.pdf b/website/docs/assets/docusaurus-asset-example-pdf.pdf new file mode 100644 index 000000000000..188262276aa4 Binary files /dev/null and b/website/docs/assets/docusaurus-asset-example-pdf.pdf differ diff --git a/website/docs/assets/docusaurus-asset-example.xyz b/website/docs/assets/docusaurus-asset-example.xyz new file mode 100644 index 000000000000..188262276aa4 Binary files /dev/null and b/website/docs/assets/docusaurus-asset-example.xyz differ diff --git a/website/docs/lifecycle-apis.md b/website/docs/lifecycle-apis.md index 14f42c598ae8..fe7025d26629 100644 --- a/website/docs/lifecycle-apis.md +++ b/website/docs/lifecycle-apis.md @@ -273,6 +273,28 @@ module.exports = function (context, options) { }; ``` +### Merge strategy + +We merge the Webpack configuration parts of plugins into the global Webpack config using [webpack-merge](https://github.com/survivejs/webpack-merge). + +It is possible to specify the merge strategy. For example, if you want a webpack rule to be prepended instead of appended: + +```js {4-11} title="docusaurus-plugin/src/index.js" +module.exports = function (context, options) { + return { + name: 'custom-docusaurus-plugin', + configureWebpack(config, isServer, utils) { + return { + mergeStrategy: {'module.rules': 'prepend'}, + module: {rules: [myRuleToPrepend]}, + }; + }, + }; +}; +``` + +Read the [webpack-merge strategy doc](https://github.com/survivejs/webpack-merge#merging-with-strategies) for more details. + ## `postBuild(props)` Called when a (production) build finishes. diff --git a/website/docs/markdown-features.mdx b/website/docs/markdown-features.mdx index d4e8cef1d7d8..14fa4dde8d10 100644 --- a/website/docs/markdown-features.mdx +++ b/website/docs/markdown-features.mdx @@ -578,16 +578,16 @@ It will produce `prism-include-languages.js` in your `src/theme` folder. You can ```js {8} title="src/theme/prism-include-languages.js" const prismIncludeLanguages = (Prism) => { - // ... + // ... - additionalLanguages.forEach((lang) => { - require(`prismjs/components/prism-${lang}`); // eslint-disable-line - }); + additionalLanguages.forEach((lang) => { + require(`prismjs/components/prism-${lang}`); // eslint-disable-line + }); - require('/path/to/your/prism-language-definition'); + require('/path/to/your/prism-language-definition'); - // ... -} + // ... +}; ``` You can refer to [Prism's official language definitions](https://github.com/PrismJS/prism/tree/master/components) when you are writing your own language definitions. @@ -932,3 +932,96 @@ class HelloWorld { You may want to implement your own `` abstraction if you find the above approach too verbose. We might just implement one in future for convenience. If you have multiple of these multi-language code tabs, and you want to sync the selection across the tab instances, refer to the [Syncing tab choices section](#syncing-tab-choices). + +## Assets + +Sometimes you want to link to static assets directly from markdown files, and it is convenient to co-locate the asset next to the markdown file using it. + +We have setup Webpack loaders to handle most common file types, so that when you import a file, you get its url, and the asset is automatically copied to the output folder. + +Let's imagine the following file structure: + +``` +# Your doc +/website/docs/myFeature.mdx + +# Some assets you want to use +/website/docs/assets/docusaurus-asset-example-banner.png +/website/docs/assets/docusaurus-asset-example-pdf.pdf +/website/docs/assets/docusaurus-asset-example.xyz +``` + +### Image assets: + +You can use images by requiring them and using an image tag through MDX: + +```mdx +# My markdown page + + +``` + +The ES imports syntax also works: + +```mdx +# My markdown page + +import myImageUrl from './assets/docusaurus-asset-example-banner.png'; + + +``` + +This results in displaying the image: + + + +:::note + +If you are using [@docusaurus/plugin-ideal-image](./using-plugins.md#docusaurusplugin-ideal-image), you need to use the dedicated image component, as documented. + +::: + +### Common assets + +In the same way, you can link to existing assets by requiring them and using the returned url in videos, links etc... + +```mdx +# My markdown page + + + Download this PDF !!! + +``` + + + Download this PDF !!! + + +### Unknown assets + +This require behavior is not supported for all file extensions, but as an escape hatch you can use the special Webpack syntax to force the `file-loader` to kick-in: + +```mdx +# My markdown page + + + Download this unknown file !!! + +``` + + + Download this unknown file !!! + diff --git a/yarn.lock b/yarn.lock index 41d7179c0e3b..eb4a2f7aa965 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7982,6 +7982,14 @@ file-entry-cache@^5.0.1: dependencies: flat-cache "^2.0.1" +file-loader@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.0.0.tgz#97bbfaab7a2460c07bcbd72d3a6922407f67649f" + integrity sha512-/aMOAYEFXDdjG0wytpTL5YQLfZnnTmLNjn+AIrJ/6HVnTfDqLsVKUUwkDf4I4kgex36BvjuXEn/TX9B/1ESyqQ== + dependencies: + loader-utils "^2.0.0" + schema-utils "^2.6.5" + file-type@5.2.0, file-type@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/file-type/-/file-type-5.2.0.tgz#2ddbea7c73ffe36368dfae49dc338c058c2b8ad6" @@ -12045,7 +12053,7 @@ mime-db@1.43.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58" integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ== -"mime-db@>= 1.43.0 < 2": +mime-db@1.44.0, "mime-db@>= 1.43.0 < 2": version "1.44.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== @@ -12069,6 +12077,13 @@ mime-types@^2.1.12, mime-types@~2.1.19: dependencies: mime-db "1.43.0" +mime-types@^2.1.26: + version "2.1.27" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" + integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== + dependencies: + mime-db "1.44.0" + mime-types@~2.1.17, mime-types@~2.1.24: version "2.1.25" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.25.tgz#39772d46621f93e2a80a856c53b86a62156a6437" @@ -18283,6 +18298,15 @@ urix@^0.1.0: resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= +url-loader@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-4.1.0.tgz#c7d6b0d6b0fccd51ab3ffc58a78d32b8d89a7be2" + integrity sha512-IzgAAIC8wRrg6NYkFIJY09vtktQcsvU8V6HhtQj9PTefbYImzLB1hufqo4m+RyM5N3mLx5BqJKccgxJS+W3kqw== + dependencies: + loader-utils "^2.0.0" + mime-types "^2.1.26" + schema-utils "^2.6.5" + url-parse-lax@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-1.0.0.tgz#7af8f303645e9bd79a272e7a14ac68bc0609da73"