Skip to content

Commit

Permalink
WIP attempt to use webpack 5 asset modules
Browse files Browse the repository at this point in the history
  • Loading branch information
slorber committed Apr 28, 2021
1 parent 46b1b82 commit 2f21d30
Show file tree
Hide file tree
Showing 7 changed files with 157 additions and 83 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `<img ${node.alt ? `alt={"${escapeHtml(node.alt)}"} ` : ''}${
node.url
? `src={require("${inlineMarkdownImageFileLoader}${pathUrl}").default}`
: ''
node.url ? `src={require("${pathUrl}?${assetQuery}").default}` : ''
}${node.title ? ` title="${escapeHtml(node.title)}"` : ''} />`;

if (jsxNode.url) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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)}"` : '';

Expand Down
6 changes: 5 additions & 1 deletion packages/docusaurus-plugin-ideal-image/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},

This comment has been minimized.

Copy link
@slorber

slorber Apr 28, 2021

Author Collaborator

not sure what to do here @alexander-akait, any hint is welcome 😅

This comment has been minimized.

Copy link
@alexander-akait

alexander-akait Apr 28, 2021

type: 'asset/resource' is like file-loader, type: 'asset/inline' is like url-loader with fallback

This comment has been minimized.

Copy link
@slorber

slorber Apr 28, 2021

Author Collaborator

yes I know, but I'm not replacing a file/url loader here but a custom image loader, so not sure if I should use asset modules here

This comment has been minimized.

Copy link
@alexander-akait

alexander-akait Apr 28, 2021

Do you use import ... from ... for this loader?

This comment has been minimized.

Copy link
@slorber

slorber Apr 28, 2021

Author Collaborator

Users should be able to use both, but in our own website, I think we only use require()

Conflict: Multiple assets emit different content to the same filename assets/ideal-img/datagit.100.png

The error seems reported for this require:

require('./showcase/datagit.png')

This comment has been minimized.

Copy link
@alexander-akait

alexander-akait Apr 28, 2021

hm, problem here https://github.com/zouhir/lqip-loader/blob/master/index.js#L31, by default lqip-loader emit file using file-loader and assets modules emit file too, emit: false should solve it, but I think better to rewrite this loader (also no es modules output supported).

Do you use these properties https://github.com/zouhir/lqip-loader/blob/master/index.js#L68? If no you can use https://webpack.js.org/guides/asset-modules/#custom-data-uri-generator

Anyway I think this (or if you implement own loader) loader should be in pitch phase, so assets modules generate file and your loader modify exports output. As you can see these lines https://github.com/zouhir/lqip-loader/blob/master/index.js#L19 is workaround...

This comment has been minimized.

Copy link
@slorber

slorber Apr 29, 2021

Author Collaborator

Thanks @alexander-akait

Unfortunately, I'm not able to understand everything yet 😅 will have to investigate this.

We have a fork of this lqip plugin but it was build before I work on Docusaurus so I don't know too much what it does yet: https://github.com/facebook/docusaurus/blob/slorber/webpack5-asset-modules/packages/lqip-loader/src/index.js

This comment has been minimized.

Copy link
@alexander-akait

alexander-akait Apr 29, 2021

Maybe it helps to understand me, loader should be https://webpack.js.org/api/loaders/#pitching-loader, so you don't need to use hacks like /^(?:export default|module.exports =)/, i.e. you write https://github.com/facebook/docusaurus/blob/slorber/webpack5-asset-modules/packages/lqip-loader/src/index.js#L67 in this.data.value, look at example

This comment has been minimized.

Copy link
@slorber

slorber Apr 30, 2021

Author Collaborator

Thanks, will look at how to refactor this lqip loader.
As I found a solution to my problem, I'll let it as is for now.

BTW, it seems the original loader used pitch before but decided to move away:
https://github.com/zouhir/lqip-loader/pull/8/files

This comment has been minimized.

Copy link
@alexander-akait

alexander-akait Apr 30, 2021

It was bad idea 😄 I think they try to solve fallback loader, but it was done in wrong way...

This comment has been minimized.

Copy link
@slorber

slorber Apr 30, 2021

Author Collaborator

Thanks :)

By the way, I was wondering, is there a way to tell webpack to only use the first encountered rule here, when using ?asset querystring?

{
  module: {
    rules: [
      {
        test: /\.png/,
        type: "asset",
        resourceQuery: /asset/
      },
      {
        test: /\.png/,
        use: {
          loader: "my-custom-image-loader"
        }
      }
    ];
  }
}

It seems required currently to use resourceQuery: { not: /asset/ },, which not may be the most convenient, particularly for the modular plugin architecture of Docusaurus, as users will have to have this "not asset" resourceQuery to their custom plugins too. Not sure oneOf is a good solution in this case, as we would have to allow plugins to register asset rules to this oneOf array


Also, small feedback, but it seems now the dot is included in [ext] so maybe you should warn if the user tries to use .[ext] pattern?

Was also wondering if it's a good idea or not to include the [query], as the doc shows: static/[hash][ext][query]

This comment has been minimized.

Copy link
@alexander-akait

alexander-akait Apr 30, 2021

It seems required currently to use resourceQuery: { not: /asset/ },, which not may be the most convenient, particularly for the modular plugin architecture of Docusaurus, as users will have to have this "not asset" resourceQuery to their custom plugins too. Not sure oneOf is a good solution in this case, as we would have to allow plugins to register asset rules to this oneOf array

Maybe this webpack/webpack#12900 (comment)?

Also, small feedback, but it seems now the dot is included in [ext] so maybe you should warn if the user tries to use .[ext] pattern?

Make sense.

Was also wondering if it's a good idea or not to include the [query], as the doc shows: static/[hash][ext][query]

It is long story - webpack-contrib/file-loader#364.

Shortly - keep query, don't remove it.
Developer can have special servers for images, example <img src="./image.png?width=100&height=100"> (should change size of image on 100x100 and can be handled external server) or <img src="./image.png?quality=low">. It may not matter for docusaurus, but in general better to have the most suitable default options.

This comment has been minimized.

Copy link
@slorber

slorber Apr 30, 2021

Author Collaborator

Thanks!

I'll open another PR for the asset module migrations because I'm a bit stuck on this assetQuery / oneOf thing for now. Will need to figure out the best solution so that our plugin ecosystem does not break

This comment has been minimized.

Copy link
@alexander-akait

alexander-akait Apr 30, 2021

Feel free to feedback and ping me

use: [
require.resolve('@docusaurus/lqip-loader'),
{
Expand Down
6 changes: 6 additions & 0 deletions packages/docusaurus/src/commands/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
applyConfigurePostCss,
applyConfigureWebpack,
compile,
getFileLoaderUtils,
} from '../webpack/utils';
import CleanWebpackPlugin from '../webpack/plugins/CleanWebpackPlugin';
import {loadI18n} from '../server/i18n';
Expand Down Expand Up @@ -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)) {
Expand Down
5 changes: 5 additions & 0 deletions packages/docusaurus/src/commands/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
applyConfigureWebpack,
applyConfigurePostCss,
getHttpsConfig,
getFileLoaderUtils,
} from '../webpack/utils';
import {getCLIOptionHost, getCLIOptionPort} from './commandUtils';
import {getTranslationsLocaleDirPath} from '../server/translations/translations';
Expand Down Expand Up @@ -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 = {
...{
Expand Down
3 changes: 2 additions & 1 deletion packages/docusaurus/src/webpack/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
206 changes: 135 additions & 71 deletions packages/docusaurus/src/webpack/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,9 +284,11 @@ export function compile(config: Configuration[]): Promise<void> {
});
}

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;
Expand All @@ -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`),
},
};
Expand All @@ -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
Expand Down

0 comments on commit 2f21d30

Please sign in to comment.