Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions packages/build/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ To install using npm
npm install @labkey/build
```

## Webpack
## Build configs

The `webpack` directory has configs that can be used to build LabKey client-side React pages.
See the [README](./webpack/README.md) in that directory for further information.
The `webpack` directory has [Rspack](https://rspack.rs) configs that can be used to build LabKey client-side React
pages. (The directory retains the `webpack/` name so consuming modules' `--config` paths did not have to change when
we migrated from webpack to Rspack.) See the [README](./webpack/README.md) in that directory for further information,
including the one required change consuming modules must make to their `package.json` build scripts.

## Release Notes
Release notes for this package are available [here](./releaseNotes/build.md).
7,946 changes: 1,265 additions & 6,681 deletions packages/build/package-lock.json

Large diffs are not rendered by default.

24 changes: 8 additions & 16 deletions packages/build/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@labkey/build",
"version": "9.1.4",
"version": "10.0.1-fb-rspack-build.1",
"description": "LabKey client-side build assets",
"files": [
"webpack/"
Expand All @@ -12,33 +12,25 @@
"author": "LabKey",
"license": "SEE LICENSE IN LICENSE.txt",
"dependencies": {
"@babel/core": "~7.29.0",
"@babel/plugin-transform-class-properties": "~7.28.6",
"@babel/plugin-transform-object-rest-spread": "~7.28.6",
"@babel/preset-env": "~7.29.5",
"@babel/preset-react": "~7.28.5",
"@babel/preset-typescript": "~7.28.5",
"@pmmmwh/react-refresh-webpack-plugin": "~0.6.2",
"@rspack/cli": "~2.0.8",
"@rspack/core": "~2.0.8",
"@rspack/dev-server": "~2.0.3",
"@rspack/plugin-react-refresh": "~2.0.2",
"ajv": "~8.18.0",
"babel-loader": "~10.1.1",
"bootstrap-sass": "~3.4.3",
"copy-webpack-plugin": "~14.0.0",
"cross-env": "~10.1.0",
"css-loader": "~7.1.4",
"fork-ts-checker-webpack-plugin": "~9.1.0",
"html-webpack-plugin": "~5.6.7",
"mini-css-extract-plugin": "~2.10.1",
"react-refresh": "~0.18.0",
"resolve-url-loader": "~5.0.0",
"rimraf": "~6.1.3",
"rspack-merge": "~1.0.1",
"sass": "~1.99.0",
"sass-loader": "~16.0.8",
"source-map-loader": "~5.0.0",
"style-loader": "~4.0.0",
"ts-checker-rspack-plugin": "~1.4.0",
"typescript": "~5.9.3",
"webpack": "~5.106.2",
"webpack-bundle-analyzer": "~5.3.0",
"webpack-cli": "~7.0.2",
"webpack-dev-server": "~5.2.4"
"webpack-bundle-analyzer": "~5.3.0"
}
}
22 changes: 22 additions & 0 deletions packages/build/releaseNotes/build.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
# @labkey/build

### version 10.0.0
*Released*: not yet released
- Migrate the build tooling from webpack to [Rspack](https://rspack.rs). Rspack is webpack-config-compatible but much
faster, and its dev-server error overlay shows the file name, line/column, and a code frame for both compilation and
TypeScript errors (the old webpack-dev-server overlay did not).
- **Breaking change for consuming modules:** the `webpack` binary is no longer provided; the `rspack` binary is. Every
consuming module must update its `package.json` build scripts to invoke `rspack` instead of `webpack`
- `webpack` → `rspack build`
- `webpack serve` → `rspack serve`
- See the migration table in `webpack/README.md`.
- Replaced `babel-loader` and the hand-ordered Babel plugins with Rspack's `builtin:swc-loader`. SWC's
`useDefineForClassFields: false` plus its native elision of `declare` class fields reproduces the Immutable `Record`
behavior that previously required `@babel/plugin-transform-typescript`'s `allowDeclareFields`.
- Replaced `fork-ts-checker-webpack-plugin` with `ts-checker-rspack-plugin`
- Replaced `mini-css-extract-plugin` with `rspack.CssExtractRspackPlugin`
- Replaced `copy-webpack-plugin` with `rspack.CopyRspackPlugin`
- Replaced `terser-webpack-plugin` with `rspack.SwcJsMinimizerRspackPlugin` (preserving the
`compress.collapse_vars: false` workaround)
- Replaced `@pmmmwh/react-refresh-webpack-plugin` with `@rspack/plugin-react-refresh`
- Kept the community `html-webpack-plugin` (Rspack-compatible) to continue generating the custom `.view.xml` /
`.lib.xml` / `.html` outputs and their LabKey-specific template parameters.

### version 9.0.0
*Released*: 9 March 2026
- Update TypeScript compiler `lib` and `target` options to `"ES2023"'
Expand Down
35 changes: 32 additions & 3 deletions packages/build/webpack/README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,39 @@
# LabKey Module React Page Development

This directory contains the shared [webpack] configurations to develop and build
This directory contains the shared [Rspack] configurations to develop and build
[React] pages within a LabKey module. These files include configurations for
building both production and development mode LabKey React Pages in a standard way.

> **Note:** This package previously used [webpack]. It now uses [Rspack], which is webpack-config-compatible but
> significantly faster and ships a dev-server error overlay that shows the offending file name, line/column, and a
> code frame for both compilation and TypeScript errors. The directory is still named `webpack/` so that the
> `--config node_modules/@labkey/build/webpack/*.config.js` paths in consuming modules did not have to change.

## Migrating a consuming module from webpack to Rspack

`@labkey/build` provides the build CLI transitively, so when this package switched its dependencies from `webpack`
to `@rspack/*`, the `webpack` binary is no longer installed in consuming modules — the `rspack` binary is. **Every
consuming module must update the build scripts in its `package.json`** to invoke `rspack` instead of `webpack`:

| Old (webpack) | New (Rspack) |
|---|---|
| `webpack --config node_modules/@labkey/build/webpack/dev.config.js` | `rspack build --config node_modules/@labkey/build/webpack/dev.config.js` |
| `webpack --config node_modules/@labkey/build/webpack/prod.config.js` | `rspack build --config node_modules/@labkey/build/webpack/prod.config.js` |
| `webpack --config package.config.js` (library packages) | `rspack build --config package.config.js` |
| `webpack serve --config node_modules/@labkey/build/webpack/watch.config.js` | `rspack serve --config node_modules/@labkey/build/webpack/watch.config.js` |

Notes:
- Bare `webpack` means "build", but bare `rspack` does not — use `rspack build` (the `build` subcommand is required).
- `webpack serve` becomes `rspack serve` (also aliased as `rspack dev`).
- The `--config`, `--color`, `--progress`, and `--profile` flags and the `NODE_ENV` / `PROD_SOURCE_MAP` environment
variables are unchanged.
- `webpack-merge` usages should be replaced with `rspack-merge`, it is a drop-in replacement so you should only need to
change the import path. and remove webpack-merge from your `package.json` devDependencies.
- If a module declares `webpack`, `webpack-cli`, or `webpack-dev-server` directly in its own `devDependencies` (most
do not — they rely on `@labkey/build` to provide them transitively), remove those entries.

Note that if build customizations are needed for a given module, the module can opt out of these shared
configurations by setting up its own webpack config files and pointing the build scripts in the
configurations by setting up its own Rspack config files and pointing the build scripts in the
module's `package.json` file at them.

### How to use the shared webpack config files
Expand All @@ -15,7 +43,7 @@ module's `package.json` file at them.
`node_modules/@labkey/build/webpack`. See examples from the [experiment] module.
1. use one of the three configuration files based on your script target: `prod.config.js`, `dev.config.js`,
`package.config.js`, or `watch.config.js`
1. make sure to pass the following environment variables as part of your webpack command:
1. make sure to pass the following environment variables as part of your rspack command:
1. `NODE_ENV` - development or production
2. `PROD_SOURCE_MAP` - optional source map setting for the production webpack config to use,
defaults to `nosources-source-map`
Expand Down Expand Up @@ -137,6 +165,7 @@ build before publishing a new `@labkey/build` version, you can do one of the fol
`node_modules/@labkey/build/webpack` directory

[React]: https://reactjs.org
[Rspack]: https://rspack.rs/
[webpack]: https://webpack.js.org/
[LabKey Gradle build]: https://www.labkey.org/Documentation/wiki-page.view?name=gradleBuild
[assay]: https://github.com/LabKey/platform/tree/develop/assay
Expand Down
106 changes: 57 additions & 49 deletions packages/build/webpack/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
*/
const fs = require('fs');
const path = require('path');
const { rspack } = require('@rspack/core');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const { TsCheckerRspackPlugin } = require('ts-checker-rspack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

const cwd = path.resolve('./').split(path.sep);
Expand Down Expand Up @@ -94,50 +94,52 @@ const SASS_PLUGINS = [
}
];

const BABEL_PLUGINS = [
// These make up @babel/preset-react, we cannot use preset-react because we need to ensure that the
// typescript plugins run before the class properties plugins in order for allowDeclareFields to work
// properly. We can use preset-react and stop using allowDeclareFields if we stop using Immutable.
'@babel/plugin-syntax-jsx',
'@babel/plugin-transform-react-jsx',
'@babel/plugin-transform-react-display-name',
// We previously used babel-loader with a hand-ordered set of plugins so that the TypeScript transform ran before the
// class-properties transform, which is what made @babel/plugin-transform-typescript's `allowDeclareFields` behave
// correctly with Immutable. Rspack uses SWC (builtin:swc-loader) instead, which is where the major speed gain comes
// from. SWC reproduces the relevant behavior:
// - TypeScript + TSX parsing handles both .ts/.tsx and .jsx (TS syntax is a superset).
// - `transform.useDefineForClassFields: false` makes SWC assign class fields directly (the loose / "set" semantics
// Babel produced) rather than emitting Object.defineProperty. Combined with SWC always ELIDING `declare` fields,
// this matches Babel's allowDeclareFields behavior: declared-only fields produce no output and therefore do not
// clobber Immutable Record defaults.
// - `transform.react.runtime: 'classic'` matches our @babel/plugin-transform-react-jsx (classic React.createElement)
// setup. In dev, `development` and `refresh` enable the React Fast Refresh transform (replacing
// react-refresh/babel + @pmmmwh/react-refresh-webpack-plugin).
// - `env.targets` carries over the browser targets we passed to @babel/preset-env (drives object-rest-spread,
// async/await, etc. down-leveling). Note: do NOT also set jsc.target — SWC errors if both are present.
// NOTE: SWC's classic JSX transform does not replicate @babel/plugin-transform-react-display-name (auto-assigning
// component displayName). We set displayName explicitly in source (see CLAUDE.md React conventions), so this is a
// no-op in practice, but it is a behavioral difference from the old Babel pipeline.
const SWC_TARGETS = 'last 2 versions, not dead, not IE 11, > 5%';

// These make up @babel/preset-typescript
['@babel/plugin-transform-typescript', {
allExtensions: true, // required when using isTSX
allowDeclareFields: true,
isTSX: true,
}],

'@babel/plugin-transform-class-properties',
'@babel/plugin-transform-object-rest-spread',
];

const BABEL_CONFIG = {
loader: 'babel-loader',
const makeSwcConfig = (isDev) => ({
loader: 'builtin:swc-loader',
options: {
babelrc: false,
cacheDirectory: true,
presets: [
[
'@babel/preset-env',
{
// support async/await
'targets': 'last 2 versions, not dead, not IE 11, > 5%',
}
],
],
plugins: BABEL_PLUGINS,
}
};
jsc: {
parser: {
syntax: 'typescript',
tsx: true,
},
transform: {
react: {
runtime: 'classic',
development: isDev,
refresh: isDev,
},
// Match Babel's loose class-fields behavior so `declare` fields are elided rather than emitted as
// `field = undefined`, which would clobber Immutable Record defaults.
useDefineForClassFields: false,
},
},
env: {
targets: SWC_TARGETS,
},
},
});

const BABEL_DEV_CONFIG = {
...BABEL_CONFIG,
options: {
...BABEL_CONFIG.options,
plugins: [require.resolve('react-refresh/babel')].concat(BABEL_PLUGINS),
}
};
const SWC_CONFIG = makeSwcConfig(false);
const SWC_DEV_CONFIG = makeSwcConfig(true);

const TS_CHECKER_CONFIG = {
typescript: {
Expand Down Expand Up @@ -257,33 +259,39 @@ module.exports = {
STYLE: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader']
// `type: 'javascript/auto'` opts these rules out of Rspack's native CSS handling so that
// CssExtractRspackPlugin (the mini-css-extract-plugin replacement) processes them instead.
type: 'javascript/auto',
use: [rspack.CssExtractRspackPlugin.loader, 'css-loader']
},
{
test: /\.s[ac]ss$/i,
use: [MiniCssExtractPlugin.loader].concat(SASS_PLUGINS),
type: 'javascript/auto',
use: [rspack.CssExtractRspackPlugin.loader].concat(SASS_PLUGINS),
},
],
STYLE_DEV: [
{
test: /\.css$/,
type: 'javascript/auto',
use: ['style-loader', 'css-loader']
},
{
test: /\.s[ac]ss$/i,
type: 'javascript/auto',
use: ['style-loader'].concat(SASS_PLUGINS),
},
],
TYPESCRIPT: [
{
test: /\.(jsx|ts|tsx)(?!.*\.(spec|test)\.(jsx?|tsx?))$/,
use: [BABEL_CONFIG]
use: [SWC_CONFIG]
}
],
TYPESCRIPT_WATCH: [
{
test: /\.(jsx|ts|tsx)(?!.*\.(spec|test)\.(jsx?|tsx?))$/,
use: [BABEL_DEV_CONFIG]
use: [SWC_DEV_CONFIG]
}
]
},
Expand Down Expand Up @@ -381,11 +389,11 @@ module.exports = {
return plugins;
}, []);

allPlugins.push(new MiniCssExtractPlugin({
allPlugins.push(new rspack.CssExtractRspackPlugin({
filename: '[name].[contenthash].css',
}));

allPlugins.push(new ForkTsCheckerWebpackPlugin(TS_CHECKER_CONFIG));
allPlugins.push(new TsCheckerRspackPlugin(TS_CHECKER_CONFIG));

if (process.env.ANALYZE) {
allPlugins.push(new BundleAnalyzerPlugin());
Expand Down
8 changes: 4 additions & 4 deletions packages/build/webpack/package.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
* in any form or by any electronic or mechanical means without written permission from LabKey Corporation.
*/
const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const { rspack } = require('@rspack/core');
const constants = require('./constants');
const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin");
const { TsCheckerRspackPlugin } = require('ts-checker-rspack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

const tsCheckerConfig = {
Expand All @@ -23,8 +23,8 @@ const tsCheckerConfig = {
};

const plugins = [
new ForkTsCheckerWebpackPlugin(tsCheckerConfig),
new CopyWebpackPlugin({
new TsCheckerRspackPlugin(tsCheckerConfig),
new rspack.CopyRspackPlugin({
patterns: [
{
// copy theme scss files into the dist dir to be used by LabKey module apps
Expand Down
15 changes: 9 additions & 6 deletions packages/build/webpack/prod.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/
const entryPoints = require('../../../../src/client/entryPoints');
const constants = require('./constants');
const TerserPlugin = require('terser-webpack-plugin');
const { rspack } = require('@rspack/core');

module.exports = {
context: constants.context,
Expand All @@ -30,11 +30,14 @@ module.exports = {
optimization: {
minimize: true,
minimizer: [
// Use the defacto Webpack Terser plugin which comes distributed with webpack.
// See https://webpack.js.org/plugins/terser-webpack-plugin
new TerserPlugin({
terserOptions: {
// For other "compress" options see https://github.com/terser/terser#compress-options
// Use Rspack's built-in SWC-based JS minimizer (replaces terser-webpack-plugin). SWC's compress options
// mirror Terser's, so the existing workaround carries over verbatim. Specifying only the JS minimizer
// here (rather than relying on Rspack's defaults, which also include a CSS minimizer) matches the prior
// webpack behavior, where only JS was minified.
// See https://rspack.rs/plugins/rspack/swc-js-minimizer-rspack-plugin
new rspack.SwcJsMinimizerRspackPlugin({
minimizerOptions: {
// For other "compress" options see https://swc.rs/docs/configuration/minification
compress: {
// Disable "Collapse single-use non-constant variables, side effects permitting."
// There are some cases where this optimization fails to recognize a side effect
Expand Down
12 changes: 7 additions & 5 deletions packages/build/webpack/watch.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
* Copyright (c) 2020-2026 LabKey Corporation. All rights reserved. No portion of this work may be reproduced
* in any form or by any electronic or mechanical means without written permission from LabKey Corporation.
*/
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const { TsCheckerRspackPlugin } = require('ts-checker-rspack-plugin');
const { ReactRefreshRspackPlugin } = require('@rspack/plugin-react-refresh');
const constants = require('./constants');
const path = require('path');
// relative to the <lk_module>/node_modules/@labkey/build/webpack dir
Expand Down Expand Up @@ -68,8 +68,10 @@ module.exports = {
emitOnErrors: false,
},
plugins: [
new ReactRefreshWebpackPlugin(),
// This Plugin type checks our TS code, @babel/preset-typescript does not type check, it only transforms
new ForkTsCheckerWebpackPlugin(constants.TS_CHECKER_DEV_CONFIG)
// Rspack's React Fast Refresh integration. The matching builtin:swc-loader transform (react.refresh: true)
// is enabled in constants.SWC_DEV_CONFIG (used by loaders.TYPESCRIPT_WATCH above).
new ReactRefreshRspackPlugin(),
// This Plugin type checks our TS code; builtin:swc-loader does not type check, it only transforms.
new TsCheckerRspackPlugin(constants.TS_CHECKER_DEV_CONFIG)
],
};
Loading