Merge pull request #5292 from backstage/blam/webpack5

Migrating build tooling to Webpack 5
This commit is contained in:
Ben Lambert
2021-08-07 16:18:21 +02:00
committed by GitHub
16 changed files with 2983 additions and 1882 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/cli': patch
'@backstage/create-app': patch
---
Migrate to using `webpack@5` 🎉
@@ -50,4 +50,5 @@ jobs:
# projetToken intentionally shared to allow collaborators to run Chromatic on forks
# https://www.chromatic.com/docs/custom-ci-provider#run-chromatic-on-external-forks-of-open-source-projects
projectToken: 9tzak77m9nj
storybookBuildDir: 'packages/storybook/dist'
workingDir: 'packages/storybook'
storybookBuildDir: 'dist'
+5
View File
@@ -38,6 +38,10 @@
"packages": [
"packages/*",
"plugins/*"
],
"nohoist": [
"**/@storybook/**",
"**/@storybook"
]
},
"resolutions": {
@@ -55,6 +59,7 @@
"@microsoft/api-extractor-model": "^7.13.3"
},
"devDependencies": {
"@types/webpack": "^5.28.0",
"@changesets/cli": "^2.14.0",
"@octokit/openapi-types": "^2.2.0",
"@spotify/prettier-config": "^10.0.0",
+15 -14
View File
@@ -50,9 +50,7 @@
"@svgr/plugin-svgo": "5.4.x",
"@svgr/rollup": "5.5.x",
"@svgr/webpack": "5.5.x",
"@types/start-server-webpack-plugin": "^2.2.0",
"@types/webpack-env": "^1.15.2",
"@types/webpack-node-externals": "^2.5.0",
"@typescript-eslint/eslint-plugin": "^v4.28.3",
"@typescript-eslint/parser": "^v4.28.3",
"@yarnpkg/lockfile": "^1.1.0",
@@ -76,18 +74,19 @@
"eslint-plugin-react-hooks": "^4.0.0",
"express": "^4.17.1",
"file-loader": "^6.2.0",
"fork-ts-checker-webpack-plugin": "^6.2.9",
"fork-ts-checker-webpack-plugin": "^4.0.5",
"fs-extra": "9.1.0",
"handlebars": "^4.7.3",
"html-webpack-plugin": "^4.3.0",
"html-webpack-plugin": "^5.3.1",
"inquirer": "^7.0.4",
"jest": "^26.0.1",
"jest-css-modules": "^2.1.0",
"json-schema": "^0.3.0",
"lodash": "^4.17.19",
"mini-css-extract-plugin": "^0.9.0",
"mini-css-extract-plugin": "^1.4.1",
"ora": "^5.3.0",
"postcss": "^8.1.0",
"process": "^0.11.10",
"raw-loader": "^4.0.1",
"react": "^16.0.0",
"react-dev-utils": "^11.0.4",
@@ -100,17 +99,18 @@
"rollup-plugin-peer-deps-external": "^2.2.2",
"rollup-plugin-postcss": "^4.0.0",
"rollup-pluginutils": "^2.8.2",
"run-script-webpack-plugin": "^0.0.11",
"semver": "^7.3.2",
"start-server-webpack-plugin": "^2.2.5",
"style-loader": "^1.2.1",
"sucrase": "^3.18.2",
"tar": "^6.1.2",
"terser-webpack-plugin": "^1.4.3",
"terser-webpack-plugin": "^5.1.3",
"ts-loader": "^8.0.17",
"typescript": "^4.0.3",
"url-loader": "^4.1.0",
"webpack": "^4.41.6",
"webpack-dev-server": "3.11.0",
"util": "^0.12.3",
"webpack": "^5.48.0",
"webpack-dev-server": "4.0.0-rc.0",
"webpack-node-externals": "^3.0.0",
"yaml": "^1.10.0",
"yaml-jest": "^1.0.5",
@@ -129,25 +129,26 @@
"@types/diff": "^5.0.0",
"@types/express": "^4.17.6",
"@types/fs-extra": "^9.0.1",
"@types/html-webpack-plugin": "^3.2.2",
"@types/http-proxy": "^1.17.4",
"@types/inquirer": "^7.3.1",
"@types/mini-css-extract-plugin": "^1.2.2",
"@types/mock-fs": "^4.13.0",
"@types/node": "^14.14.32",
"@types/react-dev-utils": "^9.0.4",
"@types/recursive-readdir": "^2.2.0",
"@types/rollup-plugin-peer-deps-external": "^2.2.0",
"@types/rollup-plugin-postcss": "^2.0.0",
"@types/tar": "^4.0.3",
"@types/webpack": "^4.41.7",
"@types/webpack-dev-server": "^3.11.0",
"@types/terser-webpack-plugin": "^5.0.4",
"@types/webpack": "^5.28.0",
"@types/webpack-dev-server": "^3.11.5",
"@types/yarnpkg__lockfile": "^1.1.4",
"del": "^6.0.0",
"mock-fs": "^4.13.0",
"nodemon": "^2.0.2",
"ts-node": "^10.0.0"
},
"resolutions": {
"@types/webpack-dev-server/@types/webpack": "^5.28.0"
},
"files": [
"asset-types",
"templates",
@@ -15,14 +15,14 @@
*/
import { resolve as resolvePath } from 'path';
import { ResolvePlugin } from 'webpack';
import { WebpackPluginInstance } from 'webpack';
import { isChildPath } from '@backstage/cli-common';
import { LernaPackage } from './types';
// Enables proper resolution of packages when linking in external packages.
// Without this the packages would depend on dependencies in the node_modules
// of the external packages themselves, leading to module duplication
export class LinkedPackageResolvePlugin implements ResolvePlugin {
export class LinkedPackageResolvePlugin implements WebpackPluginInstance {
constructor(
private readonly targetModules: string,
private readonly packages: LernaPackage[],
+7 -13
View File
@@ -26,23 +26,17 @@ export async function serveBackend(options: BackendServeOptions) {
isDev: true,
});
const compiler = webpack(config);
const watcher = compiler.watch(
{
poll: true,
},
(err: Error) => {
if (err) {
console.error(err);
} else console.log('Build succeeded');
},
);
const compiler = webpack(config, (err: Error | undefined) => {
if (err) {
console.log('here');
console.error(err);
} else console.log('Build succeeded');
});
const waitForExit = async () => {
for (const signal of ['SIGINT', 'SIGTERM'] as const) {
process.on(signal, () => {
watcher.close(() => console.log('Stopped watcher'));
compiler.close(() => console.log('Stopped watcher'));
// exit instead of resolve. The process is shutting down and resolving a promise here logs an error
process.exit();
});
+26 -15
View File
@@ -69,6 +69,10 @@ export async function buildBundle(options: BuildOptions) {
throw new Error(`Failed to compile.\n${error.message || error}`);
});
if (!stats) {
throw new Error('No stats returned');
}
if (statsJsonEnabled) {
// No @types/bfj
await require('bfj').write(
@@ -87,25 +91,32 @@ export async function buildBundle(options: BuildOptions) {
}
async function build(compiler: webpack.Compiler, isCi: boolean) {
const stats = await new Promise<webpack.Stats>((resolve, reject) => {
compiler.run((err, buildStats) => {
if (err) {
if (err.message) {
const { errors } = formatWebpackMessages({
errors: [err.message],
warnings: new Array<string>(),
} as webpack.Stats.ToJsonOutput);
const stats = await new Promise<webpack.Stats | undefined>(
(resolve, reject) => {
compiler.run((err, buildStats) => {
if (err) {
if (err.message) {
const { errors } = formatWebpackMessages({
errors: [err.message],
warnings: new Array<string>(),
_showErrors: true,
_showWarnings: true,
});
throw new Error(errors[0]);
throw new Error(errors[0]);
} else {
reject(err);
}
} else {
reject(err);
resolve(buildStats);
}
} else {
resolve(buildStats);
}
});
});
});
},
);
if (!stats) {
throw new Error('No stats provided');
}
const { errors, warnings } = formatWebpackMessages(
stats.toJson({ all: false, warnings: true, errors: true }),
);
+42 -30
View File
@@ -19,8 +19,8 @@ import { resolve as resolvePath } from 'path';
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import ModuleScopePlugin from 'react-dev-utils/ModuleScopePlugin';
import StartServerPlugin from 'start-server-webpack-plugin';
import webpack from 'webpack';
import { RunScriptWebpackPlugin } from 'run-script-webpack-plugin';
import webpack, { ProvidePlugin } from 'webpack';
import nodeExternals from 'webpack-node-externals';
import { isChildPath } from '@backstage/cli-common';
import { optimization } from './optimization';
@@ -98,10 +98,9 @@ export async function createConfig(
if (checksEnabled) {
plugins.push(
new ForkTsCheckerWebpackPlugin({
typescript: {
configFile: paths.targetTsConfig,
},
eslint: {
typescript: paths.targetTsConfig,
eslint: true,
eslintOptions: {
files: ['**', '!**/__tests__/**', '!**/?(*.)(spec|test).*'],
options: {
parserOptions: {
@@ -114,6 +113,15 @@ export async function createConfig(
);
}
// TODO(blam): process is no longer auto polyfilled by webpack in v5.
// we use the provide plugin to provide this polyfill, but lets look
// to remove this eventually!
plugins.push(
new ProvidePlugin({
process: 'process/browser',
}),
);
plugins.push(
new webpack.EnvironmentPlugin({
APP_CONFIG: options.frontendAppConfigs,
@@ -156,27 +164,33 @@ export async function createConfig(
return {
mode: isDev ? 'development' : 'production',
profile: false,
node: {
module: 'empty',
dgram: 'empty',
dns: 'mock',
fs: 'empty',
http2: 'empty',
net: 'empty',
tls: 'empty',
child_process: 'empty',
},
optimization: optimization(options),
bail: false,
performance: {
hints: false, // we check the gzip size instead
},
devtool: isDev ? 'cheap-module-eval-source-map' : 'source-map',
devtool: isDev ? 'eval-cheap-module-source-map' : 'source-map',
context: paths.targetPath,
entry: [require.resolve('react-hot-loader/patch'), paths.targetEntry],
resolve: {
extensions: ['.ts', '.tsx', '.mjs', '.js', '.jsx'],
mainFields: ['browser', 'module', 'main'],
fallback: {
module: false,
dgram: false,
dns: false,
fs: false,
http2: false,
net: false,
tls: false,
child_process: false,
/* new ignores */
path: false,
https: false,
http: false,
util: require.resolve('util/'),
},
plugins: [
new LinkedPackageResolvePlugin(paths.rootNodeModules, externalPkgs),
new ModuleScopePlugin(
@@ -200,7 +214,7 @@ export async function createConfig(
: 'static/[name].[chunkhash:8].chunk.js',
...(isDev
? {
devtoolModuleFilenameTemplate: info =>
devtoolModuleFilenameTemplate: (info: any) =>
`file:///${resolvePath(info.absoluteResourcePath).replace(
/\\/g,
'/',
@@ -237,7 +251,7 @@ export async function createBackendConfig(
? {
watch: true,
watchOptions: {
ignored: [/node_modules\/(?!\@backstage)/],
ignored: /node_modules\/(?!\@backstage)/,
},
}
: {}),
@@ -259,7 +273,7 @@ export async function createBackendConfig(
performance: {
hints: false, // we check the gzip size instead
},
devtool: isDev ? 'cheap-module-eval-source-map' : 'source-map',
devtool: isDev ? 'eval-cheap-module-source-map' : 'source-map',
context: paths.targetPath,
entry: [
'webpack/hot/poll?100',
@@ -291,7 +305,7 @@ export async function createBackendConfig(
: '[name].[chunkhash:8].chunk.js',
...(isDev
? {
devtoolModuleFilenameTemplate: info =>
devtoolModuleFilenameTemplate: (info: any) =>
`file:///${resolvePath(info.absoluteResourcePath).replace(
/\\/g,
'/',
@@ -300,7 +314,7 @@ export async function createBackendConfig(
: {}),
},
plugins: [
new StartServerPlugin({
new RunScriptWebpackPlugin({
name: 'main.js',
nodeArgs: options.inspectEnabled ? ['--inspect'] : undefined,
}),
@@ -308,10 +322,9 @@ export async function createBackendConfig(
...(checksEnabled
? [
new ForkTsCheckerWebpackPlugin({
typescript: {
configFile: paths.targetTsConfig,
},
eslint: {
typescript: paths.targetTsConfig,
eslint: true,
eslintOptions: {
files: ['**', '!**/__tests__/**', '!**/?(*.)(spec|test).*'],
options: {
parserOptions: {
@@ -347,11 +360,10 @@ function nodeExternalsWithResolve(
});
return (
context: string,
request: string,
callback: webpack.ExternalsFunctionCallback,
{ context, request }: { context?: string; request?: string },
callback: any,
) => {
currentContext = context;
currentContext = context!;
return externals(context, request, callback);
};
}
+4 -4
View File
@@ -14,14 +14,14 @@
* limitations under the License.
*/
import { Options } from 'webpack';
import { WebpackOptionsNormalized, WebpackPluginInstance } from 'webpack';
import TerserPlugin from 'terser-webpack-plugin';
import { BundlingOptions } from './types';
import { isParallelDefault } from '../parallel';
export const optimization = (
options: BundlingOptions,
): Options.Optimization => {
): WebpackOptionsNormalized['optimization'] => {
const { isDev } = options;
return {
@@ -30,9 +30,9 @@ export const optimization = (
...(!isParallelDefault(options.parallel)
? {
minimizer: [
new TerserPlugin({
(new TerserPlugin({
parallel: options.parallel,
}),
}) as unknown) as WebpackPluginInstance,
],
}
: {}),
+25 -19
View File
@@ -42,25 +42,31 @@ export async function serveBundle(options: ServeOptions) {
});
const compiler = webpack(config);
const server = new WebpackDevServer(compiler, {
hot: !process.env.CI,
contentBase: paths.targetPublic,
contentBasePublicPath: config.output?.publicPath,
publicPath: config.output?.publicPath,
historyApiFallback: {
// Paths with dots should still use the history fallback.
// See https://github.com/facebookincubator/create-react-app/issues/387.
disableDotRule: true,
},
clientLogLevel: 'warning',
stats: 'errors-warnings',
https: url.protocol === 'https:',
host,
port,
proxy: pkg.proxy,
// When the dev server is behind a proxy, the host and public hostname differ
allowedHosts: [url.hostname],
});
const server = new WebpackDevServer(
compiler as any,
{
hot: !process.env.CI,
devMiddleware: {
publicPath: config.output?.publicPath as string,
stats: 'errors-warnings',
},
static: {
publicPath: config.output?.publicPath as string,
directory: paths.targetPublic,
},
historyApiFallback: {
// Paths with dots should still use the history fallback.
// See https://github.com/facebookincubator/create-react-app/issues/387.
disableDotRule: true,
},
https: url.protocol === 'https:',
host,
port,
proxy: pkg.proxy,
// When the dev server is behind a proxy, the host and public hostname differ
allowedHosts: [url.hostname],
} as any,
);
await new Promise<void>((resolve, reject) => {
server.listen(port, host, (err?: Error) => {
+10 -4
View File
@@ -14,13 +14,13 @@
* limitations under the License.
*/
import webpack, { Module, Plugin } from 'webpack';
import webpack, { ModuleOptions, WebpackPluginInstance } from 'webpack';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import { svgrTemplate } from '../svgrTemplate';
type Transforms = {
loaders: Module['rules'];
plugins: Plugin[];
loaders: ModuleOptions['rules'];
plugins: WebpackPluginInstance[];
};
type TransformOptions = {
@@ -51,6 +51,12 @@ export const transforms = (options: TransformOptions): Transforms => {
production: !isDev,
},
},
{
test: /\.m?js/,
resolve: {
fullySpecified: false,
},
},
{
test: [/\.icon\.svg$/],
use: [
@@ -105,7 +111,7 @@ export const transforms = (options: TransformOptions): Transforms => {
},
];
const plugins = new Array<Plugin>();
const plugins = new Array<WebpackPluginInstance>();
if (isDev) {
plugins.push(new webpack.HotModuleReplacementPlugin());
+227 -1
View File
@@ -29,5 +29,231 @@ declare module '@svgr/rollup' {
}
declare module '@rollup/plugin-yaml';
declare module 'react-dev-utils/formatWebpackMessages' {
export default function (
stats: any,
): {
errors: string[];
warnings: string[];
};
}
declare module 'terser-webpack-plugin';
declare module 'react-dev-utils/openBrowser' {
export default function (url: string): boolean;
}
declare module 'react-dev-utils/ModuleScopePlugin' {
import webpack = require('webpack');
export default class ModuleScopePlugin
implements webpack.WebpackPluginInstance {
constructor(
appSrc: string | ReadonlyArray<string>,
allowedFiles?: ReadonlyArray<string>,
);
apply: (resolver: any) => void;
}
}
declare module 'react-dev-utils/FileSizeReporter' {
import webpack = require('webpack');
export interface OpaqueFileSizes {
root: string;
sizes: Record<string, number>;
}
/**
* Captures JS and CSS asset sizes inside the passed `buildFolder`. Save the
* result value to compare it after the build.
*/
export function measureFileSizesBeforeBuild(
buildFolder: string,
): Promise<OpaqueFileSizes>;
/**
* Prints the JS and CSS asset sizes after the build, and includes a size
* comparison with `previousFileSizes` that were captured earlier using
* `measureFileSizesBeforeBuild()`. `maxBundleGzipSize` and
* `maxChunkGzipSizemay` may optionally be specified to display a warning when
* the main bundle or a chunk exceeds the specified size (in bytes).
*/
export function printFileSizesAfterBuild(
webpackStats: webpack.Stats,
previousFileSizes: OpaqueFileSizes,
buildFolder: string,
maxBundleGzipSize?: number,
maxChunkGzipSize?: number,
): void;
}
declare module 'mini-css-extract-plugin' {
import webpack = require('webpack');
/**
* Lightweight CSS extraction webpack plugin.
*
* This plugin extracts CSS into separate files. It creates a CSS file per JS file which
* contains CSS. It supports On-Demand-Loading of CSS and SourceMaps.
*
* Configuration Detail: https://github.com/webpack-contrib/mini-css-extract-plugin#configuration
*/
export default class MiniCssExtractPlugin {
/**
* Webpack loader always used at the end of loaders list (ie. array index zero).
*/
static loader: string;
constructor(options?: MiniCssExtractPlugin.PluginOptions);
/**
* Apply the plugin
*/
apply(compiler: webpack.Compiler): void;
}
namespace MiniCssExtractPlugin {
interface PluginOptions {
/**
* Works like [`output.filename`](https://webpack.js.org/configuration/output/#outputfilename).
*/
filename?: Required<webpack.Configuration>['output']['filename'];
/**
* Works like [`output.chunkFilename`](https://webpack.js.org/configuration/output/#outputchunkfilename).
*/
chunkFilename?: string;
/**
* For projects where CSS ordering has been mitigated through consistent
* use of scoping or naming conventions, the CSS order warnings can be
* disabled by setting this flag to true for the plugin.
*/
ignoreOrder?: boolean;
/**
* Specify where to insert the link tag.
*
* A string value specifies a DOM query for a parent element to attach to.
*
* A function allows to override default behavior for non-entry CSS chunks.
* This code will run in the browser alongside your application. It is recommend
* to only use ECMA 5 features and syntax. The function won't have access to the
* scope of the webpack configuration module.
*
* @default function() { document.head.appendChild(linkTag); }
*/
insert?: string | ((linkTag: any) => void);
/**
* Specify additional html attributes to add to the link tag.
*
* Note: These are only applied to dynamically loaded css chunks. To modify link
* attributes for entry CSS chunks, please use html-webpack-plugin.
*/
attributes?: Record<string, string>;
/**
* This option allows loading asynchronous chunks with a custom link type, such as
* `<link type="text/css" ...>`.
*
* `false` disables the link `type` attribute.
*
* @default 'text/css'
*/
linkType?: string | false | 'text/css';
}
interface LoaderOptions {
/**
* Overrides [`output.publicPath`](https://webpack.js.org/configuration/output/#outputpublicpath).
* @default output.publicPath
*/
publicPath?: string | ((resourcePath: string, context: string) => string);
/**
* If false, the plugin will extract the CSS but **will not** emit the file
* @default true
*/
emit?: boolean;
/**
* By default, `mini-css-extract-plugin` generates JS modules that use the ES modules syntax.
* There are some cases in which using ES modules is beneficial,
* like in the case of module concatenation and tree shaking.
* @default true
*/
esModule?: boolean;
modules?: {
/**
* Enables/disables ES modules named export for locals.
*
* Names of locals are converted to camelCase. It is not allowed to use
* JavaScript reserved words in CSS class names. Options `esModule` and
* `modules.namedExport` in css-loader and MiniCssExtractPlugin.loader
* must be enabled.
*
* @default false
*/
namedExport?: boolean;
};
}
}
}
declare module 'fork-ts-checker-webpack-plugin/lib/ForkTsCheckerWebpackPlugin' {}
declare module 'webpack-node-externals' {
export default function webpackNodeExternals(
options?: webpackNodeExternals.Options,
): any;
namespace webpackNodeExternals {
type AllowlistOption = string | RegExp | AllowlistFunctionType;
type ImportTypeCallback = (moduleName: string) => string;
/** a function that accepts the module name and returns whether it should be included */
type AllowlistFunctionType = (moduleName: string) => boolean;
interface ModulesFromFileType {
exclude?: string | string[];
include?: string | string[];
}
interface Options {
/**
* An array for the externals to allow, so they will be included in the bundle.
* Can accept exact strings ('module_name'), regex patterns (/^module_name/), or a
* function that accepts the module name and returns whether it should be included.
* Important - if you have set aliases in your webpack config with the exact
* same names as modules in node_modules, you need to allowlist them so Webpack will know
* they should be bundled.
* @default []
*/
allowlist?: AllowlistOption[] | AllowlistOption;
/**
* @default ['.bin']
*/
binaryDirs?: string[];
/**
* The method in which unbundled modules will be required in the code. Best to leave as
* 'commonjs' for node modules.
* @default 'commonjs'
*/
importType?:
| 'var'
| 'this'
| 'commonjs'
| 'amd'
| 'umd'
| ImportTypeCallback;
/**
* The folder in which to search for the node modules.
* @default 'node_modules'
*/
modulesDir?: string;
/**
* Additional folders to look for node modules.
*/
additionalModuleDirs?: string[];
/**
* Read the modules from the package.json file instead of the node_modules folder.
* @default false
*/
modulesFromFile?: boolean | ModulesFromFileType;
/**
* @default false
*/
includeAbsolutePaths?: boolean;
}
}
}
+2 -1
View File
@@ -33,6 +33,7 @@
"@backstage/core-plugin-api": "^0.1.5",
"@backstage/errors": "^0.1.1",
"@backstage/theme": "^0.2.9",
"@material-table/core": "^3.1.0",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -50,7 +51,6 @@
"dagre": "^0.8.5",
"immer": "^9.0.1",
"lodash": "^4.17.15",
"@material-table/core": "^3.1.0",
"pluralize": "^8.0.0",
"prop-types": "^15.7.2",
"qs": "^6.9.4",
@@ -84,6 +84,7 @@
"@types/jest": "^26.0.7",
"@types/node": "^14.14.32",
"@types/react-helmet": "^6.1.0",
"@types/react-syntax-highlighter": "^13.5.2",
"@types/zen-observable": "^0.8.0"
},
"files": [
-1
View File
@@ -39,7 +39,6 @@
"devDependencies": {
"@types/fs-extra": "^9.0.1",
"@types/inquirer": "^7.3.1",
"@types/react-dev-utils": "^9.0.4",
"@types/recursive-readdir": "^2.2.0",
"ts-node": "^10.0.0"
},
-6
View File
@@ -7,12 +7,6 @@
"start": "start-storybook -p 6006",
"build-storybook": "build-storybook --output-dir dist"
},
"workspaces": {
"nohoist": [
"@storybook/react/**",
"@storybook/addons/**"
]
},
"dependencies": {
"@backstage/theme": "^0.2.0",
"react": "^16.12.0",
+2610 -1771
View File
File diff suppressed because it is too large Load Diff