feat(v2): Webpack 5, PostCSS 8 (#4089)

* Initial webpack 5 work

* It works on my machine (lol)

* Committing a bit more work

* It works - sorta

* Update packages/docusaurus/package.json

* at least fix prettier /shrug

* making more progress. build should work now, css stuff is still a bit broken

* Terser things

Signed-off-by: Reece Dunham <me@rdil.rocks>

* Working on things

* Vendor webpack

* Repair chunks, and tests

* Rerun prettier

* Re-add client prefetching

* Update snapshots

* Update snapshots

* I hope this works

* Remove redundant dev server code

* relock

* Trying to reduce memory usage and fix things

* Dead code elim

* Search bar works!!!

* Prefetching should work again

* lock

* ts issue

* Repair snapshot

* Run prettier

* Fix the CI for now

* fix lint-prettier

* clean-css works, now for the other one

* Fix lockfile

* Fixes prettier

* Other css minification works!!!

* Add clean-css options, fix webpack versions

Signed-off-by: Reece Dunham <me@rdil.rocks>

* Fix tests and several of the webpack loaders

Signed-off-by: Reece Dunham <me@rdil.rocks>

* Re-add support for simple css minifier

* Update other related dependencies

* Fix lockfile

* Dev server fixups

Signed-off-by: Reece Dunham <me@rdil.rocks>

* Simplify css things

* Update webpack, try with postcss 7

* Other cssnano repairs

* fix lockfile

* Clean up the babel preset

* Fix lockfile

* Bump RL SSR version

* Fix the build errors

* Lockfile fix

* It works again

* webpack 5 should close compiler after run

* add proper webpack5 persistent caching config

* upgrade webpack deps again

* reduce build perf timeouts to avoid build time regressions

* test if incremental build can run on netlify

* netlify test

* netlify test

* netlify test

* netlify test

* netlify test

* netlify test

* netlify test

* netlify test

* netlify test

* netlify test

* netlify test

* netlify test

* netlify test

* netlify test

* netlify test

* test

* test

* test

* test

* netlify test

* netlify test

* netlify test

* netlify test

* netlify test

* netlify test

* netlify test

* netlify test

* netlify test

* netlify test

* fix existsSync() calls

* replace @ts-nocheck by a temporary Webpack type

* replace @ts-nocheck by a temporary Webpack type

* replace @ts-nocheck by a temporary Webpack type

* migrate existing stats.warningsFilter to config.ignoreWarnings

* remove stats from postBuild lifecycle data doc, as it is likely unused (we'll add it back if someone ask for it)

* improve build.ts TS issues + move some sync code to async

* cleanup TS of start.ts

* fix TS error

* fix TS issues

* fix TS issues

* fix ts error

* netlify test

* netlify test

* netlify test

* netlify test

* netlify test

* script cleanup

* script cleanup

* re-enable @typescript-eslint/ban-ts-comment

* Deprecate getCacheLoader / getBabelLoader but keep retrocompatibility

* useless TS

* fix and comment gca(chunkName) prefetching function

* remove deprecated mainTemplate.requireFn

* temporarily use react-loadable-ssr-addon-v5-slorber until PR merged: https://github.com/facebook/docusaurus/pull/4089

* comment unsafeCache option

* add explicit and more precise webpack targets

* splitChunks, use new type: "css/mini-extract" as it seems recommended for webpack 5

* webpack error handling:
- log error.details as documented
- keep using react-dev-utils/formatWebpackMessages for now

* fix webpack5 warnings for evalSourceMapMiddleware.js

* typo

* rename webpackHotDevClient

* make all modifications of react-dev-utils explicit with a comment

* revert LogPlugin adapter

* loader-utils update

* add useful share cache comment

* add useful comments regarding the null-loader used in SSR for css files

* upgrade webpack-merge in a retrocompatible way

* use MiniCssExtractPlugin.emit false as recommended

* use @docusaurus/responsive-loader

* revert MiniCssExtractPlugin esModule: false change

* add link to PR for custom CleanWebpackPlugin

* pwa: add fallback to env variable or webpack 5 fails to build

* upgrade to CssMinimizerPlugin 2.0

* only build en locale for windows tests

* line breaks between errors

* add useful comment

* Fix e2e tests with Yarn2 not finding new init template dependencies

* fix bad import

* disable browserslist target as webpack already tries to use browserlists if a config is found, and it is a problem for existing sites

* webpack5 TS fixes

* fix getMinimizer order (even if it does not work yet)

* update postcss to v8, fix cssnano minimizer errors

* add NavbarItem position to types (useful for QuestDB site upgrade to Webpack5)

* add webpack cache env variable to reduce risk of webpack 5 adoption

Co-authored-by: slorber <lorber.sebastien@gmail.com>
This commit is contained in:
Reece Dunham 2021-04-30 11:06:53 -05:00 committed by GitHub
parent 27b9f34635
commit 05e7250c08
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
63 changed files with 1790 additions and 1556 deletions

View file

@ -25,11 +25,11 @@ jobs:
# Ensure build with a cold cache does not increase too much
- name: Build (cold cache)
run: yarn workspace docusaurus-2-website build --locale en
timeout-minutes: 10
timeout-minutes: 8
# Ensure build with a warm cache does not increase too much
- name: Build (warm cache)
run: yarn workspace docusaurus-2-website build --locale en
timeout-minutes: 10
timeout-minutes: 2
# TODO post a Github comment with build with perf warnings?

View file

@ -27,6 +27,6 @@ jobs:
- name: Docusaurus Jest Tests
run: yarn test
- name: Docusaurus Build
run: yarn build:v2
run: yarn build:v2 --locale en
env:
CI: true

View file

@ -71,10 +71,11 @@
"@formatjs/intl-datetimeformat": "^3.2.12",
"@formatjs/intl-numberformat": "^6.2.2",
"@formatjs/intl-pluralrules": "^4.0.11",
"@types/cssnano": "^4.0.0",
"@types/express": "^4.17.2",
"@types/fs-extra": "^9.0.6",
"@types/jest": "^26.0.20",
"@types/loader-utils": "^1.1.3",
"@types/loader-utils": "^2.0.2",
"@types/lodash": "^4.14.168",
"@types/node": "^14.14.22",
"@types/prismjs": "^1.16.2",
@ -87,9 +88,7 @@
"@types/semver": "^7.1.0",
"@types/shelljs": "^0.8.6",
"@types/wait-on": "^5.2.0",
"@types/webpack": "^4.41.0",
"@types/webpack-dev-server": "^3.9.0",
"@types/webpack-merge": "^4.1.5",
"@types/webpack-dev-server": "^3.11.1",
"@typescript-eslint/eslint-plugin": "^4.18.0",
"@typescript-eslint/parser": "^4.18.0",
"concurrently": "^5.3.0",

View file

@ -9,14 +9,17 @@ const advancedBasePreset = require('cssnano-preset-advanced');
const postCssSortMediaQueries = require('postcss-sort-media-queries');
const postCssRemoveOverriddenCustomProperties = require('./src/remove-overridden-custom-properties');
const preset = advancedBasePreset({
autoprefixer: {add: false},
discardComments: {removeAll: true},
});
module.exports = function docusaurusCssnanoPreset(opts) {
const advancedPreset = advancedBasePreset({
autoprefixer: {add: false},
discardComments: {removeAll: true},
...opts,
});
preset.plugins.unshift(
[postCssSortMediaQueries],
[postCssRemoveOverriddenCustomProperties],
);
advancedPreset.plugins.unshift(
[postCssSortMediaQueries],
[postCssRemoveOverriddenCustomProperties],
);
module.exports = preset;
return advancedPreset;
};

View file

@ -13,9 +13,9 @@
"directory": "packages/docusaurus-cssnano-preset"
},
"dependencies": {
"cssnano-preset-advanced": "^4.0.7",
"postcss": "^7.0.2",
"postcss-sort-media-queries": "^1.7.26"
"cssnano-preset-advanced": "^5.0.0",
"postcss": "^8.2.10",
"postcss-sort-media-queries": "^3.8.9"
},
"devDependencies": {
"to-vfile": "^6.0.0"

View file

@ -5,40 +5,38 @@
* LICENSE file in the root directory of this source tree.
*/
const postcss = require('postcss');
/**
* This PostCSS plugin will remove duplicate/same custom properties (which are actually overridden ones) **only** from `:root` selector.
*
* Depending on the presence of an `!important` rule in value of custom property, the following actions will happens:
*
* - If the same custom properties do **not** have an `!important` rule, then all of them will be removed except for the last one (which will actually be applied).
* - If the same custom properties have at least one `!important` rule, then only those properties that do not have this rule will be removed.
* @returns {import('postcss').Plugin}
*/
module.exports = function creator() {
return {
postcssPlugin: 'postcss-remove-overridden-custom-properties',
Declaration(decl) {
if (decl.parent.selector !== ':root') {
return;
}
/*
This PostCSS plugin will remove duplicate/same custom properties (which are actually overridden ones) **only** from `:root` selector.
const sameProperties =
decl.parent.nodes.filter((n) => n.prop === decl.prop) || [];
const hasImportantProperties = sameProperties.some((p) =>
Object.prototype.hasOwnProperty.call(p, 'important'),
);
Depending on the presence of an `!important` rule in value of custom property, the following actions will happens:
const overriddenProperties = hasImportantProperties
? sameProperties.filter(
(p) => !Object.prototype.hasOwnProperty.call(p, 'important'),
)
: sameProperties.slice(0, -1);
- If the same custom properties do **not** have an `!important` rule, then all of them will be removed except for the last one (which will actually be applied).
- If the same custom properties have at least one `!important` rule, then only those properties that do not have this rule will be removed.
*/
overriddenProperties.map((p) => p.remove());
},
};
};
module.exports = postcss.plugin(
'postcss-remove-overridden-custom-properties',
() => {
return (root) => {
root.walkDecls((decl) => {
if (decl.parent.selector !== ':root') {
return;
}
const sameProperties =
decl.parent.nodes.filter((n) => n.prop === decl.prop) || [];
const hasImportantProperties = sameProperties.some((p) =>
Object.prototype.hasOwnProperty.call(p, 'important'),
);
const overriddenProperties = hasImportantProperties
? sameProperties.filter(
(p) => !Object.prototype.hasOwnProperty.call(p, 'important'),
)
: sameProperties.slice(0, -1);
overriddenProperties.map((p) => p.remove());
});
};
},
);
module.exports.postcss = true;

View file

@ -16,10 +16,13 @@
"dependencies": {
"@docusaurus/core": "2.0.0-alpha.74",
"@docusaurus/preset-bootstrap": "2.0.0-alpha.74",
"@mdx-js/react": "^1.5.8",
"classnames": "^2.2.6",
"@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"
"react-dom": "^17.0.1",
"url-loader": "^4.1.1"
},
"browserslist": {
"production": [

View file

@ -1,10 +1,9 @@
import React from 'react';
import classnames from 'classnames';
import clsx from 'clsx';
import Layout from '@theme/Layout';
import Link from '@docusaurus/Link';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import useBaseUrl from '@docusaurus/useBaseUrl';
import styles from './styles.module.css';
const features = [
@ -43,7 +42,7 @@ const features = [
function Feature({imageUrl, title, description}) {
const imgUrl = useBaseUrl(imageUrl);
return (
<div className={classnames('col col--4', styles.feature)}>
<div className={clsx('col col--4', styles.feature)}>
{imgUrl && (
<div className="text--center">
<img className={styles.featureImage} src={imgUrl} alt={title} />

View file

@ -17,9 +17,12 @@
"@docusaurus/core": "2.0.0-alpha.74",
"@docusaurus/preset-classic": "2.0.0-alpha.74",
"@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"
"react-dom": "^17.0.1",
"url-loader": "^4.1.1"
},
"browserslist": {
"production": [

View file

@ -21,9 +21,12 @@
"@docusaurus/core": "2.0.0-alpha.74",
"@docusaurus/preset-classic": "2.0.0-alpha.74",
"@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"
"react-dom": "^17.0.1",
"url-loader": "^4.1.1"
},
"devDependencies": {
"@babel/eslint-parser": "^7.13.10",

View file

@ -28,13 +28,13 @@
"file-loader": "^6.2.0",
"fs-extra": "^9.1.0",
"github-slugger": "^1.3.0",
"loader-utils": "^2.0.0",
"gray-matter": "^4.0.2",
"mdast-util-to-string": "^2.0.0",
"remark-emoji": "^2.1.0",
"stringify-object": "^3.3.0",
"unist-util-visit": "^2.0.2",
"url-loader": "^4.1.1",
"webpack": "^4.44.1"
"webpack": "^5.28.0"
},
"devDependencies": {
"@docusaurus/types": "2.0.0-alpha.74",

View file

@ -5,7 +5,6 @@
* LICENSE file in the root directory of this source tree.
*/
const {getOptions} = require('loader-utils');
const {readFile} = require('fs-extra');
const mdx = require('@mdx-js/mdx');
const emoji = require('remark-emoji');
@ -27,7 +26,8 @@ const DEFAULT_OPTIONS = {
module.exports = async function docusaurusMdxLoader(fileString) {
const callback = this.async();
const reqOptions = getOptions(this) || {};
const reqOptions = this.getOptions() || {};
const {frontMatter, content: contentWithTitle} = parseFrontMatter(fileString);

View file

@ -38,7 +38,7 @@ const createJSX = (node, pathUrl) => {
};
async function ensureImageFileExist(imagePath, sourceFilePath) {
const imageExists = await fs.exists(imagePath);
const imageExists = await fs.pathExists(imagePath);
if (!imageExists) {
throw new Error(
`Image ${toMessageRelativeFilePath(

View file

@ -20,7 +20,7 @@ const {
} = getFileLoaderUtils();
async function ensureAssetFileExist(fileSystemAssetPath, sourceFilePath) {
const assetExists = await fs.exists(fileSystemAssetPath);
const assetExists = await fs.pathExists(fileSystemAssetPath);
if (!assetExists) {
throw new Error(
`Asset ${toMessageRelativeFilePath(
@ -85,12 +85,12 @@ async function convertToAssetLinkIfNeeded({node, staticDir, filePath}) {
toAssetLinkNode(fileSystemAssetPath);
} else if (path.isAbsolute(assetPath)) {
const fileSystemAssetPath = path.join(staticDir, assetPath);
if (await fs.exists(fileSystemAssetPath)) {
if (await fs.pathExists(fileSystemAssetPath)) {
toAssetLinkNode(fileSystemAssetPath);
}
} else {
const fileSystemAssetPath = path.join(path.dirname(filePath), assetPath);
if (await fs.exists(fileSystemAssetPath)) {
if (await fs.pathExists(fileSystemAssetPath)) {
toAssetLinkNode(fileSystemAssetPath);
}
}

View file

@ -27,12 +27,12 @@
"feed": "^4.2.2",
"fs-extra": "^9.1.0",
"globby": "^11.0.2",
"loader-utils": "^1.2.3",
"loader-utils": "^2.0.0",
"lodash": "^4.17.20",
"reading-time": "^1.3.0",
"remark-admonitions": "^1.2.1",
"tslib": "^2.1.0",
"webpack": "^4.44.1"
"webpack": "^5.28.0"
},
"peerDependencies": {
"react": "^16.8.4 || ^17.0.0",

View file

@ -44,7 +44,7 @@ import {
OptionValidationContext,
ValidationResult,
} from '@docusaurus/types';
import {Configuration, Loader} from 'webpack';
import {Configuration} from 'webpack';
import {
generateBlogFeed,
generateBlogPosts,
@ -401,7 +401,7 @@ export default function pluginContentBlog(
configureWebpack(
_config: Configuration,
isServer: boolean,
{getBabelLoader, getCacheLoader}: ConfigureWebpackUtils,
{getJSLoader}: ConfigureWebpackUtils,
) {
const {
rehypePlugins,
@ -441,8 +441,7 @@ export default function pluginContentBlog(
// Trailing slash is important, see https://github.com/facebook/docusaurus/pull/3970
.map(addTrailingPathSeparator),
use: [
getCacheLoader(isServer),
getBabelLoader(isServer),
getJSLoader({isServer}),
{
loader: require.resolve('@docusaurus/mdx-loader'),
options: {
@ -466,7 +465,7 @@ export default function pluginContentBlog(
loader: path.resolve(__dirname, './markdownLoader.js'),
options: markdownLoaderOptions,
},
].filter(Boolean) as Loader[],
].filter(Boolean),
},
],
},

View file

@ -5,16 +5,21 @@
* LICENSE file in the root directory of this source tree.
*/
import {loader} from 'webpack';
import {truncate, linkify} from './blogUtils';
import {parseQuery, getOptions} from 'loader-utils';
import {parseQuery} from 'loader-utils';
import {BlogMarkdownLoaderOptions} from './types';
const markdownLoader: loader.Loader = function (source) {
// TODO temporary until Webpack5 export this type
// see https://github.com/webpack/webpack/issues/11630
interface Loader extends Function {
(this: any, source: string): string | Buffer | void | undefined;
}
const markdownLoader: Loader = function (source) {
const filePath = this.resourcePath;
const fileString = source as string;
const callback = this.async();
const markdownLoaderOptions = getOptions(this) as BlogMarkdownLoaderOptions;
const markdownLoaderOptions = this.getOptions() as BlogMarkdownLoaderOptions;
// Linkify blog posts
let finalContent = linkify({
@ -24,8 +29,8 @@ const markdownLoader: loader.Loader = function (source) {
});
// Truncate content if requested (e.g: file.md?truncated=true).
const truncated: string | undefined = this.resourceQuery
? parseQuery(this.resourceQuery).truncated
const truncated: boolean | undefined = this.resourceQuery
? !!parseQuery(this.resourceQuery).truncated
: undefined;
if (truncated) {

View file

@ -43,7 +43,7 @@
"shelljs": "^0.8.4",
"tslib": "^2.1.0",
"utility-types": "^3.10.0",
"webpack": "^4.44.1"
"webpack": "^5.28.0"
},
"peerDependencies": {
"react": "^16.8.4 || ^17.0.0",

View file

@ -33,7 +33,6 @@ import {
} from '../types';
import {toSidebarsProp} from '../props';
// @ts-expect-error: TODO typedefs missing?
import {validate} from 'webpack';
import {DefaultSidebarItemsGenerator} from '../sidebarItemsGenerator';
@ -253,7 +252,7 @@ describe('simple website', () => {
false,
);
const errors = validate(config);
expect(errors.length).toBe(0);
expect(errors).toBeUndefined();
});
test('content', async () => {

View file

@ -331,7 +331,7 @@ export default function pluginContentDocs(
},
configureWebpack(_config, isServer, utils) {
const {getBabelLoader, getCacheLoader} = utils;
const {getJSLoader} = utils;
const {
rehypePlugins,
remarkPlugins,
@ -361,8 +361,7 @@ export default function pluginContentDocs(
// Trailing slash is important, see https://github.com/facebook/docusaurus/pull/3970
.map(addTrailingPathSeparator),
use: compact([
getCacheLoader(isServer),
getBabelLoader(isServer),
getJSLoader({isServer}),
{
loader: require.resolve('@docusaurus/mdx-loader'),
options: {
@ -387,16 +386,13 @@ export default function pluginContentDocs(
};
}
// Suppress warnings about non-existing of versions file.
const stats = {
warningsFilter: [VERSIONS_JSON_FILE],
};
return {
stats,
devServer: {
stats,
},
ignoreWarnings: [
// Suppress warnings about non-existing of versions file.
(e) =>
e.message.includes("Can't resolve") &&
e.message.includes(VERSIONS_JSON_FILE),
],
resolve: {
alias: {
'~docs': pluginDataDirRoot,

View file

@ -5,15 +5,19 @@
* LICENSE file in the root directory of this source tree.
*/
import {getOptions} from 'loader-utils';
import {loader} from 'webpack';
import {linkify} from './linkify';
import {DocsMarkdownOption} from '../types';
const markdownLoader: loader.Loader = function (source) {
// TODO temporary until Webpack5 export this type
// see https://github.com/webpack/webpack/issues/11630
interface Loader extends Function {
(this: any, source: string): string | Buffer | void | undefined;
}
const markdownLoader: Loader = function (source) {
const fileString = source as string;
const callback = this.async();
const options = getOptions(this) as DocsMarkdownOption;
const options = this.getOptions() as DocsMarkdownOption;
return (
callback && callback(null, linkify(fileString, this.resourcePath, options))
);

View file

@ -24,13 +24,12 @@
"@docusaurus/utils": "2.0.0-alpha.74",
"@docusaurus/utils-validation": "2.0.0-alpha.74",
"globby": "^11.0.2",
"loader-utils": "^1.2.3",
"lodash": "^4.17.20",
"minimatch": "^3.0.4",
"remark-admonitions": "^1.2.1",
"slash": "^3.0.0",
"tslib": "^2.1.0",
"webpack": "^4.44.1"
"webpack": "^5.28.0"
},
"peerDependencies": {
"react": "^16.8.4 || ^17.0.0",

View file

@ -26,7 +26,7 @@ import {
ValidationResult,
ConfigureWebpackUtils,
} from '@docusaurus/types';
import {Configuration, Loader} from 'webpack';
import {Configuration} from 'webpack';
import admonitions from 'remark-admonitions';
import {PluginOptionSchema} from './pluginOptionSchema';
import {
@ -192,7 +192,7 @@ export default function pluginContentPages(
configureWebpack(
_config: Configuration,
isServer: boolean,
{getBabelLoader, getCacheLoader}: ConfigureWebpackUtils,
{getJSLoader}: ConfigureWebpackUtils,
) {
const {
rehypePlugins,
@ -214,8 +214,7 @@ export default function pluginContentPages(
// Trailing slash is important, see https://github.com/facebook/docusaurus/pull/3970
.map(addTrailingPathSeparator),
use: [
getCacheLoader(isServer),
getBabelLoader(isServer),
getJSLoader({isServer}),
{
loader: require.resolve('@docusaurus/mdx-loader'),
options: {
@ -248,7 +247,7 @@ export default function pluginContentPages(
// contentPath,
},
},
].filter(Boolean) as Loader[],
].filter(Boolean),
},
],
},

View file

@ -5,13 +5,16 @@
* LICENSE file in the root directory of this source tree.
*/
import {loader} from 'webpack';
// import {getOptions} from 'loader-utils';
// TODO temporary until Webpack5 export this type
// see https://github.com/webpack/webpack/issues/11630
interface Loader extends Function {
(this: any, source: string): string | Buffer | void | undefined;
}
const markdownLoader: loader.Loader = function (fileString) {
const markdownLoader: Loader = function (fileString) {
const callback = this.async();
// const options = getOptions(this);
// const options = this.getOptions();
// TODO provide additinal md processing here? like interlinking pages?
// fileString = linkify(fileString)

View file

@ -24,11 +24,11 @@
"@docusaurus/lqip-loader": "2.0.0-alpha.74",
"@docusaurus/types": "2.0.0-alpha.74",
"@endiliey/react-ideal-image": "^0.0.11",
"@endiliey/responsive-loader": "^1.3.2",
"react-waypoint": "^9.0.2",
"@docusaurus/responsive-loader": "1.4.0",
"sharp": "^0.27.1",
"tslib": "^2.1.0",
"webpack": "^4.44.1"
"webpack": "^5.28.0"
},
"peerDependencies": {
"react": "^16.8.4 || ^17.0.0",

View file

@ -35,12 +35,11 @@ export default function (
use: [
require.resolve('@docusaurus/lqip-loader'),
{
loader: require.resolve('@endiliey/responsive-loader'),
loader: require.resolve('@docusaurus/responsive-loader'),
options: {
emitFile: !isServer, // don't emit for server-side rendering
disable: !isProd,
// eslint-disable-next-line
adapter: require('@endiliey/responsive-loader/sharp'),
adapter: require('@docusaurus/responsive-loader/sharp'),
name: isProd
? 'assets/ideal-img/[name].[hash:hex:7].[width].[ext]'
: 'assets/ideal-img/[name].[width].[ext]',

View file

@ -22,9 +22,9 @@
"babel-loader": "^8.2.2",
"clsx": "^1.1.1",
"core-js": "^2.6.5",
"terser-webpack-plugin": "^4.1.0",
"webpack": "^4.44.1",
"webpack-merge": "^4.2.2",
"terser-webpack-plugin": "^5.1.1",
"webpack": "^5.28.0",
"webpack-merge": "^5.7.3",
"workbox-build": "^6.1.1",
"workbox-precaching": "^6.1.1",
"workbox-window": "^6.1.1"

View file

@ -5,7 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/
const LogPlugin = require('@docusaurus/core/lib/webpack/plugins/LogPlugin');
const LogPlugin = require('@docusaurus/core/lib/webpack/plugins/LogPlugin')
.default;
const {compile} = require('@docusaurus/core/lib/webpack/utils');
const path = require('path');
const webpack = require('webpack');
@ -127,7 +128,7 @@ function plugin(context, options) {
},
plugins: [
new webpack.EnvironmentPlugin({
PWA_SW_CUSTOM: swCustom,
PWA_SW_CUSTOM: swCustom || '', // fallback value required with Webpack 5
}),
new LogPlugin({
name: 'Service Worker',

View file

@ -41,12 +41,12 @@
"infima": "0.2.0-alpha.23",
"lodash": "^4.17.20",
"parse-numeric-range": "^1.2.0",
"postcss": "^7.0.2",
"postcss": "^8.2.10",
"prism-react-renderer": "^1.1.1",
"prismjs": "^1.23.0",
"prop-types": "^15.7.2",
"react-router-dom": "^5.2.0",
"rtlcss": "^2.6.2"
"rtlcss": "^3.1.2"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "2.0.0-alpha.74"

View file

@ -10,7 +10,7 @@ import {ThemeConfig} from '@docusaurus/theme-common';
import {getTranslationFiles, translateThemeConfig} from './translations';
import path from 'path';
import Module from 'module';
import postcss, {Root as PostCssRoot} from 'postcss';
import type {AcceptedPlugin, Result, Plugin as PostCssPlugin} from 'postcss';
import rtlcss from 'rtlcss';
import {readDefaultCodeTranslationMessages} from '@docusaurus/utils';
@ -139,14 +139,11 @@ export default function docusaurusThemeClassic(
.map((lang) => `prism-${lang}`)
.join('|');
// See https://github.com/facebook/docusaurus/pull/3382
const useDocsWarningFilter = (warning: string) =>
warning.includes("Can't resolve '@theme-init/hooks/useDocs");
return {
stats: {
warningsFilter: useDocsWarningFilter,
},
ignoreWarnings: [
// See https://github.com/facebook/docusaurus/pull/3382
(e) => e.message.includes("Can't resolve '@theme-init/hooks/useDocs"),
],
plugins: [
new ContextReplacementPlugin(
/prismjs[\\/]components$/,
@ -156,29 +153,21 @@ export default function docusaurusThemeClassic(
};
},
configurePostCss(postCssOptions) {
configurePostCss(postCssOptions: {plugins: AcceptedPlugin[]}) {
if (direction === 'rtl') {
postCssOptions.plugins.push(
postcss.plugin('RtlCssPlugin', () => {
const resolvedInfimaFile = require.resolve(
getInfimaCSSFile(direction),
);
function isInfimaCSSFile(file?: string) {
return file === resolvedInfimaFile;
const resolvedInfimaFile = require.resolve(getInfimaCSSFile(direction));
const plugin: PostCssPlugin = {
postcssPlugin: 'RtlCssPlugin',
prepare: (result: Result) => {
const file = result.root?.source?.input?.file;
// Skip Infima as we are using the its RTL version.
if (file === resolvedInfimaFile) {
return {};
}
return function (root: PostCssRoot) {
const file = root?.source?.input.file;
// Skip Infima as we are using the its RTL version.
if (isInfimaCSSFile(file)) {
return;
}
rtlcss.process(root);
};
}),
);
return rtlcss(result.root);
},
};
postCssOptions.plugins.push(plugin);
}
return postCssOptions;

View file

@ -15,6 +15,7 @@ export type NavbarItem = {
type?: string | undefined;
items?: NavbarItem[];
label?: string;
position?: 'left' | 'right';
};
export type NavbarLogo = {

View file

@ -14,10 +14,10 @@
},
"license": "MIT",
"dependencies": {
"@types/webpack": "^4.41.0",
"commander": "^5.1.0",
"joi": "^17.4.0",
"querystring": "0.2.0",
"webpack-merge": "^4.2.2"
"webpack": "^5.28.0",
"webpack-merge": "^5.7.3"
}
}

View file

@ -7,12 +7,16 @@
// ESLint doesn't understand types dependencies in d.ts
// eslint-disable-next-line import/no-extraneous-dependencies
import type {Loader, Configuration, Stats} from 'webpack';
import type {RuleSetRule, Configuration} from 'webpack';
import type {Command} from 'commander';
import type {ParsedUrlQueryInput} from 'querystring';
import type {MergeStrategy} from 'webpack-merge';
import type Joi from 'joi';
// Convert webpack-merge webpack-merge enum to union type
// For type retro-compatible webpack-merge upgrade: we used string literals before)
// see https://github.com/survivejs/webpack-merge/issues/179
type MergeStrategy = 'match' | 'merge' | 'append' | 'prepend' | 'replace';
export type ReportingSeverity = 'ignore' | 'log' | 'warn' | 'error' | 'throw';
export type ThemeConfig = {
@ -186,18 +190,12 @@ export interface InjectedHtmlTags {
export type HtmlTags = string | HtmlTagObject | (string | HtmlTagObject)[];
export interface Props extends LoadContext, InjectedHtmlTags {
siteMetadata: DocusaurusSiteMetadata;
routes: RouteConfig[];
routesPaths: string[];
plugins: Plugin<unknown>[];
}
/**
* Same as `Props` but also has webpack stats appended.
*/
export interface PropsPostBuild extends Props {
stats: Stats.ToJsonOutput;
}
export interface PluginContentLoadedActions {
addRoute(config: RouteConfig): void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -228,7 +226,7 @@ export interface Plugin<Content> {
actions: PluginContentLoadedActions;
}): void;
routesLoaded?(routes: RouteConfig[]): void; // TODO remove soon, deprecated (alpha-60)
postBuild?(props: PropsPostBuild): void;
postBuild?(props: Props): void;
postStart?(props: Props): void;
configureWebpack?(
config: Configuration,
@ -335,15 +333,23 @@ export interface ConfigureWebpackUtils {
cssOptions: {
[key: string]: unknown;
},
) => Loader[];
) => RuleSetRule[];
getJSLoader: (options: {
isServer: boolean;
babelOptions?: Record<string, unknown>;
}) => RuleSetRule;
// TODO deprecated: remove before end of 2021?
getCacheLoader: (
isServer: boolean,
cacheOptions?: Record<string, unknown>,
) => Loader | null;
) => RuleSetRule | null;
// TODO deprecated: remove before end of 2021?
getBabelLoader: (
isServer: boolean,
babelOptions?: Record<string, unknown>,
) => Loader;
options?: Record<string, unknown>,
) => RuleSetRule;
}
interface HtmlTagObject {

View file

@ -39,8 +39,6 @@
"dependencies": {
"@babel/core": "^7.12.16",
"@babel/generator": "^7.12.15",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.13",
"@babel/plugin-proposal-optional-chaining": "^7.12.16",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-runtime": "^7.12.15",
"@babel/preset-env": "^7.12.16",
@ -60,14 +58,15 @@
"babel-loader": "^8.2.2",
"babel-plugin-dynamic-import-node": "2.3.0",
"boxen": "^5.0.0",
"cache-loader": "^4.1.0",
"chalk": "^4.1.0",
"chokidar": "^3.5.1",
"clean-css": "^5.1.1",
"commander": "^5.1.0",
"copy-webpack-plugin": "^6.4.1",
"copy-webpack-plugin": "^8.1.0",
"core-js": "^3.9.1",
"css-loader": "^5.1.1",
"css-minimizer-webpack-plugin": "^2.0.0",
"cssnano": "^5.0.1",
"del": "^6.0.0",
"detect-port": "^1.3.0",
"eta": "^1.12.1",
@ -78,24 +77,22 @@
"globby": "^11.0.2",
"html-minifier-terser": "^5.1.1",
"html-tags": "^3.1.0",
"html-webpack-plugin": "^4.5.0",
"html-webpack-plugin": "^5.2.0",
"import-fresh": "^3.3.0",
"is-root": "^2.1.0",
"leven": "^3.1.0",
"lodash": "^4.17.20",
"mini-css-extract-plugin": "^0.8.0",
"mini-css-extract-plugin": "^1.4.0",
"module-alias": "^2.2.2",
"nprogress": "^0.2.0",
"null-loader": "^4.0.0",
"optimize-css-assets-webpack-plugin": "^5.0.4",
"pnp-webpack-plugin": "^1.6.4",
"postcss": "^8.2.7",
"postcss-loader": "^4.1.0",
"postcss": "^8.2.10",
"postcss-loader": "^5.2.0",
"prompts": "^2.4.0",
"react-dev-utils": "^11.0.1",
"react-error-overlay": "^6.0.9",
"react-helmet": "^6.1.0",
"react-loadable": "^5.5.0",
"react-loadable-ssr-addon": "^0.3.0",
"react-loadable-ssr-addon-v5-slorber": "^1.0.1",
"react-router": "^5.2.0",
"react-router-config": "^5.1.1",
"react-router-dom": "^5.2.0",
@ -105,15 +102,16 @@
"serve-handler": "^6.1.3",
"shelljs": "^0.8.4",
"std-env": "^2.2.1",
"terser-webpack-plugin": "^4.1.0",
"strip-ansi": "^6.0.0",
"terser-webpack-plugin": "^5.1.1",
"tslib": "^2.1.0",
"update-notifier": "^5.1.0",
"url-loader": "^4.1.1",
"wait-on": "^5.2.1",
"webpack": "^4.44.1",
"webpack": "^5.28.0",
"webpack-bundle-analyzer": "^4.4.0",
"webpack-dev-server": "^3.11.2",
"webpack-merge": "^4.2.2",
"webpack-merge": "^5.7.3",
"webpackbar": "^5.0.0-3"
},
"peerDependencies": {

View file

@ -64,12 +64,6 @@ function getTransformOptions(isServer: boolean): TransformOptions {
isServer
? require.resolve('babel-plugin-dynamic-import-node')
: require.resolve('@babel/plugin-syntax-dynamic-import'),
// Optional chaining and nullish coalescing are supported in @babel/preset-env,
// but not yet supported in webpack due to support missing from acorn.
// These can be removed once we bumped to webpack 5.
// See https://github.com/facebook/docusaurus/issues/2908
require.resolve('@babel/plugin-proposal-optional-chaining'),
require.resolve('@babel/plugin-proposal-nullish-coalescing-operator'),
],
};
}

View file

@ -10,7 +10,7 @@ import React from 'react';
import {StaticRouter} from 'react-router-dom';
import ReactDOMServer from 'react-dom/server';
import {Helmet} from 'react-helmet';
import {getBundles} from 'react-loadable-ssr-addon';
import {getBundles} from 'react-loadable-ssr-addon-v5-slorber';
import Loadable from 'react-loadable';
import {minify} from 'html-minifier-terser';

View file

@ -9,8 +9,8 @@ import chalk from 'chalk';
import CopyWebpackPlugin from 'copy-webpack-plugin';
import fs from 'fs-extra';
import path from 'path';
import ReactLoadableSSRAddon from 'react-loadable-ssr-addon';
import {Configuration, Plugin} from 'webpack';
import ReactLoadableSSRAddon from 'react-loadable-ssr-addon-v5-slorber';
import {Configuration} from 'webpack';
import {BundleAnalyzerPlugin} from 'webpack-bundle-analyzer';
import merge from 'webpack-merge';
import {STATIC_DIR_NAME} from '../constants';
@ -21,9 +21,9 @@ import {BuildCLIOptions, Props} from '@docusaurus/types';
import createClientConfig from '../webpack/client';
import createServerConfig from '../webpack/server';
import {
compile,
applyConfigureWebpack,
applyConfigurePostCss,
applyConfigureWebpack,
compile,
} from '../webpack/utils';
import CleanWebpackPlugin from '../webpack/plugins/CleanWebpackPlugin';
import {loadI18n} from '../server/i18n';
@ -44,15 +44,14 @@ export default async function build(
isLastLocale: boolean;
}) {
try {
const result = await buildLocale({
// console.log(chalk.green(`Site successfully built in locale=${locale}`));
return await buildLocale({
siteDir,
locale,
cliOptions,
forceTerminate,
isLastLocale,
});
// console.log(chalk.green(`Site successfully built in locale=${locale}`));
return result;
} catch (e) {
console.error(`error building locale=${locale}`);
throw e;
@ -146,7 +145,7 @@ async function buildLocale({
new ReactLoadableSSRAddon({
filename: clientManifestPath,
}),
].filter(Boolean) as Plugin[],
].filter(Boolean),
},
);
@ -160,7 +159,7 @@ async function buildLocale({
});
const staticDir = path.resolve(siteDir, STATIC_DIR_NAME);
if (fs.existsSync(staticDir)) {
if (await fs.pathExists(staticDir)) {
serverConfig = merge(serverConfig, {
plugins: [
new CopyWebpackPlugin({
@ -200,12 +199,12 @@ async function buildLocale({
// Make sure generated client-manifest is cleaned first so we don't reuse
// the one from previous builds.
if (fs.existsSync(clientManifestPath)) {
fs.unlinkSync(clientManifestPath);
if (await fs.pathExists(clientManifestPath)) {
await fs.unlink(clientManifestPath);
}
// Run webpack to build JS bundle (client) and static html files (server).
const finalCompileResult = await compile([clientConfig, serverConfig]);
await compile([clientConfig, serverConfig]);
// Remove server.bundle.js because it is not needed.
if (
@ -214,11 +213,9 @@ async function buildLocale({
typeof serverConfig.output.filename === 'string'
) {
const serverBundle = path.join(outDir, serverConfig.output.filename);
fs.pathExists(serverBundle).then((exist) => {
if (exist) {
fs.unlink(serverBundle);
}
});
if (await fs.pathExists(serverBundle)) {
await fs.unlink(serverBundle);
}
}
// Plugin Lifecycle - postBuild.
@ -227,7 +224,7 @@ async function buildLocale({
if (!plugin.postBuild) {
return;
}
await plugin.postBuild({...props, stats: finalCompileResult});
await plugin.postBuild(props);
}),
);

View file

@ -15,7 +15,8 @@ import {debounce} from 'lodash';
import openBrowser from 'react-dev-utils/openBrowser';
import {prepareUrls} from 'react-dev-utils/WebpackDevServerUtils';
import errorOverlayMiddleware from 'react-dev-utils/errorOverlayMiddleware';
import evalSourceMapMiddleware from 'react-dev-utils/evalSourceMapMiddleware';
// import evalSourceMapMiddleware from 'react-dev-utils/evalSourceMapMiddleware';
import evalSourceMapMiddleware from '../webpack/react-dev-utils-webpack5/evalSourceMapMiddleware';
import webpack from 'webpack';
import WebpackDevServer from 'webpack-dev-server';
import merge from 'webpack-merge';
@ -191,16 +192,12 @@ export default async function start(
baseUrl,
express.static(path.resolve(siteDir, STATIC_DIR_NAME)),
);
// This lets us fetch source contents from webpack for the error overlay.
app.use(evalSourceMapMiddleware(server));
// This lets us open files from the runtime error overlay.
app.use(errorOverlayMiddleware());
// TODO: add plugins beforeDevServer and afterDevServer hook
},
},
...config.devServer,
};
const compiler = webpack(config);
if (process.env.E2E_TEST) {
@ -213,6 +210,7 @@ export default async function start(
process.exit(0);
});
}
const devServer = new WebpackDevServer(compiler, devServerConfig);
devServer.listen(port, host, (err) => {
if (err) {

View file

@ -9,6 +9,10 @@ export const NODE_MAJOR_VERSION = parseInt(
process.versions.node.split('.')[0],
10,
);
export const NODE_MINOR_VERSION = parseInt(
process.versions.node.split('.')[1],
10,
);
// Can be overridden with cli option --out-dir
export const DEFAULT_BUILD_DIR_NAME = 'build';

View file

@ -325,6 +325,7 @@ ${Object.keys(registry)
const props: Props = {
siteConfig,
siteConfigPath,
siteMetadata,
siteDir,
outDir,
baseUrl,

View file

@ -84,6 +84,12 @@ describe('base webpack config', () => {
baseUrl: '',
generatedFilesDir: '',
routesPaths: '',
i18n: {
currentLocale: 'en',
},
siteMetadata: {
docusaurusVersion: '2.0.0-alpha.70',
},
};
afterEach(() => {

View file

@ -16,7 +16,7 @@ describe('webpack dev config', () => {
const props = await loadSetup('simple');
const config = createClientConfig(props);
const errors = validate(config);
expect(errors.length).toBe(0);
expect(errors).toBeUndefined();
});
test('custom', async () => {
@ -24,6 +24,6 @@ describe('webpack dev config', () => {
const props = await loadSetup('custom');
const config = createClientConfig(props);
const errors = validate(config);
expect(errors.length).toBe(0);
expect(errors).toBeUndefined();
});
});

View file

@ -16,7 +16,7 @@ describe('webpack production config', () => {
const props = await loadSetup('simple');
const config = createServerConfig({props});
const errors = validate(config);
expect(errors.length).toBe(0);
expect(errors).toBeUndefined();
});
test('custom', async () => {
@ -24,6 +24,6 @@ describe('webpack production config', () => {
const props = await loadSetup('custom');
const config = createServerConfig({props});
const errors = validate(config);
expect(errors.length).toBe(0);
expect(errors).toBeUndefined();
});
});

View file

@ -5,11 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {
// @ts-expect-error: seems it's not in the typedefs???
validate,
Configuration,
} from 'webpack';
import {validate, Configuration} from 'webpack';
import path from 'path';
import {
@ -55,7 +51,7 @@ describe('extending generated webpack config', () => {
},
});
const errors = validate(config);
expect(errors.length).toBe(0);
expect(errors).toBeUndefined();
});
test('webpack-merge with user webpack config object', async () => {
@ -83,7 +79,7 @@ describe('extending generated webpack config', () => {
},
});
const errors = validate(config);
expect(errors.length).toBe(0);
expect(errors).toBeUndefined();
});
test('webpack-merge with custom strategy', async () => {

View file

@ -7,13 +7,11 @@
import fs from 'fs-extra';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import PnpWebpackPlugin from 'pnp-webpack-plugin';
import path from 'path';
import {Configuration, Loader} from 'webpack';
import {Configuration} from 'webpack';
import {Props} from '@docusaurus/types';
import {
getBabelLoader,
getCacheLoader,
getJSLoader,
getStyleLoaders,
getFileLoaderUtils,
getCustomBabelConfigFilePath,
@ -63,8 +61,14 @@ export function createBaseConfig(
isServer: boolean,
minify: boolean = true,
): Configuration {
const {outDir, siteDir, baseUrl, generatedFilesDir, routesPaths} = props;
const {
outDir,
siteDir,
baseUrl,
generatedFilesDir,
routesPaths,
siteMetadata,
} = props;
const totalPages = routesPaths.length;
const isProd = process.env.NODE_ENV === 'production';
const minimizeEnabled = minify && isProd && !isServer;
@ -72,11 +76,32 @@ export function createBaseConfig(
const fileLoaderUtils = getFileLoaderUtils();
const name = isServer ? 'server' : 'client';
const mode = isProd ? 'production' : 'development';
return {
mode: isProd ? 'production' : 'development',
mode,
name,
cache: {
// TODO temporary env variable to reduce risk of Webpack 5 release
// maybe expose an official api, once this is solved? https://github.com/webpack/webpack/issues/13034
type:
(process.env.DOCUSAURUS_WEBPACK_CACHE_TYPE as 'filesystem') ||
'filesystem',
// Can we share the same cache across locales?
// Exploring that question at https://github.com/webpack/webpack/issues/13034
// name: `${name}-${mode}`,
name: `${name}-${mode}-${props.i18n.currentLocale}`,
version: siteMetadata.docusaurusVersion,
buildDependencies: {
// When one of dependencies change, cache is invalidated
config: [
__filename,
path.join(__dirname, isServer ? 'server.js' : 'client.js'),
],
},
},
output: {
// Use future version of asset emitting logic, which allows freeing memory of assets after emitting.
futureEmitAssets: true,
pathinfo: false,
path: outDir,
filename: isProd ? 'assets/js/[name].[contenthash:8].js' : '[name].js',
@ -89,8 +114,9 @@ export function createBaseConfig(
performance: {
hints: false,
},
devtool: isProd ? false : 'cheap-module-eval-source-map',
devtool: isProd ? undefined : 'eval-cheap-module-source-map',
resolve: {
unsafeCache: false, // not enabled, does not seem to improve perf much
extensions: ['.wasm', '.mjs', '.js', '.jsx', '.ts', '.tsx', '.json'],
symlinks: true,
roots: [
@ -121,10 +147,8 @@ export function createBaseConfig(
'node_modules',
path.resolve(fs.realpathSync(process.cwd()), 'node_modules'),
],
plugins: [PnpWebpackPlugin],
},
resolveLoader: {
plugins: [PnpWebpackPlugin.moduleLoader(module)],
modules: ['node_modules', path.join(siteDir, 'node_modules')],
},
optimization: {
@ -137,7 +161,7 @@ export function createBaseConfig(
splitChunks: isServer
? false
: {
// Since the chunk name includes all origin chunk names its recommended for production builds with long term caching to NOT include [name] in the filenames
// Since the chunk name includes all origin chunk names it's recommended for production builds with long term caching to NOT include [name] in the filenames
name: false,
cacheGroups: {
// disable the built-in cacheGroups
@ -153,7 +177,7 @@ export function createBaseConfig(
// See https://github.com/facebook/docusaurus/issues/2006
styles: {
name: 'styles',
test: /\.css$/,
type: 'css/mini-extract',
chunks: `all`,
enforce: true,
priority: 50,
@ -172,9 +196,11 @@ export function createBaseConfig(
test: /\.(j|t)sx?$/,
exclude: excludeJS,
use: [
getCacheLoader(isServer),
getBabelLoader(isServer, getCustomBabelConfigFilePath(siteDir)),
].filter(Boolean) as Loader[],
getJSLoader({
isServer,
babelOptions: getCustomBabelConfigFilePath(siteDir),
}),
],
},
{
test: CSS_REGEX,
@ -191,7 +217,7 @@ export function createBaseConfig(
use: getStyleLoaders(isServer, {
modules: {
localIdentName: isProd
? `[local]_[hash:base64:4]`
? `[local]_[contenthash:base64:4]`
: `[local]_[path]`,
exportOnlyLocals: isServer,
},

View file

@ -24,10 +24,14 @@ export default function createClientConfig(
const config = createBaseConfig(props, false, minify);
const clientConfig = merge(config, {
// target: 'browserslist', // useless, disabled on purpose (errors on existing sites with no browserslist cfg)
entry: [
// Instead of the default WebpackDevServer client, we use a custom one
// like CRA to bring better experience.
!isProd && require.resolve('react-dev-utils/webpackHotDevClient'),
// note: the one in ./dev is modified to work with Docusaurus
// !isProd && require.resolve('react-dev-utils/hotDevServer.js'),
!isProd &&
require.resolve('./react-dev-utils-webpack5/webpackHotDevClient.js'),
path.resolve(__dirname, '../client/clientEntry.js'),
].filter(Boolean) as string[],
optimization: {
@ -56,10 +60,6 @@ export default function createClientConfig(
),
);
stats.toJson('errors-only').errors.forEach((e) => {
console.error(e);
});
process.exit(1);
}
});

View file

@ -15,8 +15,10 @@ class ChunkAssetPlugin {
/* We modify webpack runtime to add an extra function called "__webpack_require__.gca"
that will allow us to get the corresponding chunk asset for a webpack chunk.
Pass it the chunkName or chunkId you want to load.
For example: if you have a chunk named "my-chunk-name" that will map to "/0a84b5e7.c8e35c7a.js" as its corresponding output path
__webpack_require__.gca("my-chunk-name") will return "/0a84b5e7.c8e35c7a.js" */
For example: if you have a chunk named "my-chunk-name" that will map to "/publicPath/0a84b5e7.c8e35c7a.js" as its corresponding output path
__webpack_require__.gca("my-chunk-name") will return "/publicPath/0a84b5e7.c8e35c7a.js"
"gca" stands for "get chunk asset"
*/
mainTemplate.hooks.requireExtensions.tap(pluginName, (source, chunk) => {
const chunkIdToName = chunk.getChunkMaps(false).name;
const chunkNameToId = Object.create(null);
@ -25,16 +27,18 @@ class ChunkAssetPlugin {
chunkNameToId[chunkName] = chunkId;
});
const buf = [source];
buf.push('');
buf.push('// function to get chunk assets');
buf.push('// function to get chunk asset');
buf.push(
// If chunkName is passed, we convert it to chunk id
// Note that jsonpScriptSrc is an internal webpack function
`${
mainTemplate.requireFn
}.gca = function(chunkId) { chunkId = ${JSON.stringify(
// If chunkName is passed, we convert it to chunk asset url
// .p => public path url ("/" or "/baseUrl/")
// .u(chunkId) => chunk asset url ("assets/js/x63b64xd.contentHash.js")
// not sure where this is documented, but this link was helpful: https://programmer.help/blogs/5d68849083e1a.html
//
// Note: __webpack_require__.gca() is called in docusaurus.ts for prefetching
// Note: we previously used jsonpScriptSrc (Webpack 4)
`__webpack_require__.gca = function(chunkId) { chunkId = ${JSON.stringify(
chunkNameToId,
)}[chunkId]||chunkId; return jsonpScriptSrc(chunkId); };`,
)}[chunkId]||chunkId; return __webpack_require__.p + __webpack_require__.u(chunkId); };`,
);
return Template.asString(buf);
});

View file

@ -27,22 +27,13 @@
// Forked from https://github.com/johnagan/clean-webpack-plugin
// Modified to optimize performance for Docusaurus specific use case
// More context: https://github.com/facebook/docusaurus/pull/1839
import {Compiler, Stats} from 'webpack';
import path from 'path';
import {sync as delSync} from 'del';
export interface Options {
/** @deprecated */
allowExternal?: unknown;
/**
* Simulate the removal of files
*
* default: false
*/
dry?: boolean;
/**
* Write Logs to Console
* (Always enabled when dry is true)
@ -74,71 +65,19 @@ export interface Options {
* default: ['**\/*']
*/
cleanOnceBeforeBuildPatterns?: string[];
/**
* Removes files after every build (including watch mode) that match this pattern.
* Used for files that are not created directly by Webpack.
*
* Use !negative patterns to exclude files
*
* default: disabled
*/
cleanAfterEveryBuildPatterns?: string[];
/**
* Allow clean patterns outside of process.cwd()
*
* requires dry option to be explicitly set
*
* default: false
*/
dangerouslyAllowCleanPatternsOutsideProject?: boolean;
}
class CleanWebpackPlugin {
private readonly dry: boolean;
private readonly verbose: boolean;
private readonly cleanStaleWebpackAssets: boolean;
private readonly protectWebpackAssets: boolean;
private readonly cleanAfterEveryBuildPatterns: string[];
private readonly cleanOnceBeforeBuildPatterns: string[];
private readonly dangerouslyAllowCleanPatternsOutsideProject: boolean;
private currentAssets: string[];
private initialClean: boolean;
private outputPath: string;
constructor(options: Options = {}) {
if (typeof options !== 'object' || Array.isArray(options) === true) {
throw new Error(`clean-webpack-plugin only accepts an options object. See:
https://github.com/johnagan/clean-webpack-plugin#options-and-defaults-optional`);
}
if (options.allowExternal) {
throw new Error(
'clean-webpack-plugin: `allowExternal` option no longer supported. Use `dangerouslyAllowCleanPatternsOutsideProject`',
);
}
if (
options.dangerouslyAllowCleanPatternsOutsideProject === true &&
options.dry !== true &&
options.dry !== false
) {
// eslint-disable-next-line no-console
console.warn(
'clean-webpack-plugin: dangerouslyAllowCleanPatternsOutsideProject requires dry: false to be explicitly set. Enabling dry mode',
);
}
this.dangerouslyAllowCleanPatternsOutsideProject =
options.dangerouslyAllowCleanPatternsOutsideProject === true || false;
this.dry =
options.dry === true || options.dry === false
? options.dry
: this.dangerouslyAllowCleanPatternsOutsideProject === true || false;
this.verbose = this.dry === true || options.verbose === true || false;
this.verbose = options.verbose === true || false;
this.cleanStaleWebpackAssets =
options.cleanStaleWebpackAssets === true ||
@ -152,12 +91,6 @@ class CleanWebpackPlugin {
? options.protectWebpackAssets
: true;
this.cleanAfterEveryBuildPatterns = Array.isArray(
options.cleanAfterEveryBuildPatterns,
)
? options.cleanAfterEveryBuildPatterns
: [];
this.cleanOnceBeforeBuildPatterns = Array.isArray(
options.cleanOnceBeforeBuildPatterns,
)
@ -194,34 +127,17 @@ class CleanWebpackPlugin {
this.outputPath = compiler.options.output.path;
/**
* webpack 4+ comes with a new plugin system.
*
* Check for hooks in-order to support old plugin system
*/
const {hooks} = compiler;
if (this.cleanOnceBeforeBuildPatterns.length !== 0) {
if (hooks) {
hooks.compile.tap('clean-webpack-plugin', () => {
this.handleInitial();
});
} else {
compiler.plugin('compile', () => {
this.handleInitial();
});
}
hooks.compile.tap('clean-webpack-plugin', () => {
this.handleInitial();
});
}
if (hooks) {
hooks.done.tap('clean-webpack-plugin', (stats) => {
this.handleDone(stats);
});
} else {
compiler.plugin('done', (stats) => {
this.handleDone(stats);
});
}
hooks.done.tap('clean-webpack-plugin', (stats) => {
this.handleDone(stats);
});
}
/**
@ -258,13 +174,10 @@ class CleanWebpackPlugin {
* Fetch Webpack's output asset files
*/
const statsAssets =
stats.toJson(
{
all: false,
assets: true,
},
true,
).assets || [];
stats.toJson({
all: false,
assets: true,
}).assets || [];
const assets = statsAssets.map((asset: {name: string}) => {
return asset.name;
});
@ -275,9 +188,7 @@ class CleanWebpackPlugin {
* (relies on del's cwd: outputPath option)
*/
const staleFiles = this.currentAssets.filter((previousAsset) => {
const assetCurrent = assets.includes(previousAsset) === false;
return assetCurrent;
return assets.includes(previousAsset) === false;
});
/**
@ -294,13 +205,6 @@ class CleanWebpackPlugin {
removePatterns.push(...staleFiles);
}
/**
* Remove cleanAfterEveryBuildPatterns
*/
if (this.cleanAfterEveryBuildPatterns.length !== 0) {
removePatterns.push(...this.cleanAfterEveryBuildPatterns);
}
if (removePatterns.length !== 0) {
this.removeFiles(removePatterns);
}
@ -309,10 +213,10 @@ class CleanWebpackPlugin {
removeFiles(patterns: string[]): void {
try {
const deleted = delSync(patterns, {
force: this.dangerouslyAllowCleanPatternsOutsideProject,
force: false,
// Change context to build directory
cwd: this.outputPath,
dryRun: this.dry,
dryRun: false,
dot: true,
ignore: this.protectWebpackAssets ? this.currentAssets : [],
});
@ -324,15 +228,13 @@ class CleanWebpackPlugin {
deleted.forEach((file) => {
const filename = path.relative(process.cwd(), file);
const message = this.dry ? 'dry' : 'removed';
/**
* Use console.warn over .log
* https://github.com/webpack/webpack/issues/1904
* https://github.com/johnagan/clean-webpack-plugin/issues/11
*/
// eslint-disable-next-line no-console
console.warn(`clean-webpack-plugin: ${message} ${filename}`);
console.warn(`clean-webpack-plugin: removed ${filename}`);
});
}
} catch (error) {

View file

@ -1,31 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const WebpackBar = require('webpackbar');
const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');
function showError(arr) {
console.log(`\n\n${arr.join('')}`);
}
class LogPlugin extends WebpackBar {
apply(compiler) {
super.apply(compiler);
compiler.hooks.done.tap('WebpackNiceLog', (stats) => {
if (stats.hasErrors()) {
const messages = formatWebpackMessages(
stats.toJson('errors-only', true),
);
if (messages.errors.length) {
showError(messages.errors);
}
}
});
}
}
module.exports = LogPlugin;

View file

@ -0,0 +1,35 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import WebpackBar from 'webpackbar';
import {Compiler} from 'webpack';
// import formatWebpackMessages from 'react-dev-utils/formatWebpackMessages';
import formatWebpackMessages from '../react-dev-utils-webpack5/formatWebpackMessages';
function showError(arr) {
console.log(`\n\n${arr.join('\n')}`);
}
export default class LogPlugin extends WebpackBar {
apply(compiler: Compiler): void {
super.apply(compiler);
// TODO can't this be done in compile(configs) alongside the warnings???
compiler.hooks.done.tap('DocusaurusLogPlugin', (stats) => {
if (stats.hasErrors()) {
const errorsWarnings = stats.toJson('errors-warnings');
// TODO do we really want to keep this legacy logic?
// let's wait and see how the react-dev-utils support Webpack5
// we probably want to print the error stacktraces here
const messages = formatWebpackMessages(errorsWarnings);
if (messages.errors.length) {
showError(messages.errors);
}
}
});
}
}

View file

@ -0,0 +1,11 @@
This is a temporary copy of
CRA / react-dev-utils does not support Webpack 5 yet
https://github.com/facebook/create-react-app/issues/9994
This folder is a temporary copy of some react-dev-utils code to which we made some changes to support Webpack 5 without warnings
TODO remove this folder once Webpack 5 is supported (https://github.com/facebook/create-react-app/issues/9994)
The comment `// modified for Docusaurus` is added near the modified elements

View file

@ -0,0 +1,57 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/* eslint-disable */
/*
* THIS FILE IS MODIFIED FOR DOCUSAURUS
* the above copyright header must be preserved for license compliance.
*/
/*
Implementation based on comment: https://github.com/facebook/create-react-app/issues/9994#issuecomment-811289191
*/
function base64SourceMap(source) {
const base64 = Buffer.from(JSON.stringify(source.map()), 'utf8').toString(
'base64',
);
return `data:application/json;charset=utf-8;base64,${base64}`;
}
// modified for Docusaurus => remove webpack 5 deprecation warnings
// See https://github.com/facebook/create-react-app/issues/9994#issuecomment-811289191
function getSourceById(server, id) {
const module = Array.from(server._stats.compilation.modules).find(
(m) => server._stats.compilation.chunkGraph.getModuleId(m) == id,
);
return module.originalSource();
}
/*
* Middleware responsible for retrieving a generated source
* Receives a webpack internal url: "webpack-internal:///<module-id>"
* Returns a generated source: "<source-text><sourceMappingURL><sourceURL>"
*
* Based on EvalSourceMapDevToolModuleTemplatePlugin.js
*/
module.exports = function createEvalSourceMapMiddleware(server) {
return function handleWebpackInternalMiddleware(req, res, next) {
if (req.url.startsWith('/__get-internal-source')) {
const fileName = req.query.fileName;
const id = fileName.match(/webpack-internal:\/\/\/(.+)/)[1];
if (!id || !server._stats) {
next();
}
const source = getSourceById(server, id);
const sourceMapURL = `//# sourceMappingURL=${base64SourceMap(source)}`;
const sourceURL = `//# sourceURL=webpack-internal:///${module.id}`;
res.end(`${source.source()}\n${sourceMapURL}\n${sourceURL}`);
} else {
next();
}
};
};

View file

@ -0,0 +1,138 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/* eslint-disable */
/*
* THIS FILE IS MODIFIED FOR DOCUSAURUS
* the above copyright header must be preserved for license compliance.
*/
/*
Implementation has been copied from https://github.com/facebook/create-react-app/pull/10656
*/
const friendlySyntaxErrorLabel = 'Syntax error:';
function isLikelyASyntaxError(message) {
return message.indexOf(friendlySyntaxErrorLabel) !== -1;
}
// Cleans up webpack error messages.
function formatMessage(message) {
let lines = [];
// modified for Docusaurus => see https://github.com/facebook/create-react-app/pull/10656
if (typeof message === 'string') {
lines = message.split('\n');
} else if ('message' in message) {
lines = message['message'].split('\n');
} else if (Array.isArray(message)) {
message.forEach((message) => {
if ('message' in message) {
lines = message['message'].split('\n');
}
});
}
// Strip webpack-added headers off errors/warnings
// https://github.com/webpack/webpack/blob/master/lib/ModuleError.js
lines = lines.filter((line) => !/Module [A-z ]+\(from/.test(line));
// Transform parsing error into syntax error
// TODO: move this to our ESLint formatter?
lines = lines.map((line) => {
const parsingError = /Line (\d+):(?:(\d+):)?\s*Parsing error: (.+)$/.exec(
line,
);
if (!parsingError) {
return line;
}
const [, errorLine, errorColumn, errorMessage] = parsingError;
return `${friendlySyntaxErrorLabel} ${errorMessage} (${errorLine}:${errorColumn})`;
});
message = lines.join('\n');
// Smoosh syntax errors (commonly found in CSS)
message = message.replace(
/SyntaxError\s+\((\d+):(\d+)\)\s*(.+?)\n/g,
`${friendlySyntaxErrorLabel} $3 ($1:$2)\n`,
);
// Clean up export errors
message = message.replace(
/^.*export '(.+?)' was not found in '(.+?)'.*$/gm,
`Attempted import error: '$1' is not exported from '$2'.`,
);
message = message.replace(
/^.*export 'default' \(imported as '(.+?)'\) was not found in '(.+?)'.*$/gm,
`Attempted import error: '$2' does not contain a default export (imported as '$1').`,
);
message = message.replace(
/^.*export '(.+?)' \(imported as '(.+?)'\) was not found in '(.+?)'.*$/gm,
`Attempted import error: '$1' is not exported from '$3' (imported as '$2').`,
);
lines = message.split('\n');
// Remove leading newline
if (lines.length > 2 && lines[1].trim() === '') {
lines.splice(1, 1);
}
// Clean up file name
lines[0] = lines[0].replace(/^(.*) \d+:\d+-\d+$/, '$1');
// Cleans up verbose "module not found" messages for files and packages.
if (lines[1] && lines[1].indexOf('Module not found: ') === 0) {
lines = [
lines[0],
lines[1]
.replace('Error: ', '')
.replace('Module not found: Cannot find file:', 'Cannot find file:'),
];
}
// Add helpful message for users trying to use Sass for the first time
if (lines[1] && lines[1].match(/Cannot find module.+node-sass/)) {
lines[1] = 'To import Sass files, you first need to install node-sass.\n';
lines[1] +=
'Run `npm install node-sass` or `yarn add node-sass` inside your workspace.';
}
message = lines.join('\n');
// Internal stacks are generally useless so we strip them... with the
// exception of stacks containing `webpack:` because they're normally
// from user code generated by webpack. For more information see
// https://github.com/facebook/create-react-app/pull/1050
message = message.replace(
/^\s*at\s((?!webpack:).)*:\d+:\d+[\s)]*(\n|$)/gm,
'',
); // at ... ...:x:y
message = message.replace(/^\s*at\s<anonymous>(\n|$)/gm, ''); // at <anonymous>
lines = message.split('\n');
// Remove duplicated newlines
lines = lines.filter(
(line, index, arr) =>
index === 0 ||
line.trim() !== '' ||
line.trim() !== arr[index - 1].trim(),
);
// Reassemble the message
message = lines.join('\n');
return message.trim();
}
function formatWebpackMessages(json) {
const formattedErrors = json.errors.map(formatMessage);
const formattedWarnings = json.warnings.map(formatMessage);
const result = {errors: formattedErrors, warnings: formattedWarnings};
if (result.errors.some(isLikelyASyntaxError)) {
// If there are any syntax errors, show just them.
result.errors = result.errors.filter(isLikelyASyntaxError);
}
return result;
}
module.exports = formatWebpackMessages;

View file

@ -0,0 +1,285 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/* eslint-disable */
/*
* THIS FILE IS MODIFIED FOR DOCUSAURUS
* the above copyright header must be preserved for license compliance.
*/
// This alternative WebpackDevServer combines the functionality of:
// https://github.com/webpack/webpack-dev-server/blob/webpack-1/client/index.js
// https://github.com/webpack/webpack/blob/webpack-1/hot/dev-server.js
// It only supports their simplest configuration (hot updates on same server).
// It makes some opinionated choices on top, like adding a syntax error overlay
// that looks similar to our console output. The error overlay is inspired by:
// https://github.com/glenjamin/webpack-hot-middleware
var stripAnsi = require('strip-ansi');
var url = require('url');
var launchEditorEndpoint = require('react-dev-utils/launchEditorEndpoint'); // modified for Docusaurus
var formatWebpackMessages = require('./formatWebpackMessages');
var ErrorOverlay = require('react-error-overlay');
ErrorOverlay.setEditorHandler(function editorHandler(errorLocation) {
// Keep this sync with errorOverlayMiddleware.js
fetch(
launchEditorEndpoint +
'?fileName=' +
window.encodeURIComponent(errorLocation.fileName) +
'&lineNumber=' +
window.encodeURIComponent(errorLocation.lineNumber || 1) +
'&colNumber=' +
window.encodeURIComponent(errorLocation.colNumber || 1),
);
});
// We need to keep track of if there has been a runtime error.
// Essentially, we cannot guarantee application state was not corrupted by the
// runtime error. To prevent confusing behavior, we forcibly reload the entire
// application. This is handled below when we are notified of a compile (code
// change).
// See https://github.com/facebook/create-react-app/issues/3096
var hadRuntimeError = false;
ErrorOverlay.startReportingRuntimeErrors({
onError: function () {
hadRuntimeError = true;
},
filename: '/static/js/bundle.js',
});
if (module.hot && typeof module.hot.dispose === 'function') {
module.hot.dispose(function () {
// TODO: why do we need this?
ErrorOverlay.stopReportingRuntimeErrors();
});
}
// Connect to WebpackDevServer via a socket.
var connection = new WebSocket(
url.format({
protocol: window.location.protocol === 'https:' ? 'wss' : 'ws',
// modified for Docusaurus => avoid "ReferenceError: process is not defined"
hostname: window.location.hostname,
port: window.location.port,
// Hardcoded in WebpackDevServer
pathname: '/sockjs-node',
slashes: true,
}),
);
// Unlike WebpackDevServer client, we won't try to reconnect
// to avoid spamming the console. Disconnect usually happens
// when developer stops the server.
connection.onclose = function () {
if (typeof console !== 'undefined' && typeof console.info === 'function') {
console.info(
'The development server has disconnected.\nRefresh the page if necessary.',
);
}
};
// Remember some state related to hot module replacement.
var isFirstCompilation = true;
var mostRecentCompilationHash = null;
var hasCompileErrors = false;
function clearOutdatedErrors() {
// Clean up outdated compile errors, if any.
if (typeof console !== 'undefined' && typeof console.clear === 'function') {
if (hasCompileErrors) {
console.clear();
}
}
}
// Successful compilation.
function handleSuccess() {
clearOutdatedErrors();
var isHotUpdate = !isFirstCompilation;
isFirstCompilation = false;
hasCompileErrors = false;
// Attempt to apply hot updates or reload.
if (isHotUpdate) {
tryApplyUpdates(function onHotUpdateSuccess() {
// Only dismiss it when we're sure it's a hot update.
// Otherwise it would flicker right before the reload.
tryDismissErrorOverlay();
});
}
}
// Compilation with warnings (e.g. ESLint).
function handleWarnings(warnings) {
clearOutdatedErrors();
var isHotUpdate = !isFirstCompilation;
isFirstCompilation = false;
hasCompileErrors = false;
function printWarnings() {
// Print warnings to the console.
var formatted = formatWebpackMessages({
warnings: warnings,
errors: [],
});
if (typeof console !== 'undefined' && typeof console.warn === 'function') {
for (var i = 0; i < formatted.warnings.length; i++) {
if (i === 5) {
console.warn(
'There were more warnings in other files.\n' +
'You can find a complete log in the terminal.',
);
break;
}
console.warn(stripAnsi(formatted.warnings[i]));
}
}
}
printWarnings();
// Attempt to apply hot updates or reload.
if (isHotUpdate) {
tryApplyUpdates(function onSuccessfulHotUpdate() {
// Only dismiss it when we're sure it's a hot update.
// Otherwise it would flicker right before the reload.
tryDismissErrorOverlay();
});
}
}
// Compilation with errors (e.g. syntax error or missing modules).
function handleErrors(errors) {
clearOutdatedErrors();
isFirstCompilation = false;
hasCompileErrors = true;
// "Massage" webpack messages.
var formatted = formatWebpackMessages({
errors: errors,
warnings: [],
});
// Only show the first error.
ErrorOverlay.reportBuildError(formatted.errors[0]);
// Also log them to the console.
if (typeof console !== 'undefined' && typeof console.error === 'function') {
for (var i = 0; i < formatted.errors.length; i++) {
console.error(stripAnsi(formatted.errors[i]));
}
}
// Do not attempt to reload now.
// We will reload on next success instead.
}
function tryDismissErrorOverlay() {
if (!hasCompileErrors) {
ErrorOverlay.dismissBuildError();
}
}
// There is a newer version of the code available.
function handleAvailableHash(hash) {
// Update last known compilation hash.
mostRecentCompilationHash = hash;
}
// Handle messages from the server.
connection.onmessage = function (e) {
var message = JSON.parse(e.data);
switch (message.type) {
case 'hash':
handleAvailableHash(message.data);
break;
case 'still-ok':
case 'ok':
handleSuccess();
break;
case 'content-changed':
// Triggered when a file from `contentBase` changed.
window.location.reload();
break;
case 'warnings':
handleWarnings(message.data);
break;
case 'errors':
handleErrors(message.data);
break;
default:
// Do nothing.
}
};
// Is there a newer version of this code available?
function isUpdateAvailable() {
/* globals __webpack_hash__ */
// __webpack_hash__ is the hash of the current compilation.
// It's a global variable injected by webpack.
return mostRecentCompilationHash !== __webpack_hash__;
}
// webpack disallows updates in other states.
function canApplyUpdates() {
return module.hot.status() === 'idle';
}
// Attempt to update code on the fly, fall back to a hard reload.
function tryApplyUpdates(onHotUpdateSuccess) {
if (!module.hot) {
// HotModuleReplacementPlugin is not in webpack configuration.
window.location.reload();
return;
}
if (!isUpdateAvailable() || !canApplyUpdates()) {
return;
}
function handleApplyUpdates(err, updatedModules) {
// NOTE: This var is injected by Webpack's DefinePlugin, and is a boolean instead of string.
// const hasReactRefresh = process.env.FAST_REFRESH;
const hasReactRefresh = true; // modified for Docusaurus => avoid "ReferenceError: process is not defined"
const wantsForcedReload = err || !updatedModules || hadRuntimeError;
// React refresh can handle hot-reloading over errors.
if (!hasReactRefresh && wantsForcedReload) {
window.location.reload();
return;
}
if (typeof onHotUpdateSuccess === 'function') {
// Maybe we want to do something.
onHotUpdateSuccess();
}
if (isUpdateAvailable()) {
// While we were updating, there was a new update! Do it again.
tryApplyUpdates();
}
}
// https://webpack.github.io/docs/hot-module-replacement.html#check
var result = module.hot.check(/* autoApply */ true, handleApplyUpdates);
// // webpack 2 returns a Promise instead of invoking a callback
if (result && result.then) {
result.then(
function (updatedModules) {
handleApplyUpdates(null, updatedModules);
},
function (err) {
handleApplyUpdates(err, null);
},
);
}
}

View file

@ -14,6 +14,7 @@ import {Props} from '@docusaurus/types';
import {createBaseConfig} from './base';
import WaitPlugin from './plugins/WaitPlugin';
import LogPlugin from './plugins/LogPlugin';
import {NODE_MAJOR_VERSION, NODE_MINOR_VERSION} from '../constants';
export default function createServerConfig({
props,
@ -43,6 +44,7 @@ export default function createServerConfig({
return ssgPath;
});
const serverConfig = merge(config, {
target: `node${NODE_MAJOR_VERSION}.${NODE_MINOR_VERSION}`,
entry: {
main: path.resolve(__dirname, '../client/serverEntry.js'),
},
@ -52,7 +54,6 @@ export default function createServerConfig({
// Workaround for Webpack 4 Bug (https://github.com/webpack/webpack/issues/6522)
globalObject: 'this',
},
target: 'node',
plugins: [
// Wait until manifest from client bundle is generated
new WaitPlugin({

View file

@ -6,20 +6,20 @@
*/
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import env from 'std-env';
import merge from 'webpack-merge';
import {
mergeWithCustomize,
customizeArray,
customizeObject,
CustomizeRule,
} from 'webpack-merge';
import webpack, {
Configuration,
Loader,
NewLoader,
Plugin,
RuleSetRule,
Stats,
WebpackPluginInstance,
} from 'webpack';
import fs from 'fs-extra';
import TerserPlugin from 'terser-webpack-plugin';
import OptimizeCSSAssetsPlugin from 'optimize-css-assets-webpack-plugin';
import CleanCss from 'clean-css';
import CssMinimizerPlugin from 'css-minimizer-webpack-plugin';
import path from 'path';
import crypto from 'crypto';
import chalk from 'chalk';
@ -28,13 +28,13 @@ import {
ConfigureWebpackFn,
ConfigurePostCssFn,
PostCssOptions,
ConfigureWebpackUtils,
} from '@docusaurus/types';
import CssNanoPreset from '@docusaurus/cssnano-preset';
import {version as cacheLoaderVersion} from 'cache-loader/package.json';
import {
BABEL_CONFIG_FILE_NAME,
OUTPUT_STATIC_ASSETS_DIR_NAME,
} from '../constants';
import {memoize} from 'lodash';
// Utility method to get style loaders
export function getStyleLoaders(
@ -42,24 +42,36 @@ export function getStyleLoaders(
cssOptions: {
[key: string]: unknown;
} = {},
): Loader[] {
): RuleSetRule[] {
if (isServer) {
return [
cssOptions.modules
? {
return cssOptions.modules
? [
{
loader: require.resolve('css-loader'),
options: cssOptions,
}
: require.resolve('null-loader'),
];
},
]
: [
{
loader: MiniCssExtractPlugin.loader,
options: {
// Don't emit CSS files for SSR (previously used null-loader)
// See https://github.com/webpack-contrib/mini-css-extract-plugin/issues/90#issuecomment-811991738
emit: false,
},
},
{
loader: require.resolve('css-loader'),
options: cssOptions,
},
];
}
const isProd = process.env.NODE_ENV === 'production';
const loaders = [
return [
{
loader: MiniCssExtractPlugin.loader,
options: {
hmr: !isProd,
esModule: true,
},
},
{
@ -84,24 +96,6 @@ export function getStyleLoaders(
},
},
];
return loaders;
}
export function getCacheLoader(
isServer: boolean,
cacheOptions?: {[key: string]: unknown},
): Loader | null {
if (env.ci || env.test) {
return null;
}
return {
loader: require.resolve('cache-loader'),
options: {
cacheIdentifier: `cache-loader:${cacheLoaderVersion}${isServer}`,
...cacheOptions,
},
};
}
export function getCustomBabelConfigFilePath(
@ -141,16 +135,50 @@ export function getBabelOptions({
}
}
export function getBabelLoader(
isServer: boolean,
babelOptions?: TransformOptions | string,
): Loader {
// Name is generic on purpose
// we want to support multiple js loader implementations (babel + esbuild)
export function getJSLoader({
isServer,
babelOptions,
}: {
isServer: boolean;
babelOptions?: TransformOptions | string;
}): RuleSetRule {
return {
loader: require.resolve('babel-loader'),
options: getBabelOptions({isServer, babelOptions}),
};
}
// TODO remove this before end of 2021?
const warnBabelLoaderOnce = memoize(function () {
console.warn(
chalk.yellow(
'Docusaurus plans to support multiple JS loader strategies (Babel, esbuild...): getBabelLoader(isServer) is now deprecated in favor of getJSLoader({isServer})',
),
);
});
const getBabelLoaderDeprecated = function getBabelLoaderDeprecated(
isServer: boolean,
babelOptions?: TransformOptions | string,
) {
warnBabelLoaderOnce();
return getJSLoader({isServer, babelOptions});
};
// TODO remove this before end of 2021 ?
const warnCacheLoaderOnce = memoize(function () {
console.warn(
chalk.yellow(
'Docusaurus uses Webpack 5 and getCacheLoader() usage is now deprecated',
),
);
});
function getCacheLoaderDeprecated() {
warnCacheLoaderOnce();
return null;
}
/**
* Helper function to modify webpack config
* @param configureWebpack a webpack config or a function to modify config
@ -164,15 +192,21 @@ export function applyConfigureWebpack(
isServer: boolean,
): Configuration {
// Export some utility functions
const utils = {
const utils: ConfigureWebpackUtils = {
getStyleLoaders,
getCacheLoader,
getBabelLoader,
getJSLoader,
getBabelLoader: getBabelLoaderDeprecated,
getCacheLoader: getCacheLoaderDeprecated,
};
if (typeof configureWebpack === 'function') {
const {mergeStrategy, ...res} = configureWebpack(config, isServer, utils);
if (res && typeof res === 'object') {
return merge.strategy(mergeStrategy ?? {})(config, res);
// @ts-expect-error: annoying error due to enums: https://github.com/survivejs/webpack-merge/issues/179
const customizeRules: Record<string, CustomizeRule> = mergeStrategy ?? {};
return mergeWithCustomize({
customizeArray: customizeArray(customizeRules),
customizeObject: customizeObject(customizeRules),
})(config, res);
}
}
return config;
@ -182,13 +216,13 @@ export function applyConfigurePostCss(
configurePostCss: NonNullable<ConfigurePostCssFn>,
config: Configuration,
): Configuration {
type LocalPostCSSLoader = Loader & {
type LocalPostCSSLoader = unknown & {
options: {postcssOptions: PostCssOptions};
};
// TODO not ideal heuristic but good enough for our usecase?
function isPostCssLoader(loader: Loader): loader is LocalPostCSSLoader {
return !!(loader as NewLoader)?.options?.postcssOptions;
function isPostCssLoader(loader: unknown): loader is LocalPostCSSLoader {
return !!(loader as any)?.options?.postcssOptions;
}
// Does not handle all edge cases, but good enough for now
@ -206,67 +240,46 @@ export function applyConfigurePostCss(
}
}
config.module?.rules.forEach(overridePostCssOptions);
config.module?.rules?.forEach(overridePostCssOptions);
return config;
}
// See https://webpack.js.org/configuration/stats/#statswarningsfilter
// @slorber: note sure why we have to re-implement this logic
// just know that legacy had this only partially implemented, so completed it
type WarningFilter = string | RegExp | ((warning: string) => boolean);
function filterWarnings(
warningsFilter: WarningFilter[],
warnings: string[],
): string[] {
function isWarningFiltered(warning: string): boolean {
return warningsFilter.some((warningFilter) => {
if (typeof warningFilter === 'string') {
return warning.includes(warningFilter);
} else if (warningFilter instanceof RegExp) {
return !!warning.match(warningFilter);
} else if (warningFilter instanceof Function) {
return warningFilter(warning);
} else {
throw new Error(`Unknown warningFilter type = ${typeof warningFilter}`);
}
});
}
return warnings.filter((warning) => !isWarningFiltered(warning));
}
export function compile(config: Configuration[]): Promise<Stats.ToJsonOutput> {
export function compile(config: Configuration[]): Promise<void> {
return new Promise((resolve, reject) => {
const compiler = webpack(config);
compiler.run((err, stats) => {
if (err) {
reject(new Error(err.toString()));
console.error(err.stack || err);
// @ts-expect-error: see https://webpack.js.org/api/node/#error-handling
if (err.details) {
// @ts-expect-error: see https://webpack.js.org/api/node/#error-handling
console.error(err.details);
}
reject(err);
}
// let plugins consume all the stats
const allStats = stats?.toJson('errors-warnings');
const errorsWarnings = stats?.toJson('errors-warnings');
if (stats?.hasErrors()) {
allStats.errors.forEach((e) => {
console.error(e);
});
reject(new Error('Failed to compile with errors.'));
}
if (stats?.hasWarnings()) {
// Custom filtering warnings (see https://github.com/webpack/webpack/issues/7841).
let warnings = [...allStats.warnings];
const warningsFilter = ((config[0].stats as Stats.ToJsonOptionsObject)
?.warningsFilter || []) as WarningFilter[];
if (Array.isArray(warningsFilter)) {
warnings = filterWarnings(warningsFilter, warnings);
}
warnings.forEach((warning) => {
if (errorsWarnings && stats?.hasWarnings()) {
errorsWarnings.warnings?.forEach((warning) => {
console.warn(warning);
});
}
resolve(allStats);
// Webpack 5 requires calling close() so that persistent caching works
// See https://github.com/webpack/webpack.js.org/pull/4775
compiler.close((errClose) => {
if (errClose) {
console.error(
chalk.red('Error while closing Webpack compiler', errClose),
);
reject(errClose);
} else {
resolve();
}
});
});
});
}
@ -275,8 +288,8 @@ type AssetFolder = 'images' | 'files' | 'fonts' | 'medias';
type FileLoaderUtils = {
loaders: {
file: (options: {folder: AssetFolder}) => Loader;
url: (options: {folder: AssetFolder}) => Loader;
file: (options: {folder: AssetFolder}) => RuleSetRule;
url: (options: {folder: AssetFolder}) => RuleSetRule;
inlineMarkdownImageFileLoader: string;
inlineMarkdownLinkFileLoader: string;
};
@ -298,7 +311,7 @@ export function getFileLoaderUtils(): FileLoaderUtils {
const fileLoaderFileName = (folder: AssetFolder) =>
`${OUTPUT_STATIC_ASSETS_DIR_NAME}/${folder}/[name]-[hash].[ext]`;
const loaders = {
const loaders: FileLoaderUtils['loaders'] = {
file: (options: {folder: AssetFolder}) => {
return {
loader: require.resolve(`file-loader`),
@ -331,19 +344,19 @@ export function getFileLoaderUtils(): FileLoaderUtils {
)}!`,
};
const rules = {
const rules: FileLoaderUtils['rules'] = {
/**
* Loads image assets, inlines images via a data URI if they are below
* the size threshold
*/
images: (): RuleSetRule => {
images: () => {
return {
use: [loaders.url({folder: 'images'})],
test: /\.(ico|jpg|jpeg|png|gif|webp)(\?.*)?$/,
};
},
fonts: (): RuleSetRule => {
fonts: () => {
return {
use: [loaders.url({folder: 'fonts'})],
test: /\.(woff|woff2|eot|ttf|otf)$/,
@ -354,14 +367,14 @@ export function getFileLoaderUtils(): FileLoaderUtils {
* Loads audio and video and inlines them via a data URI if they are below
* the size threshold
*/
media: (): RuleSetRule => {
media: () => {
return {
use: [loaders.url({folder: 'medias'})],
test: /\.(mp4|webm|ogv|wav|mp3|m4a|aac|oga|flac)$/,
};
},
svg: (): RuleSetRule => {
svg: () => {
return {
test: /\.svg?$/,
oneOf: [
@ -383,7 +396,7 @@ export function getFileLoaderUtils(): FileLoaderUtils {
// 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: {
test: /\.(ts|tsx|js|jsx|md|mdx)$/,
and: [/\.(ts|tsx|js|jsx|md|mdx)$/],
},
},
{
@ -393,7 +406,7 @@ export function getFileLoaderUtils(): FileLoaderUtils {
};
},
otherAssets: (): RuleSetRule => {
otherAssets: () => {
return {
use: [loaders.file({folder: 'files'})],
test: /\.(pdf|doc|docx|xls|xlsx|zip|rar)$/,
@ -476,12 +489,12 @@ function getTerserParallel() {
return terserParallel;
}
export function getMinimizer(useSimpleCssMinifier = false): Plugin[] {
export function getMinimizer(
useSimpleCssMinifier = false,
): WebpackPluginInstance[] {
const minimizer = [
new TerserPlugin({
cache: true,
parallel: getTerserParallel(),
sourceMap: false,
terserOptions: {
parse: {
// we want uglify-js to parse ecma 8 code. However, we don't want it
@ -508,26 +521,20 @@ export function getMinimizer(useSimpleCssMinifier = false): Plugin[] {
},
}),
];
if (useSimpleCssMinifier) {
minimizer.push(
new OptimizeCSSAssetsPlugin({
cssProcessorPluginOptions: {
preset: 'default',
},
}),
);
minimizer.push(new CssMinimizerPlugin());
} else {
minimizer.push(
...[
new OptimizeCSSAssetsPlugin({
cssProcessorPluginOptions: {
preset: CssNanoPreset,
// Using the array syntax to add 2 minimizers
// see https://github.com/webpack-contrib/css-minimizer-webpack-plugin#array
new CssMinimizerPlugin({
minimizerOptions: [
// CssNano options
{
preset: require.resolve('@docusaurus/cssnano-preset'),
},
}),
new OptimizeCSSAssetsPlugin({
cssProcessor: CleanCss,
cssProcessorOptions: {
// CleanCss options
{
inline: false,
level: {
1: {
@ -540,8 +547,12 @@ export function getMinimizer(useSimpleCssMinifier = false): Plugin[] {
},
},
},
}),
],
],
minify: [
CssMinimizerPlugin.cssnanoMinify,
CssMinimizerPlugin.cleanCssMinify,
],
}),
);
}

View file

@ -14,7 +14,6 @@
"license": "MIT",
"dependencies": {
"file-loader": "^6.2.0",
"loader-utils": "^1.2.3",
"lodash": "^4.17.20",
"node-vibrant": "^3.1.5",
"sharp": "^0.27.1"

View file

@ -5,7 +5,6 @@
* LICENSE file in the root directory of this source tree.
*/
const loaderUtils = require('loader-utils');
const lqip = require('./lqip');
module.exports = function (contentBuffer) {
@ -15,7 +14,7 @@ module.exports = function (contentBuffer) {
const callback = this.async();
const imgPath = this.resourcePath;
const config = loaderUtils.getOptions(this) || {};
const config = this.getOptions() || {};
config.base64 = 'base64' in config ? config.base64 : true;
config.palette = 'palette' in config ? config.palette : false;

View file

@ -31,6 +31,7 @@
/* Advanced Options */
"resolveJsonModule": true,
"skipLibCheck": true, // @types/webpack and webpack/types.d.ts are not the same thing
/* Use tslib */
"importHelpers": true,

View file

@ -398,7 +398,6 @@ type Props = {
postBodyTags: string;
routesPaths: string[];
plugins: Plugin<any>[];
stats: Stats.ToJsonOutput;
};
```
@ -408,13 +407,11 @@ Example:
module.exports = function (context, options) {
return {
name: 'docusaurus-plugin',
async postBuild({siteConfig = {}, routesPaths = [], outDir, stats}) {
async postBuild({siteConfig = {}, routesPaths = [], outDir}) {
// Print out to console all the rendered routes.
routesPaths.map((route) => {
console.log(route);
});
// Print out to console all the webpack stats.
console.log(stats);
},
};
};

View file

@ -1,15 +1,27 @@
# Note: this file's config override the Netlify UI admin config
# Note: this file's config overrides the Netlify UI admin config
# /!\ due to using a monorepo it can be a bit messy to configure Netlify
# See also https://github.com/netlify/build/issues/2483
# default/production build
[build]
base = "/"
command = "yarn workspace docusaurus-2-website netlify:build:production"
command = "yarn --cwd .. build:packages && yarn build"
publish = "website/build"
# we build deploy previews with a /build/ baseUrl on purpose
# permits to test that baseUrl works fine (this often breaks!)
[build.environment]
NETLIFY_USE_YARN = "true"
YARN_VERSION = "1.22.5"
NODE_VERSION = "14"
[context.production]
command = "yarn --cwd .. build:packages && yarn netlify:build:production"
[context.deploy-preview]
command = "yarn workspace docusaurus-2-website netlify:build:deployPreview"
publish = "website/build"
command = "yarn --cwd .. build:packages && yarn netlify:build:deployPreview"
[[plugins]]
package = "netlify-plugin-cache"
[plugins.inputs]
paths = [
"node_modules/.cache/webpack",
]

View file

@ -36,6 +36,7 @@
"@docusaurus/theme-live-codeblock": "2.0.0-alpha.74",
"clsx": "^1.1.1",
"color": "^3.1.3",
"netlify-plugin-cache": "^1.0.3",
"npm-to-yarn": "^1.0.0-2",
"react": "^17.0.1",
"react-dom": "^17.0.1",

1810
yarn.lock

File diff suppressed because it is too large Load diff