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 # Ensure build with a cold cache does not increase too much
- name: Build (cold cache) - name: Build (cold cache)
run: yarn workspace docusaurus-2-website build --locale en 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 # Ensure build with a warm cache does not increase too much
- name: Build (warm cache) - name: Build (warm cache)
run: yarn workspace docusaurus-2-website build --locale en 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? # TODO post a Github comment with build with perf warnings?

View file

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

View file

@ -71,10 +71,11 @@
"@formatjs/intl-datetimeformat": "^3.2.12", "@formatjs/intl-datetimeformat": "^3.2.12",
"@formatjs/intl-numberformat": "^6.2.2", "@formatjs/intl-numberformat": "^6.2.2",
"@formatjs/intl-pluralrules": "^4.0.11", "@formatjs/intl-pluralrules": "^4.0.11",
"@types/cssnano": "^4.0.0",
"@types/express": "^4.17.2", "@types/express": "^4.17.2",
"@types/fs-extra": "^9.0.6", "@types/fs-extra": "^9.0.6",
"@types/jest": "^26.0.20", "@types/jest": "^26.0.20",
"@types/loader-utils": "^1.1.3", "@types/loader-utils": "^2.0.2",
"@types/lodash": "^4.14.168", "@types/lodash": "^4.14.168",
"@types/node": "^14.14.22", "@types/node": "^14.14.22",
"@types/prismjs": "^1.16.2", "@types/prismjs": "^1.16.2",
@ -87,9 +88,7 @@
"@types/semver": "^7.1.0", "@types/semver": "^7.1.0",
"@types/shelljs": "^0.8.6", "@types/shelljs": "^0.8.6",
"@types/wait-on": "^5.2.0", "@types/wait-on": "^5.2.0",
"@types/webpack": "^4.41.0", "@types/webpack-dev-server": "^3.11.1",
"@types/webpack-dev-server": "^3.9.0",
"@types/webpack-merge": "^4.1.5",
"@typescript-eslint/eslint-plugin": "^4.18.0", "@typescript-eslint/eslint-plugin": "^4.18.0",
"@typescript-eslint/parser": "^4.18.0", "@typescript-eslint/parser": "^4.18.0",
"concurrently": "^5.3.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 postCssSortMediaQueries = require('postcss-sort-media-queries');
const postCssRemoveOverriddenCustomProperties = require('./src/remove-overridden-custom-properties'); const postCssRemoveOverriddenCustomProperties = require('./src/remove-overridden-custom-properties');
const preset = advancedBasePreset({ module.exports = function docusaurusCssnanoPreset(opts) {
autoprefixer: {add: false}, const advancedPreset = advancedBasePreset({
discardComments: {removeAll: true}, autoprefixer: {add: false},
}); discardComments: {removeAll: true},
...opts,
});
preset.plugins.unshift( advancedPreset.plugins.unshift(
[postCssSortMediaQueries], [postCssSortMediaQueries],
[postCssRemoveOverriddenCustomProperties], [postCssRemoveOverriddenCustomProperties],
); );
module.exports = preset; return advancedPreset;
};

View file

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

View file

@ -5,40 +5,38 @@
* LICENSE file in the root directory of this source tree. * 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;
}
/* const sameProperties =
This PostCSS plugin will remove duplicate/same custom properties (which are actually overridden ones) **only** from `:root` selector. 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). overriddenProperties.map((p) => p.remove());
- If the same custom properties have at least one `!important` rule, then only those properties that do not have this rule will be removed. },
*/ };
};
module.exports = postcss.plugin( module.exports.postcss = true;
'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());
});
};
},
);

View file

@ -16,10 +16,13 @@
"dependencies": { "dependencies": {
"@docusaurus/core": "2.0.0-alpha.74", "@docusaurus/core": "2.0.0-alpha.74",
"@docusaurus/preset-bootstrap": "2.0.0-alpha.74", "@docusaurus/preset-bootstrap": "2.0.0-alpha.74",
"@mdx-js/react": "^1.5.8", "@mdx-js/react": "^1.6.21",
"classnames": "^2.2.6", "@svgr/webpack": "^5.5.0",
"clsx": "^1.1.1",
"file-loader": "^6.2.0",
"react": "^17.0.1", "react": "^17.0.1",
"react-dom": "^17.0.1" "react-dom": "^17.0.1",
"url-loader": "^4.1.1"
}, },
"browserslist": { "browserslist": {
"production": [ "production": [

View file

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

View file

@ -17,9 +17,12 @@
"@docusaurus/core": "2.0.0-alpha.74", "@docusaurus/core": "2.0.0-alpha.74",
"@docusaurus/preset-classic": "2.0.0-alpha.74", "@docusaurus/preset-classic": "2.0.0-alpha.74",
"@mdx-js/react": "^1.6.21", "@mdx-js/react": "^1.6.21",
"@svgr/webpack": "^5.5.0",
"clsx": "^1.1.1", "clsx": "^1.1.1",
"file-loader": "^6.2.0",
"react": "^17.0.1", "react": "^17.0.1",
"react-dom": "^17.0.1" "react-dom": "^17.0.1",
"url-loader": "^4.1.1"
}, },
"browserslist": { "browserslist": {
"production": [ "production": [

View file

@ -21,9 +21,12 @@
"@docusaurus/core": "2.0.0-alpha.74", "@docusaurus/core": "2.0.0-alpha.74",
"@docusaurus/preset-classic": "2.0.0-alpha.74", "@docusaurus/preset-classic": "2.0.0-alpha.74",
"@mdx-js/react": "^1.6.21", "@mdx-js/react": "^1.6.21",
"@svgr/webpack": "^5.5.0",
"clsx": "^1.1.1", "clsx": "^1.1.1",
"file-loader": "^6.2.0",
"react": "^17.0.1", "react": "^17.0.1",
"react-dom": "^17.0.1" "react-dom": "^17.0.1",
"url-loader": "^4.1.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/eslint-parser": "^7.13.10", "@babel/eslint-parser": "^7.13.10",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,16 +5,21 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import {loader} from 'webpack';
import {truncate, linkify} from './blogUtils'; import {truncate, linkify} from './blogUtils';
import {parseQuery, getOptions} from 'loader-utils'; import {parseQuery} from 'loader-utils';
import {BlogMarkdownLoaderOptions} from './types'; 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 filePath = this.resourcePath;
const fileString = source as string; const fileString = source as string;
const callback = this.async(); const callback = this.async();
const markdownLoaderOptions = getOptions(this) as BlogMarkdownLoaderOptions; const markdownLoaderOptions = this.getOptions() as BlogMarkdownLoaderOptions;
// Linkify blog posts // Linkify blog posts
let finalContent = linkify({ let finalContent = linkify({
@ -24,8 +29,8 @@ const markdownLoader: loader.Loader = function (source) {
}); });
// Truncate content if requested (e.g: file.md?truncated=true). // Truncate content if requested (e.g: file.md?truncated=true).
const truncated: string | undefined = this.resourceQuery const truncated: boolean | undefined = this.resourceQuery
? parseQuery(this.resourceQuery).truncated ? !!parseQuery(this.resourceQuery).truncated
: undefined; : undefined;
if (truncated) { if (truncated) {

View file

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

View file

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

View file

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

View file

@ -5,15 +5,19 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import {getOptions} from 'loader-utils';
import {loader} from 'webpack';
import {linkify} from './linkify'; import {linkify} from './linkify';
import {DocsMarkdownOption} from '../types'; 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 fileString = source as string;
const callback = this.async(); const callback = this.async();
const options = getOptions(this) as DocsMarkdownOption; const options = this.getOptions() as DocsMarkdownOption;
return ( return (
callback && callback(null, linkify(fileString, this.resourcePath, options)) callback && callback(null, linkify(fileString, this.resourcePath, options))
); );

View file

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

View file

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

View file

@ -5,13 +5,16 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import {loader} from 'webpack'; // TODO temporary until Webpack5 export this type
// import {getOptions} from 'loader-utils'; // 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 callback = this.async();
// const options = getOptions(this); // const options = this.getOptions();
// TODO provide additinal md processing here? like interlinking pages? // TODO provide additinal md processing here? like interlinking pages?
// fileString = linkify(fileString) // fileString = linkify(fileString)

View file

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

View file

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

View file

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

View file

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

View file

@ -41,12 +41,12 @@
"infima": "0.2.0-alpha.23", "infima": "0.2.0-alpha.23",
"lodash": "^4.17.20", "lodash": "^4.17.20",
"parse-numeric-range": "^1.2.0", "parse-numeric-range": "^1.2.0",
"postcss": "^7.0.2", "postcss": "^8.2.10",
"prism-react-renderer": "^1.1.1", "prism-react-renderer": "^1.1.1",
"prismjs": "^1.23.0", "prismjs": "^1.23.0",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"rtlcss": "^2.6.2" "rtlcss": "^3.1.2"
}, },
"devDependencies": { "devDependencies": {
"@docusaurus/module-type-aliases": "2.0.0-alpha.74" "@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 {getTranslationFiles, translateThemeConfig} from './translations';
import path from 'path'; import path from 'path';
import Module from 'module'; 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 rtlcss from 'rtlcss';
import {readDefaultCodeTranslationMessages} from '@docusaurus/utils'; import {readDefaultCodeTranslationMessages} from '@docusaurus/utils';
@ -139,14 +139,11 @@ export default function docusaurusThemeClassic(
.map((lang) => `prism-${lang}`) .map((lang) => `prism-${lang}`)
.join('|'); .join('|');
// See https://github.com/facebook/docusaurus/pull/3382
const useDocsWarningFilter = (warning: string) =>
warning.includes("Can't resolve '@theme-init/hooks/useDocs");
return { return {
stats: { ignoreWarnings: [
warningsFilter: useDocsWarningFilter, // See https://github.com/facebook/docusaurus/pull/3382
}, (e) => e.message.includes("Can't resolve '@theme-init/hooks/useDocs"),
],
plugins: [ plugins: [
new ContextReplacementPlugin( new ContextReplacementPlugin(
/prismjs[\\/]components$/, /prismjs[\\/]components$/,
@ -156,29 +153,21 @@ export default function docusaurusThemeClassic(
}; };
}, },
configurePostCss(postCssOptions) { configurePostCss(postCssOptions: {plugins: AcceptedPlugin[]}) {
if (direction === 'rtl') { if (direction === 'rtl') {
postCssOptions.plugins.push( const resolvedInfimaFile = require.resolve(getInfimaCSSFile(direction));
postcss.plugin('RtlCssPlugin', () => { const plugin: PostCssPlugin = {
const resolvedInfimaFile = require.resolve( postcssPlugin: 'RtlCssPlugin',
getInfimaCSSFile(direction), prepare: (result: Result) => {
); const file = result.root?.source?.input?.file;
function isInfimaCSSFile(file?: string) { // Skip Infima as we are using the its RTL version.
return file === resolvedInfimaFile; if (file === resolvedInfimaFile) {
return {};
} }
return rtlcss(result.root);
return function (root: PostCssRoot) { },
const file = root?.source?.input.file; };
postCssOptions.plugins.push(plugin);
// Skip Infima as we are using the its RTL version.
if (isInfimaCSSFile(file)) {
return;
}
rtlcss.process(root);
};
}),
);
} }
return postCssOptions; return postCssOptions;

View file

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

View file

@ -14,10 +14,10 @@
}, },
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/webpack": "^4.41.0",
"commander": "^5.1.0", "commander": "^5.1.0",
"joi": "^17.4.0", "joi": "^17.4.0",
"querystring": "0.2.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 doesn't understand types dependencies in d.ts
// eslint-disable-next-line import/no-extraneous-dependencies // 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 {Command} from 'commander';
import type {ParsedUrlQueryInput} from 'querystring'; import type {ParsedUrlQueryInput} from 'querystring';
import type {MergeStrategy} from 'webpack-merge';
import type Joi from 'joi'; 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 ReportingSeverity = 'ignore' | 'log' | 'warn' | 'error' | 'throw';
export type ThemeConfig = { export type ThemeConfig = {
@ -186,18 +190,12 @@ export interface InjectedHtmlTags {
export type HtmlTags = string | HtmlTagObject | (string | HtmlTagObject)[]; export type HtmlTags = string | HtmlTagObject | (string | HtmlTagObject)[];
export interface Props extends LoadContext, InjectedHtmlTags { export interface Props extends LoadContext, InjectedHtmlTags {
siteMetadata: DocusaurusSiteMetadata;
routes: RouteConfig[]; routes: RouteConfig[];
routesPaths: string[]; routesPaths: string[];
plugins: Plugin<unknown>[]; plugins: Plugin<unknown>[];
} }
/**
* Same as `Props` but also has webpack stats appended.
*/
export interface PropsPostBuild extends Props {
stats: Stats.ToJsonOutput;
}
export interface PluginContentLoadedActions { export interface PluginContentLoadedActions {
addRoute(config: RouteConfig): void; addRoute(config: RouteConfig): void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -228,7 +226,7 @@ export interface Plugin<Content> {
actions: PluginContentLoadedActions; actions: PluginContentLoadedActions;
}): void; }): void;
routesLoaded?(routes: RouteConfig[]): void; // TODO remove soon, deprecated (alpha-60) routesLoaded?(routes: RouteConfig[]): void; // TODO remove soon, deprecated (alpha-60)
postBuild?(props: PropsPostBuild): void; postBuild?(props: Props): void;
postStart?(props: Props): void; postStart?(props: Props): void;
configureWebpack?( configureWebpack?(
config: Configuration, config: Configuration,
@ -335,15 +333,23 @@ export interface ConfigureWebpackUtils {
cssOptions: { cssOptions: {
[key: string]: unknown; [key: string]: unknown;
}, },
) => Loader[]; ) => RuleSetRule[];
getJSLoader: (options: {
isServer: boolean;
babelOptions?: Record<string, unknown>;
}) => RuleSetRule;
// TODO deprecated: remove before end of 2021?
getCacheLoader: ( getCacheLoader: (
isServer: boolean, isServer: boolean,
cacheOptions?: Record<string, unknown>, cacheOptions?: Record<string, unknown>,
) => Loader | null; ) => RuleSetRule | null;
// TODO deprecated: remove before end of 2021?
getBabelLoader: ( getBabelLoader: (
isServer: boolean, isServer: boolean,
babelOptions?: Record<string, unknown>, options?: Record<string, unknown>,
) => Loader; ) => RuleSetRule;
} }
interface HtmlTagObject { interface HtmlTagObject {

View file

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

View file

@ -64,12 +64,6 @@ function getTransformOptions(isServer: boolean): TransformOptions {
isServer isServer
? require.resolve('babel-plugin-dynamic-import-node') ? require.resolve('babel-plugin-dynamic-import-node')
: require.resolve('@babel/plugin-syntax-dynamic-import'), : 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 {StaticRouter} from 'react-router-dom';
import ReactDOMServer from 'react-dom/server'; import ReactDOMServer from 'react-dom/server';
import {Helmet} from 'react-helmet'; 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 Loadable from 'react-loadable';
import {minify} from 'html-minifier-terser'; import {minify} from 'html-minifier-terser';

View file

@ -9,8 +9,8 @@ import chalk from 'chalk';
import CopyWebpackPlugin from 'copy-webpack-plugin'; import CopyWebpackPlugin from 'copy-webpack-plugin';
import fs from 'fs-extra'; import fs from 'fs-extra';
import path from 'path'; import path from 'path';
import ReactLoadableSSRAddon from 'react-loadable-ssr-addon'; import ReactLoadableSSRAddon from 'react-loadable-ssr-addon-v5-slorber';
import {Configuration, Plugin} from 'webpack'; import {Configuration} from 'webpack';
import {BundleAnalyzerPlugin} from 'webpack-bundle-analyzer'; import {BundleAnalyzerPlugin} from 'webpack-bundle-analyzer';
import merge from 'webpack-merge'; import merge from 'webpack-merge';
import {STATIC_DIR_NAME} from '../constants'; import {STATIC_DIR_NAME} from '../constants';
@ -21,9 +21,9 @@ import {BuildCLIOptions, Props} from '@docusaurus/types';
import createClientConfig from '../webpack/client'; import createClientConfig from '../webpack/client';
import createServerConfig from '../webpack/server'; import createServerConfig from '../webpack/server';
import { import {
compile,
applyConfigureWebpack,
applyConfigurePostCss, applyConfigurePostCss,
applyConfigureWebpack,
compile,
} from '../webpack/utils'; } from '../webpack/utils';
import CleanWebpackPlugin from '../webpack/plugins/CleanWebpackPlugin'; import CleanWebpackPlugin from '../webpack/plugins/CleanWebpackPlugin';
import {loadI18n} from '../server/i18n'; import {loadI18n} from '../server/i18n';
@ -44,15 +44,14 @@ export default async function build(
isLastLocale: boolean; isLastLocale: boolean;
}) { }) {
try { try {
const result = await buildLocale({ // console.log(chalk.green(`Site successfully built in locale=${locale}`));
return await buildLocale({
siteDir, siteDir,
locale, locale,
cliOptions, cliOptions,
forceTerminate, forceTerminate,
isLastLocale, isLastLocale,
}); });
// console.log(chalk.green(`Site successfully built in locale=${locale}`));
return result;
} catch (e) { } catch (e) {
console.error(`error building locale=${locale}`); console.error(`error building locale=${locale}`);
throw e; throw e;
@ -146,7 +145,7 @@ async function buildLocale({
new ReactLoadableSSRAddon({ new ReactLoadableSSRAddon({
filename: clientManifestPath, filename: clientManifestPath,
}), }),
].filter(Boolean) as Plugin[], ].filter(Boolean),
}, },
); );
@ -160,7 +159,7 @@ async function buildLocale({
}); });
const staticDir = path.resolve(siteDir, STATIC_DIR_NAME); const staticDir = path.resolve(siteDir, STATIC_DIR_NAME);
if (fs.existsSync(staticDir)) { if (await fs.pathExists(staticDir)) {
serverConfig = merge(serverConfig, { serverConfig = merge(serverConfig, {
plugins: [ plugins: [
new CopyWebpackPlugin({ new CopyWebpackPlugin({
@ -200,12 +199,12 @@ async function buildLocale({
// Make sure generated client-manifest is cleaned first so we don't reuse // Make sure generated client-manifest is cleaned first so we don't reuse
// the one from previous builds. // the one from previous builds.
if (fs.existsSync(clientManifestPath)) { if (await fs.pathExists(clientManifestPath)) {
fs.unlinkSync(clientManifestPath); await fs.unlink(clientManifestPath);
} }
// Run webpack to build JS bundle (client) and static html files (server). // 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. // Remove server.bundle.js because it is not needed.
if ( if (
@ -214,11 +213,9 @@ async function buildLocale({
typeof serverConfig.output.filename === 'string' typeof serverConfig.output.filename === 'string'
) { ) {
const serverBundle = path.join(outDir, serverConfig.output.filename); const serverBundle = path.join(outDir, serverConfig.output.filename);
fs.pathExists(serverBundle).then((exist) => { if (await fs.pathExists(serverBundle)) {
if (exist) { await fs.unlink(serverBundle);
fs.unlink(serverBundle); }
}
});
} }
// Plugin Lifecycle - postBuild. // Plugin Lifecycle - postBuild.
@ -227,7 +224,7 @@ async function buildLocale({
if (!plugin.postBuild) { if (!plugin.postBuild) {
return; 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 openBrowser from 'react-dev-utils/openBrowser';
import {prepareUrls} from 'react-dev-utils/WebpackDevServerUtils'; import {prepareUrls} from 'react-dev-utils/WebpackDevServerUtils';
import errorOverlayMiddleware from 'react-dev-utils/errorOverlayMiddleware'; 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 webpack from 'webpack';
import WebpackDevServer from 'webpack-dev-server'; import WebpackDevServer from 'webpack-dev-server';
import merge from 'webpack-merge'; import merge from 'webpack-merge';
@ -191,16 +192,12 @@ export default async function start(
baseUrl, baseUrl,
express.static(path.resolve(siteDir, STATIC_DIR_NAME)), express.static(path.resolve(siteDir, STATIC_DIR_NAME)),
); );
// This lets us fetch source contents from webpack for the error overlay. // This lets us fetch source contents from webpack for the error overlay.
app.use(evalSourceMapMiddleware(server)); app.use(evalSourceMapMiddleware(server));
// This lets us open files from the runtime error overlay. // This lets us open files from the runtime error overlay.
app.use(errorOverlayMiddleware()); app.use(errorOverlayMiddleware());
// TODO: add plugins beforeDevServer and afterDevServer hook
}, },
}, },
...config.devServer,
}; };
const compiler = webpack(config); const compiler = webpack(config);
if (process.env.E2E_TEST) { if (process.env.E2E_TEST) {
@ -213,6 +210,7 @@ export default async function start(
process.exit(0); process.exit(0);
}); });
} }
const devServer = new WebpackDevServer(compiler, devServerConfig); const devServer = new WebpackDevServer(compiler, devServerConfig);
devServer.listen(port, host, (err) => { devServer.listen(port, host, (err) => {
if (err) { if (err) {

View file

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

View file

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

View file

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

View file

@ -16,7 +16,7 @@ describe('webpack dev config', () => {
const props = await loadSetup('simple'); const props = await loadSetup('simple');
const config = createClientConfig(props); const config = createClientConfig(props);
const errors = validate(config); const errors = validate(config);
expect(errors.length).toBe(0); expect(errors).toBeUndefined();
}); });
test('custom', async () => { test('custom', async () => {
@ -24,6 +24,6 @@ describe('webpack dev config', () => {
const props = await loadSetup('custom'); const props = await loadSetup('custom');
const config = createClientConfig(props); const config = createClientConfig(props);
const errors = validate(config); 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 props = await loadSetup('simple');
const config = createServerConfig({props}); const config = createServerConfig({props});
const errors = validate(config); const errors = validate(config);
expect(errors.length).toBe(0); expect(errors).toBeUndefined();
}); });
test('custom', async () => { test('custom', async () => {
@ -24,6 +24,6 @@ describe('webpack production config', () => {
const props = await loadSetup('custom'); const props = await loadSetup('custom');
const config = createServerConfig({props}); const config = createServerConfig({props});
const errors = validate(config); 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. * LICENSE file in the root directory of this source tree.
*/ */
import { import {validate, Configuration} from 'webpack';
// @ts-expect-error: seems it's not in the typedefs???
validate,
Configuration,
} from 'webpack';
import path from 'path'; import path from 'path';
import { import {
@ -55,7 +51,7 @@ describe('extending generated webpack config', () => {
}, },
}); });
const errors = validate(config); const errors = validate(config);
expect(errors.length).toBe(0); expect(errors).toBeUndefined();
}); });
test('webpack-merge with user webpack config object', async () => { test('webpack-merge with user webpack config object', async () => {
@ -83,7 +79,7 @@ describe('extending generated webpack config', () => {
}, },
}); });
const errors = validate(config); const errors = validate(config);
expect(errors.length).toBe(0); expect(errors).toBeUndefined();
}); });
test('webpack-merge with custom strategy', async () => { test('webpack-merge with custom strategy', async () => {

View file

@ -7,13 +7,11 @@
import fs from 'fs-extra'; import fs from 'fs-extra';
import MiniCssExtractPlugin from 'mini-css-extract-plugin'; import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import PnpWebpackPlugin from 'pnp-webpack-plugin';
import path from 'path'; import path from 'path';
import {Configuration, Loader} from 'webpack'; import {Configuration} from 'webpack';
import {Props} from '@docusaurus/types'; import {Props} from '@docusaurus/types';
import { import {
getBabelLoader, getJSLoader,
getCacheLoader,
getStyleLoaders, getStyleLoaders,
getFileLoaderUtils, getFileLoaderUtils,
getCustomBabelConfigFilePath, getCustomBabelConfigFilePath,
@ -63,8 +61,14 @@ export function createBaseConfig(
isServer: boolean, isServer: boolean,
minify: boolean = true, minify: boolean = true,
): Configuration { ): Configuration {
const {outDir, siteDir, baseUrl, generatedFilesDir, routesPaths} = props; const {
outDir,
siteDir,
baseUrl,
generatedFilesDir,
routesPaths,
siteMetadata,
} = props;
const totalPages = routesPaths.length; const totalPages = routesPaths.length;
const isProd = process.env.NODE_ENV === 'production'; const isProd = process.env.NODE_ENV === 'production';
const minimizeEnabled = minify && isProd && !isServer; const minimizeEnabled = minify && isProd && !isServer;
@ -72,11 +76,32 @@ export function createBaseConfig(
const fileLoaderUtils = getFileLoaderUtils(); const fileLoaderUtils = getFileLoaderUtils();
const name = isServer ? 'server' : 'client';
const mode = isProd ? 'production' : 'development';
return { 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: { output: {
// Use future version of asset emitting logic, which allows freeing memory of assets after emitting.
futureEmitAssets: true,
pathinfo: false, pathinfo: false,
path: outDir, path: outDir,
filename: isProd ? 'assets/js/[name].[contenthash:8].js' : '[name].js', filename: isProd ? 'assets/js/[name].[contenthash:8].js' : '[name].js',
@ -89,8 +114,9 @@ export function createBaseConfig(
performance: { performance: {
hints: false, hints: false,
}, },
devtool: isProd ? false : 'cheap-module-eval-source-map', devtool: isProd ? undefined : 'eval-cheap-module-source-map',
resolve: { resolve: {
unsafeCache: false, // not enabled, does not seem to improve perf much
extensions: ['.wasm', '.mjs', '.js', '.jsx', '.ts', '.tsx', '.json'], extensions: ['.wasm', '.mjs', '.js', '.jsx', '.ts', '.tsx', '.json'],
symlinks: true, symlinks: true,
roots: [ roots: [
@ -121,10 +147,8 @@ export function createBaseConfig(
'node_modules', 'node_modules',
path.resolve(fs.realpathSync(process.cwd()), 'node_modules'), path.resolve(fs.realpathSync(process.cwd()), 'node_modules'),
], ],
plugins: [PnpWebpackPlugin],
}, },
resolveLoader: { resolveLoader: {
plugins: [PnpWebpackPlugin.moduleLoader(module)],
modules: ['node_modules', path.join(siteDir, 'node_modules')], modules: ['node_modules', path.join(siteDir, 'node_modules')],
}, },
optimization: { optimization: {
@ -137,7 +161,7 @@ export function createBaseConfig(
splitChunks: isServer splitChunks: isServer
? false ? 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, name: false,
cacheGroups: { cacheGroups: {
// disable the built-in cacheGroups // disable the built-in cacheGroups
@ -153,7 +177,7 @@ export function createBaseConfig(
// See https://github.com/facebook/docusaurus/issues/2006 // See https://github.com/facebook/docusaurus/issues/2006
styles: { styles: {
name: 'styles', name: 'styles',
test: /\.css$/, type: 'css/mini-extract',
chunks: `all`, chunks: `all`,
enforce: true, enforce: true,
priority: 50, priority: 50,
@ -172,9 +196,11 @@ export function createBaseConfig(
test: /\.(j|t)sx?$/, test: /\.(j|t)sx?$/,
exclude: excludeJS, exclude: excludeJS,
use: [ use: [
getCacheLoader(isServer), getJSLoader({
getBabelLoader(isServer, getCustomBabelConfigFilePath(siteDir)), isServer,
].filter(Boolean) as Loader[], babelOptions: getCustomBabelConfigFilePath(siteDir),
}),
],
}, },
{ {
test: CSS_REGEX, test: CSS_REGEX,
@ -191,7 +217,7 @@ export function createBaseConfig(
use: getStyleLoaders(isServer, { use: getStyleLoaders(isServer, {
modules: { modules: {
localIdentName: isProd localIdentName: isProd
? `[local]_[hash:base64:4]` ? `[local]_[contenthash:base64:4]`
: `[local]_[path]`, : `[local]_[path]`,
exportOnlyLocals: isServer, exportOnlyLocals: isServer,
}, },

View file

@ -24,10 +24,14 @@ export default function createClientConfig(
const config = createBaseConfig(props, false, minify); const config = createBaseConfig(props, false, minify);
const clientConfig = merge(config, { const clientConfig = merge(config, {
// target: 'browserslist', // useless, disabled on purpose (errors on existing sites with no browserslist cfg)
entry: [ entry: [
// Instead of the default WebpackDevServer client, we use a custom one // Instead of the default WebpackDevServer client, we use a custom one
// like CRA to bring better experience. // 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'), path.resolve(__dirname, '../client/clientEntry.js'),
].filter(Boolean) as string[], ].filter(Boolean) as string[],
optimization: { optimization: {
@ -56,10 +60,6 @@ export default function createClientConfig(
), ),
); );
stats.toJson('errors-only').errors.forEach((e) => {
console.error(e);
});
process.exit(1); process.exit(1);
} }
}); });

View file

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

View file

@ -27,22 +27,13 @@
// Forked from https://github.com/johnagan/clean-webpack-plugin // Forked from https://github.com/johnagan/clean-webpack-plugin
// Modified to optimize performance for Docusaurus specific use case // Modified to optimize performance for Docusaurus specific use case
// More context: https://github.com/facebook/docusaurus/pull/1839
import {Compiler, Stats} from 'webpack'; import {Compiler, Stats} from 'webpack';
import path from 'path'; import path from 'path';
import {sync as delSync} from 'del'; import {sync as delSync} from 'del';
export interface Options { export interface Options {
/** @deprecated */
allowExternal?: unknown;
/**
* Simulate the removal of files
*
* default: false
*/
dry?: boolean;
/** /**
* Write Logs to Console * Write Logs to Console
* (Always enabled when dry is true) * (Always enabled when dry is true)
@ -74,71 +65,19 @@ export interface Options {
* default: ['**\/*'] * default: ['**\/*']
*/ */
cleanOnceBeforeBuildPatterns?: string[]; 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 { class CleanWebpackPlugin {
private readonly dry: boolean;
private readonly verbose: boolean; private readonly verbose: boolean;
private readonly cleanStaleWebpackAssets: boolean; private readonly cleanStaleWebpackAssets: boolean;
private readonly protectWebpackAssets: boolean; private readonly protectWebpackAssets: boolean;
private readonly cleanAfterEveryBuildPatterns: string[];
private readonly cleanOnceBeforeBuildPatterns: string[]; private readonly cleanOnceBeforeBuildPatterns: string[];
private readonly dangerouslyAllowCleanPatternsOutsideProject: boolean;
private currentAssets: string[]; private currentAssets: string[];
private initialClean: boolean; private initialClean: boolean;
private outputPath: string; private outputPath: string;
constructor(options: Options = {}) { constructor(options: Options = {}) {
if (typeof options !== 'object' || Array.isArray(options) === true) { this.verbose = options.verbose === true || false;
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.cleanStaleWebpackAssets = this.cleanStaleWebpackAssets =
options.cleanStaleWebpackAssets === true || options.cleanStaleWebpackAssets === true ||
@ -152,12 +91,6 @@ class CleanWebpackPlugin {
? options.protectWebpackAssets ? options.protectWebpackAssets
: true; : true;
this.cleanAfterEveryBuildPatterns = Array.isArray(
options.cleanAfterEveryBuildPatterns,
)
? options.cleanAfterEveryBuildPatterns
: [];
this.cleanOnceBeforeBuildPatterns = Array.isArray( this.cleanOnceBeforeBuildPatterns = Array.isArray(
options.cleanOnceBeforeBuildPatterns, options.cleanOnceBeforeBuildPatterns,
) )
@ -194,34 +127,17 @@ class CleanWebpackPlugin {
this.outputPath = compiler.options.output.path; 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; const {hooks} = compiler;
if (this.cleanOnceBeforeBuildPatterns.length !== 0) { if (this.cleanOnceBeforeBuildPatterns.length !== 0) {
if (hooks) { hooks.compile.tap('clean-webpack-plugin', () => {
hooks.compile.tap('clean-webpack-plugin', () => { this.handleInitial();
this.handleInitial(); });
});
} else {
compiler.plugin('compile', () => {
this.handleInitial();
});
}
} }
if (hooks) { hooks.done.tap('clean-webpack-plugin', (stats) => {
hooks.done.tap('clean-webpack-plugin', (stats) => { this.handleDone(stats);
this.handleDone(stats); });
});
} else {
compiler.plugin('done', (stats) => {
this.handleDone(stats);
});
}
} }
/** /**
@ -258,13 +174,10 @@ class CleanWebpackPlugin {
* Fetch Webpack's output asset files * Fetch Webpack's output asset files
*/ */
const statsAssets = const statsAssets =
stats.toJson( stats.toJson({
{ all: false,
all: false, assets: true,
assets: true, }).assets || [];
},
true,
).assets || [];
const assets = statsAssets.map((asset: {name: string}) => { const assets = statsAssets.map((asset: {name: string}) => {
return asset.name; return asset.name;
}); });
@ -275,9 +188,7 @@ class CleanWebpackPlugin {
* (relies on del's cwd: outputPath option) * (relies on del's cwd: outputPath option)
*/ */
const staleFiles = this.currentAssets.filter((previousAsset) => { const staleFiles = this.currentAssets.filter((previousAsset) => {
const assetCurrent = assets.includes(previousAsset) === false; return assets.includes(previousAsset) === false;
return assetCurrent;
}); });
/** /**
@ -294,13 +205,6 @@ class CleanWebpackPlugin {
removePatterns.push(...staleFiles); removePatterns.push(...staleFiles);
} }
/**
* Remove cleanAfterEveryBuildPatterns
*/
if (this.cleanAfterEveryBuildPatterns.length !== 0) {
removePatterns.push(...this.cleanAfterEveryBuildPatterns);
}
if (removePatterns.length !== 0) { if (removePatterns.length !== 0) {
this.removeFiles(removePatterns); this.removeFiles(removePatterns);
} }
@ -309,10 +213,10 @@ class CleanWebpackPlugin {
removeFiles(patterns: string[]): void { removeFiles(patterns: string[]): void {
try { try {
const deleted = delSync(patterns, { const deleted = delSync(patterns, {
force: this.dangerouslyAllowCleanPatternsOutsideProject, force: false,
// Change context to build directory // Change context to build directory
cwd: this.outputPath, cwd: this.outputPath,
dryRun: this.dry, dryRun: false,
dot: true, dot: true,
ignore: this.protectWebpackAssets ? this.currentAssets : [], ignore: this.protectWebpackAssets ? this.currentAssets : [],
}); });
@ -324,15 +228,13 @@ class CleanWebpackPlugin {
deleted.forEach((file) => { deleted.forEach((file) => {
const filename = path.relative(process.cwd(), file); const filename = path.relative(process.cwd(), file);
const message = this.dry ? 'dry' : 'removed';
/** /**
* Use console.warn over .log * Use console.warn over .log
* https://github.com/webpack/webpack/issues/1904 * https://github.com/webpack/webpack/issues/1904
* https://github.com/johnagan/clean-webpack-plugin/issues/11 * https://github.com/johnagan/clean-webpack-plugin/issues/11
*/ */
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.warn(`clean-webpack-plugin: ${message} ${filename}`); console.warn(`clean-webpack-plugin: removed ${filename}`);
}); });
} }
} catch (error) { } 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 {createBaseConfig} from './base';
import WaitPlugin from './plugins/WaitPlugin'; import WaitPlugin from './plugins/WaitPlugin';
import LogPlugin from './plugins/LogPlugin'; import LogPlugin from './plugins/LogPlugin';
import {NODE_MAJOR_VERSION, NODE_MINOR_VERSION} from '../constants';
export default function createServerConfig({ export default function createServerConfig({
props, props,
@ -43,6 +44,7 @@ export default function createServerConfig({
return ssgPath; return ssgPath;
}); });
const serverConfig = merge(config, { const serverConfig = merge(config, {
target: `node${NODE_MAJOR_VERSION}.${NODE_MINOR_VERSION}`,
entry: { entry: {
main: path.resolve(__dirname, '../client/serverEntry.js'), 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) // Workaround for Webpack 4 Bug (https://github.com/webpack/webpack/issues/6522)
globalObject: 'this', globalObject: 'this',
}, },
target: 'node',
plugins: [ plugins: [
// Wait until manifest from client bundle is generated // Wait until manifest from client bundle is generated
new WaitPlugin({ new WaitPlugin({

View file

@ -6,20 +6,20 @@
*/ */
import MiniCssExtractPlugin from 'mini-css-extract-plugin'; import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import env from 'std-env'; import {
import merge from 'webpack-merge'; mergeWithCustomize,
customizeArray,
customizeObject,
CustomizeRule,
} from 'webpack-merge';
import webpack, { import webpack, {
Configuration, Configuration,
Loader,
NewLoader,
Plugin,
RuleSetRule, RuleSetRule,
Stats, WebpackPluginInstance,
} from 'webpack'; } from 'webpack';
import fs from 'fs-extra'; import fs from 'fs-extra';
import TerserPlugin from 'terser-webpack-plugin'; import TerserPlugin from 'terser-webpack-plugin';
import OptimizeCSSAssetsPlugin from 'optimize-css-assets-webpack-plugin'; import CssMinimizerPlugin from 'css-minimizer-webpack-plugin';
import CleanCss from 'clean-css';
import path from 'path'; import path from 'path';
import crypto from 'crypto'; import crypto from 'crypto';
import chalk from 'chalk'; import chalk from 'chalk';
@ -28,13 +28,13 @@ import {
ConfigureWebpackFn, ConfigureWebpackFn,
ConfigurePostCssFn, ConfigurePostCssFn,
PostCssOptions, PostCssOptions,
ConfigureWebpackUtils,
} from '@docusaurus/types'; } from '@docusaurus/types';
import CssNanoPreset from '@docusaurus/cssnano-preset';
import {version as cacheLoaderVersion} from 'cache-loader/package.json';
import { import {
BABEL_CONFIG_FILE_NAME, BABEL_CONFIG_FILE_NAME,
OUTPUT_STATIC_ASSETS_DIR_NAME, OUTPUT_STATIC_ASSETS_DIR_NAME,
} from '../constants'; } from '../constants';
import {memoize} from 'lodash';
// Utility method to get style loaders // Utility method to get style loaders
export function getStyleLoaders( export function getStyleLoaders(
@ -42,24 +42,36 @@ export function getStyleLoaders(
cssOptions: { cssOptions: {
[key: string]: unknown; [key: string]: unknown;
} = {}, } = {},
): Loader[] { ): RuleSetRule[] {
if (isServer) { if (isServer) {
return [ return cssOptions.modules
cssOptions.modules ? [
? { {
loader: require.resolve('css-loader'), loader: require.resolve('css-loader'),
options: cssOptions, 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'; return [
const loaders = [
{ {
loader: MiniCssExtractPlugin.loader, loader: MiniCssExtractPlugin.loader,
options: { 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( export function getCustomBabelConfigFilePath(
@ -141,16 +135,50 @@ export function getBabelOptions({
} }
} }
export function getBabelLoader( // Name is generic on purpose
isServer: boolean, // we want to support multiple js loader implementations (babel + esbuild)
babelOptions?: TransformOptions | string, export function getJSLoader({
): Loader { isServer,
babelOptions,
}: {
isServer: boolean;
babelOptions?: TransformOptions | string;
}): RuleSetRule {
return { return {
loader: require.resolve('babel-loader'), loader: require.resolve('babel-loader'),
options: getBabelOptions({isServer, babelOptions}), 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 * Helper function to modify webpack config
* @param configureWebpack a webpack config or a function to modify config * @param configureWebpack a webpack config or a function to modify config
@ -164,15 +192,21 @@ export function applyConfigureWebpack(
isServer: boolean, isServer: boolean,
): Configuration { ): Configuration {
// Export some utility functions // Export some utility functions
const utils = { const utils: ConfigureWebpackUtils = {
getStyleLoaders, getStyleLoaders,
getCacheLoader, getJSLoader,
getBabelLoader, getBabelLoader: getBabelLoaderDeprecated,
getCacheLoader: getCacheLoaderDeprecated,
}; };
if (typeof configureWebpack === 'function') { if (typeof configureWebpack === 'function') {
const {mergeStrategy, ...res} = configureWebpack(config, isServer, utils); const {mergeStrategy, ...res} = configureWebpack(config, isServer, utils);
if (res && typeof res === 'object') { 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; return config;
@ -182,13 +216,13 @@ export function applyConfigurePostCss(
configurePostCss: NonNullable<ConfigurePostCssFn>, configurePostCss: NonNullable<ConfigurePostCssFn>,
config: Configuration, config: Configuration,
): Configuration { ): Configuration {
type LocalPostCSSLoader = Loader & { type LocalPostCSSLoader = unknown & {
options: {postcssOptions: PostCssOptions}; options: {postcssOptions: PostCssOptions};
}; };
// TODO not ideal heuristic but good enough for our usecase? // TODO not ideal heuristic but good enough for our usecase?
function isPostCssLoader(loader: Loader): loader is LocalPostCSSLoader { function isPostCssLoader(loader: unknown): loader is LocalPostCSSLoader {
return !!(loader as NewLoader)?.options?.postcssOptions; return !!(loader as any)?.options?.postcssOptions;
} }
// Does not handle all edge cases, but good enough for now // 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; return config;
} }
// See https://webpack.js.org/configuration/stats/#statswarningsfilter export function compile(config: Configuration[]): Promise<void> {
// @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> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const compiler = webpack(config); const compiler = webpack(config);
compiler.run((err, stats) => { compiler.run((err, stats) => {
if (err) { 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 // let plugins consume all the stats
const allStats = stats?.toJson('errors-warnings'); const errorsWarnings = stats?.toJson('errors-warnings');
if (stats?.hasErrors()) { if (stats?.hasErrors()) {
allStats.errors.forEach((e) => {
console.error(e);
});
reject(new Error('Failed to compile with errors.')); reject(new Error('Failed to compile with errors.'));
} }
if (stats?.hasWarnings()) { if (errorsWarnings && stats?.hasWarnings()) {
// Custom filtering warnings (see https://github.com/webpack/webpack/issues/7841). errorsWarnings.warnings?.forEach((warning) => {
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) => {
console.warn(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 = { type FileLoaderUtils = {
loaders: { loaders: {
file: (options: {folder: AssetFolder}) => Loader; file: (options: {folder: AssetFolder}) => RuleSetRule;
url: (options: {folder: AssetFolder}) => Loader; url: (options: {folder: AssetFolder}) => RuleSetRule;
inlineMarkdownImageFileLoader: string; inlineMarkdownImageFileLoader: string;
inlineMarkdownLinkFileLoader: string; inlineMarkdownLinkFileLoader: string;
}; };
@ -298,7 +311,7 @@ export function getFileLoaderUtils(): FileLoaderUtils {
const fileLoaderFileName = (folder: AssetFolder) => const fileLoaderFileName = (folder: AssetFolder) =>
`${OUTPUT_STATIC_ASSETS_DIR_NAME}/${folder}/[name]-[hash].[ext]`; `${OUTPUT_STATIC_ASSETS_DIR_NAME}/${folder}/[name]-[hash].[ext]`;
const loaders = { const loaders: FileLoaderUtils['loaders'] = {
file: (options: {folder: AssetFolder}) => { file: (options: {folder: AssetFolder}) => {
return { return {
loader: require.resolve(`file-loader`), 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 * Loads image assets, inlines images via a data URI if they are below
* the size threshold * the size threshold
*/ */
images: (): RuleSetRule => { images: () => {
return { return {
use: [loaders.url({folder: 'images'})], use: [loaders.url({folder: 'images'})],
test: /\.(ico|jpg|jpeg|png|gif|webp)(\?.*)?$/, test: /\.(ico|jpg|jpeg|png|gif|webp)(\?.*)?$/,
}; };
}, },
fonts: (): RuleSetRule => { fonts: () => {
return { return {
use: [loaders.url({folder: 'fonts'})], use: [loaders.url({folder: 'fonts'})],
test: /\.(woff|woff2|eot|ttf|otf)$/, 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 * Loads audio and video and inlines them via a data URI if they are below
* the size threshold * the size threshold
*/ */
media: (): RuleSetRule => { media: () => {
return { return {
use: [loaders.url({folder: 'medias'})], use: [loaders.url({folder: 'medias'})],
test: /\.(mp4|webm|ogv|wav|mp3|m4a|aac|oga|flac)$/, test: /\.(mp4|webm|ogv|wav|mp3|m4a|aac|oga|flac)$/,
}; };
}, },
svg: (): RuleSetRule => { svg: () => {
return { return {
test: /\.svg?$/, test: /\.svg?$/,
oneOf: [ oneOf: [
@ -383,7 +396,7 @@ export function getFileLoaderUtils(): FileLoaderUtils {
// We don't want to use SVGR loader for non-React source code // We don't want to use SVGR loader for non-React source code
// ie we don't want to use SVGR for CSS files... // ie we don't want to use SVGR for CSS files...
issuer: { 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 { return {
use: [loaders.file({folder: 'files'})], use: [loaders.file({folder: 'files'})],
test: /\.(pdf|doc|docx|xls|xlsx|zip|rar)$/, test: /\.(pdf|doc|docx|xls|xlsx|zip|rar)$/,
@ -476,12 +489,12 @@ function getTerserParallel() {
return terserParallel; return terserParallel;
} }
export function getMinimizer(useSimpleCssMinifier = false): Plugin[] { export function getMinimizer(
useSimpleCssMinifier = false,
): WebpackPluginInstance[] {
const minimizer = [ const minimizer = [
new TerserPlugin({ new TerserPlugin({
cache: true,
parallel: getTerserParallel(), parallel: getTerserParallel(),
sourceMap: false,
terserOptions: { terserOptions: {
parse: { parse: {
// we want uglify-js to parse ecma 8 code. However, we don't want it // 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) { if (useSimpleCssMinifier) {
minimizer.push( minimizer.push(new CssMinimizerPlugin());
new OptimizeCSSAssetsPlugin({
cssProcessorPluginOptions: {
preset: 'default',
},
}),
);
} else { } else {
minimizer.push( minimizer.push(
...[ // Using the array syntax to add 2 minimizers
new OptimizeCSSAssetsPlugin({ // see https://github.com/webpack-contrib/css-minimizer-webpack-plugin#array
cssProcessorPluginOptions: { new CssMinimizerPlugin({
preset: CssNanoPreset, minimizerOptions: [
// CssNano options
{
preset: require.resolve('@docusaurus/cssnano-preset'),
}, },
}), // CleanCss options
new OptimizeCSSAssetsPlugin({ {
cssProcessor: CleanCss,
cssProcessorOptions: {
inline: false, inline: false,
level: { level: {
1: { 1: {
@ -540,8 +547,12 @@ export function getMinimizer(useSimpleCssMinifier = false): Plugin[] {
}, },
}, },
}, },
}), ],
], minify: [
CssMinimizerPlugin.cssnanoMinify,
CssMinimizerPlugin.cleanCssMinify,
],
}),
); );
} }

View file

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

View file

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

View file

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

View file

@ -398,7 +398,6 @@ type Props = {
postBodyTags: string; postBodyTags: string;
routesPaths: string[]; routesPaths: string[];
plugins: Plugin<any>[]; plugins: Plugin<any>[];
stats: Stats.ToJsonOutput;
}; };
``` ```
@ -408,13 +407,11 @@ Example:
module.exports = function (context, options) { module.exports = function (context, options) {
return { return {
name: 'docusaurus-plugin', name: 'docusaurus-plugin',
async postBuild({siteConfig = {}, routesPaths = [], outDir, stats}) { async postBuild({siteConfig = {}, routesPaths = [], outDir}) {
// Print out to console all the rendered routes. // Print out to console all the rendered routes.
routesPaths.map((route) => { routesPaths.map((route) => {
console.log(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] [build]
base = "/" command = "yarn --cwd .. build:packages && yarn build"
command = "yarn workspace docusaurus-2-website netlify:build:production"
publish = "website/build" publish = "website/build"
# we build deploy previews with a /build/ baseUrl on purpose [build.environment]
# permits to test that baseUrl works fine (this often breaks!) 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] [context.deploy-preview]
command = "yarn workspace docusaurus-2-website netlify:build:deployPreview" command = "yarn --cwd .. build:packages && yarn netlify:build:deployPreview"
publish = "website/build"
[[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", "@docusaurus/theme-live-codeblock": "2.0.0-alpha.74",
"clsx": "^1.1.1", "clsx": "^1.1.1",
"color": "^3.1.3", "color": "^3.1.3",
"netlify-plugin-cache": "^1.0.3",
"npm-to-yarn": "^1.0.0-2", "npm-to-yarn": "^1.0.0-2",
"react": "^17.0.1", "react": "^17.0.1",
"react-dom": "^17.0.1", "react-dom": "^17.0.1",

1810
yarn.lock

File diff suppressed because it is too large Load diff