Compare commits

...

8 Commits

Author SHA1 Message Date
slorber 645bf67b50 case-insensitive webpack rule regexes 2021-05-13 17:38:39 +02:00
slorber ef727ac013 replace require() by new URL() 2021-05-13 17:34:03 +02:00
slorber af7acc9e04 Merge branch 'master' into slorber/asset-modules
# Conflicts:
#	packages/docusaurus-mdx-loader/src/remark/transformImage/index.js
#	packages/docusaurus-mdx-loader/src/remark/transformLinks/index.js
2021-05-13 17:08:27 +02:00
slorber 7a5886cccf webpack asset modules cleanup 2021-04-30 19:05:44 +02:00
slorber f21430ea9d typo 2021-04-30 19:05:38 +02:00
slorber fe60790a53 Fix asset modules bug: use "not asset" resourceQuery, remove .default 2021-04-30 19:05:31 +02:00
slorber 0c859fe96f fix ideal-image warning due to missing hash:
> Conflict: Multiple assets emit different content to the same filename assets/ideal-img/datagit.100.png
see https://github.com/facebook/docusaurus/pull/4089#discussion_r622013656
2021-04-30 19:05:06 +02:00
slorber 4de1610e60 WIP attempt to use webpack 5 asset modules 2021-04-30 19:04:43 +02:00
13 changed files with 170 additions and 142 deletions

View File

@ -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": [

View File

@ -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": [

View File

@ -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",

View File

@ -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 = `<img ${node.alt ? `alt={"${escapeHtml(node.alt)}"} ` : ''}${
node.url
? `src={require("${inlineMarkdownImageFileLoader}${escapePath(
pathUrl,
)}").default}`
? // see https://github.com/facebook/docusaurus/pull/4708#discussion_r624515715
`src={new URL("${escapePath(pathUrl)}?${assetQuery}", import.meta.url)}`
: ''
}${node.title ? ` title="${escapeHtml(node.title)}"` : ''} />`;

View File

@ -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)}"` : '';

View File

@ -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,
},
},

View File

@ -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)) {

View File

@ -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 = {
...{

View File

@ -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,

View File

@ -284,137 +284,157 @@ export function compile(config: Configuration[]): Promise<void> {
});
}
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]`;
function fileNameGenerator(folder: AssetFolder) {
return {
filename: `${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),
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,
},
};
},
url: (options: {folder: AssetFolder}) => {
return {
loader: require.resolve(`url-loader`),
options: {
limit: urlLoaderLimit,
name: fileLoaderFileName(options.folder),
fallback: require.resolve(`file-loader`),
},
};
},
},
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,
};
}
function fontAssetRule(): RuleSetRule {
return {
...baseAssetRule('fonts'),
test: /\.(woff|woff2|eot|ttf|otf)$/i,
};
}
function mediaAssetRule(): RuleSetRule {
return {
...baseAssetRule('medias'),
test: /\.(mp4|webm|ogv|wav|mp3|m4a|aac|oga|flac)$/i,
};
}
function fileAssetRule(): RuleSetRule {
return {
...baseAssetRule('files'),
test: /\.(pdf|doc|docx|xls|xlsx|zip|rar)$/i,
type: 'asset/resource',
};
}
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],
},
},
],
},
svgAssetRule(),
],
};
}
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)(\?.*)?$/,
};
},
fonts: () => {
return {
use: [loaders.url({folder: '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)$/,
};
},
svg: () => {
return {
test: /\.svg?$/,
oneOf: [
{
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)$/],
},
},
{
use: [loaders.url({folder: 'images'})],
},
],
};
},
otherAssets: () => {
return {
use: [loaders.file({folder: 'files'})],
test: /\.(pdf|doc|docx|xls|xlsx|zip|rar)$/,
};
},
images: imageAssetRule,
fonts: fontAssetRule,
media: mediaAssetRule,
svg: svgComponentOrAssetRule,
files: fileAssetRule,
};
return {loaders, rules};
// 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
{
type: 'asset/resource',
resourceQuery: assetResourceQuery,
generator: fileNameGenerator('files'),
},
],
},
} as Configuration);
}
return {rules, assetQuery, prependAssetQueryRules};
}
// Ensure the certificate and key provided are valid and if not

View File

@ -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
<img src={require('@site/static/img/myImage.png').default} />
<img src={require('@site/static/img/myImage.png')} />
```
:::

View File

@ -28,7 +28,7 @@ You can use images in Markdown, or by requiring them and using a JSX image tag:
# My Markdown page
<img
src={require('./assets/docusaurus-asset-example-banner.png').default}
src={require('./assets/docusaurus-asset-example-banner.png')}
alt="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
<a
target="_blank"
href={require('./assets/docusaurus-asset-example-pdf.pdf').default}>
<a target="_blank" href={require('./assets/docusaurus-asset-example-pdf.pdf')}>
Download this PDF
</a>
@ -77,7 +75,10 @@ or
<a
target="_blank"
href={require('../../assets/docusaurus-asset-example-pdf.pdf').default}>
href={new URL(
'../../assets/docusaurus-asset-example-pdf.pdf',
import.meta.url,
).toString()}>
Download this PDF
</a>

View File

@ -27,7 +27,7 @@ import DocusaurusImageUrl from '@site/static/img/docusaurus.png';
```
```jsx title="MyComponent.js"
<img src={require('@site/static/img/docusaurus.png').default} />
<img src={require('@site/static/img/docusaurus.png')} />
```
```jsx title="MyComponent.js"