From 1c30b49d3d5d5e16e72cdcc25c9ed149164c6cd6 Mon Sep 17 00:00:00 2001 From: Subho <108616679+subhoghoshX@users.noreply.github.com> Date: Wed, 12 Apr 2023 13:53:20 +0530 Subject: [PATCH] Move from webpack to esbuild bundler --- .babelrc.json | 14 --- .eslintrc.json | 1 - Makefile | 4 +- README.md | 6 +- build.js | 136 +++++++++++++++++++++ package.json | 29 ++--- packaging/cockpit-starter-kit.spec.in | 2 +- src/index.html | 3 +- test/run | 4 + webpack.config.js | 168 -------------------------- 10 files changed, 156 insertions(+), 211 deletions(-) delete mode 100644 .babelrc.json create mode 100755 build.js delete mode 100644 webpack.config.js diff --git a/.babelrc.json b/.babelrc.json deleted file mode 100644 index dfe57f2..0000000 --- a/.babelrc.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "presets": [ - ["@babel/env", { - "targets": { - "chrome": "57", - "firefox": "52", - "safari": "10.3", - "edge": "16", - "opera": "44" - } - }], - "@babel/preset-react" - ] -} diff --git a/.eslintrc.json b/.eslintrc.json index f2b0ea2..7818ff7 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -5,7 +5,6 @@ "es6": true }, "extends": ["eslint:recommended", "standard", "standard-jsx", "react-app"], - "parser": "@babel/eslint-parser", "parserOptions": { "ecmaVersion": "7", "ecmaFeatures": { diff --git a/Makefile b/Makefile index e590c8f..abaa718 100644 --- a/Makefile +++ b/Makefile @@ -81,8 +81,8 @@ $(SPEC): packaging/$(SPEC).in $(NODE_MODULES_TEST) provides=$$(npm ls --omit dev --package-lock-only --depth=Infinity | grep -Eo '[^[:space:]]+@[^[:space:]]+' | sort -u | sed 's/^/Provides: bundled(npm(/; s/\(.*\)@/\1)) = /'); \ awk -v p="$$provides" '{gsub(/%{VERSION}/, "$(VERSION)"); gsub(/%{NPM_PROVIDES}/, p)}1' $< > $@ -$(DIST_TEST): $(NODE_MODULES_TEST) $(COCKPIT_REPO_STAMP) $(shell find src/ -type f) package.json webpack.config.js - NODE_ENV=$(NODE_ENV) node_modules/.bin/webpack +$(DIST_TEST): $(NODE_MODULES_TEST) $(COCKPIT_REPO_STAMP) $(shell find src/ -type f) package.json build.js + NODE_ENV=$(NODE_ENV) ./build.js watch: NODE_ENV=$(NODE_ENV) npm run watch diff --git a/README.md b/README.md index 5ea7479..0ac7bc4 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ After changing the code and running `make` again, reload the Cockpit page in your browser. You can also use -[watch mode](https://webpack.js.org/guides/development/#using-watch-mode) to +[watch mode](https://esbuild.github.io/api/#watch) to automatically update the bundle on every code change with $ npm run watch @@ -106,11 +106,11 @@ Violations of some rules can be fixed automatically by: Rules configuration can be found in the `.stylelintrc.json` file. -During fast iterative development, you can also choose to not run stylelint. +During fast iterative development, you can also choose to not run eslint/stylelint. This speeds up the build and avoids build failures due to e. g. ill-formatted css or other issues: - $ make STYLELINT=0 + $ make LINT=0 # Running tests locally diff --git a/build.js b/build.js new file mode 100755 index 0000000..6b62d01 --- /dev/null +++ b/build.js @@ -0,0 +1,136 @@ +#!/usr/bin/env node + +import fs from 'node:fs'; +import path from 'node:path'; +import os from 'node:os'; + +import copy from 'esbuild-plugin-copy'; + +import { cleanPlugin } from './pkg/lib/esbuild-cleanup-plugin.js'; +import { cockpitCompressPlugin } from './pkg/lib/esbuild-compress-plugin.js'; +import { cockpitPoEsbuildPlugin } from './pkg/lib/cockpit-po-plugin.js'; +import { cockpitRsyncEsbuildPlugin } from './pkg/lib/cockpit-rsync-plugin.js'; +import { esbuildStylesPlugins } from './pkg/lib/esbuild-common.js'; +import { eslintPlugin } from './pkg/lib/esbuild-eslint-plugin.js'; +import { stylelintPlugin } from './pkg/lib/esbuild-stylelint-plugin.js'; + +const useWasm = os.arch() !== 'x64'; +const esbuild = (await import(useWasm ? 'esbuild-wasm' : 'esbuild')).default; + +const production = process.env.NODE_ENV === 'production'; +const watchMode = process.env.ESBUILD_WATCH === "true"; +// linters dominate the build time, so disable them for production builds by default, but enable in watch mode +const lint = process.env.LINT ? (process.env.LINT !== 0) : (watchMode || !production); +// List of directories to use when using import statements +const nodePaths = ['pkg/lib']; +const outdir = 'dist'; + +// Obtain package name from package.json +const packageJson = JSON.parse(fs.readFileSync('package.json')); + +function notifyEndPlugin() { + return { + name: 'notify-end', + setup(build) { + let startTime; + + build.onStart(() => { + startTime = new Date(); + }); + + build.onEnd(() => { + const endTime = new Date(); + const timeStamp = endTime.toTimeString().split(' ')[0]; + console.log(`${timeStamp}: Build finished in ${endTime - startTime} ms`); + }); + } + }; +} + +const cwd = process.cwd(); + +// similar to fs.watch(), but recursively watches all subdirectories +function watch_dirs(dir, on_change) { + const callback = (ev, dir, fname) => { + // only listen for "change" events, as renames are noisy + // ignore hidden files + const isHidden = /^\./.test(fname); + if (ev !== "change" || isHidden) { + return; + } + on_change(path.join(dir, fname)); + }; + + fs.watch(dir, {}, (ev, path) => callback(ev, dir, path)); + + // watch all subdirectories in dir + const d = fs.opendirSync(dir); + let dirent; + + while ((dirent = d.readSync()) !== null) { + if (dirent.isDirectory()) + watch_dirs(path.join(dir, dirent.name), on_change); + } + d.closeSync(); +} + +const context = await esbuild.context({ + ...!production ? { sourcemap: "linked" } : {}, + bundle: true, + entryPoints: ['./src/index.js'], + external: ['*.woff', '*.woff2', '*.jpg', '*.svg', '../../assets*'], // Allow external font files which live in ../../static/fonts + legalComments: 'external', // Move all legal comments to a .LEGAL.txt file + loader: { ".js": "jsx" }, + minify: production, + nodePaths, + outdir, + target: ['es2020'], + plugins: [ + cleanPlugin(), + ...lint + ? [ + stylelintPlugin({ filter: new RegExp(cwd + '\/src\/.*\.(css?|scss?)$') }), + eslintPlugin({ filter: new RegExp(cwd + '\/src\/.*\.(jsx?|js?)$') }) + ] + : [], + // Esbuild will only copy assets that are explicitly imported and used + // in the code. This is a problem for index.html and manifest.json which are not imported + copy({ + assets: [ + { from: ['./src/manifest.json'], to: ['./manifest.json'] }, + { from: ['./src/index.html'], to: ['./index.html'] }, + ] + }), + ...esbuildStylesPlugins, + cockpitPoEsbuildPlugin(), + ...production ? [cockpitCompressPlugin()] : [], + cockpitRsyncEsbuildPlugin({ dest: packageJson.name }), + notifyEndPlugin(), + ] +}); + +try { + await context.rebuild(); +} catch (e) { + if (!watchMode) + process.exit(1); + // ignore errors in watch mode +} + +if (watchMode) { + const on_change = async path => { + console.log("change detected:", path); + await context.cancel(); + + try { + await context.rebuild(); + } catch (e) {} // ignore in watch mode + }; + + watch_dirs('src', on_change); + + // wait forever until Control-C + await new Promise(() => {}); +} + +context.dispose(); diff --git a/package.json b/package.json index 0bbcd20..f3d5179 100644 --- a/package.json +++ b/package.json @@ -7,25 +7,21 @@ "author": "", "license": "LGPL-2.1", "scripts": { - "watch": "webpack --watch --progress", - "build": "webpack", + "watch": "ESBUILD_WATCH='true' ./build.js", + "build": "./build.js", "eslint": "eslint --ext .js --ext .jsx src/", "eslint:fix": "eslint --fix --ext .js --ext .jsx src/", "stylelint": "stylelint src/*{.css,scss}", "stylelint:fix": "stylelint --fix src/*{.css,scss}" }, "devDependencies": { - "@babel/core": "^7.5.4", - "@babel/eslint-parser": "^7.17.0", - "@babel/preset-env": "^7.5.4", - "@babel/preset-react": "^7.0.0", "argparse": "^2.0.1", - "babel-loader": "^8.0.6", "chrome-remote-interface": "^0.32.1", - "compression-webpack-plugin": "^9.0.0", - "copy-webpack-plugin": "^9.0.0", - "css-loader": "^6.5.0", - "css-minimizer-webpack-plugin": "^3.0.1", + "esbuild": "^0.17.15", + "esbuild-plugin-copy": "^2.1.1", + "esbuild-plugin-replace": "^1.3.0", + "esbuild-sass-plugin": "^2.8.0", + "esbuild-wasm": "^0.17.16", "eslint": "^8.13.0", "eslint-config-react-app": "^7.0.0", "eslint-config-standard": "^17.0.0-1", @@ -36,22 +32,15 @@ "eslint-plugin-promise": "^6.0.0", "eslint-plugin-react": "^7.29.4", "eslint-plugin-react-hooks": "^4.4.0", - "eslint-webpack-plugin": "^3.1.1", "htmlparser": "^1.7.7", "jed": "^1.1.1", - "mini-css-extract-plugin": "^2.5.3", "po2json": "^1.0.0-alpha", "qunit": "^2.9.3", - "sass": "^1.35.2", - "sass-loader": "^12.1.0", + "sass": "^1.61.0", "sizzle": "^2.3.3", - "string-replace-loader": "^3.0.0", "stylelint": "^14.9.1", "stylelint-config-standard-scss": "^5.0.0", - "stylelint-webpack-plugin": "^3.3.0", - "terser-webpack-plugin": "^5.1.4", - "webpack": "^5.54.0", - "webpack-cli": "^4.9.1" + "stylelint-formatter-pretty": "^3.2.0" }, "dependencies": { "@patternfly/patternfly": "4.224.4", diff --git a/packaging/cockpit-starter-kit.spec.in b/packaging/cockpit-starter-kit.spec.in index fc6141f..44d6952 100644 --- a/packaging/cockpit-starter-kit.spec.in +++ b/packaging/cockpit-starter-kit.spec.in @@ -48,7 +48,7 @@ appstream-util validate-relax --nonet %{buildroot}/%{_datadir}/metainfo/* %files %doc README.md -%license LICENSE dist/index.js.LICENSE.txt.gz +%license LICENSE dist/index.js.LEGAL.txt dist/index.css.LEGAL.txt %{_datadir}/cockpit/* %{_datadir}/metainfo/* diff --git a/src/index.html b/src/index.html index 3845f06..6eb0369 100644 --- a/src/index.html +++ b/src/index.html @@ -24,9 +24,8 @@ along with this package; If not, see . - - + diff --git a/test/run b/test/run index 5b532b8..32a21df 100755 --- a/test/run +++ b/test/run @@ -7,4 +7,8 @@ set -eu TEST_SCENARIO="${TEST_SCENARIO:-}" [ "${TEST_SCENARIO}" = "${TEST_SCENARIO##firefox}" ] || export TEST_BROWSER=firefox export RUN_TESTS_OPTIONS=--track-naughties + +# linters are off by default for production builds, but we want to run them in CI +export LINT=1 + make check diff --git a/webpack.config.js b/webpack.config.js deleted file mode 100644 index 8a003ae..0000000 --- a/webpack.config.js +++ /dev/null @@ -1,168 +0,0 @@ -import fs from "fs"; - -import copy from "copy-webpack-plugin"; -import extract from "mini-css-extract-plugin"; -import TerserJSPlugin from 'terser-webpack-plugin'; -import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'; -import CompressionPlugin from "compression-webpack-plugin"; -import ESLintPlugin from 'eslint-webpack-plugin'; -import StylelintPlugin from 'stylelint-webpack-plugin'; - -import { CockpitPoWebpackPlugin } from './pkg/lib/cockpit-po-plugin.js'; -import { CockpitRsyncWebpackPlugin } from "./pkg/lib/cockpit-rsync-plugin.js"; - -/* A standard nodejs and webpack pattern */ -const production = process.env.NODE_ENV === 'production'; - -/* development options for faster iteration */ -const eslint = process.env.ESLINT !== '0'; - -/* Default to disable csslint for faster production builds */ -const stylelint = process.env.STYLELINT ? (process.env.STYLELINT !== '0') : !production; - -// Obtain package name from package.json -const packageJson = JSON.parse(fs.readFileSync('package.json')); - -// Non-JS files which are copied verbatim to dist/ -const copy_files = [ - "./src/index.html", - "./src/manifest.json", -]; - -const plugins = [ - new copy({ patterns: copy_files }), - new extract({ filename: "[name].css" }), - new CockpitPoWebpackPlugin(), - new CockpitRsyncWebpackPlugin({ dest: packageJson.name }), -]; - -if (eslint) { - plugins.push(new ESLintPlugin({ extensions: ["js", "jsx"], failOnWarning: true, })); -} - -if (stylelint) { - plugins.push(new StylelintPlugin({ - context: "src/", - })); -} - -/* Only minimize when in production mode */ -if (production) { - plugins.unshift(new CompressionPlugin({ - test: /\.(js|html|css)$/, - deleteOriginalAssets: true - })); -} - -const config = { - mode: production ? 'production' : 'development', - resolve: { - modules: ["node_modules", 'pkg/lib'], - alias: { 'font-awesome': 'font-awesome-sass/assets/stylesheets' }, - }, - resolveLoader: { - modules: ["node_modules", 'pkg/lib'], - }, - watchOptions: { - ignored: /node_modules/, - }, - entry: { - index: "./src/index.js", - }, - // cockpit.js gets included via