Skip to content
Open
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
71 changes: 38 additions & 33 deletions .webpack/webpack.base.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@ const dotenv = require('dotenv');
const path = require('path');
const fs = require('fs');

const webpack = require('webpack');
const webpack = require('@rspack/core');

// ~~ PLUGINS
// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const TerserJSPlugin = require('terser-webpack-plugin');

// ~~ PackageJSON
// const vtkRules = require('vtk.js/Utilities/config/dependency.js').webpack.core
Expand All @@ -18,9 +17,15 @@ const TerserJSPlugin = require('terser-webpack-plugin');
const loadWebWorkersRule = require('./rules/loadWebWorkers.js');
const transpileJavaScriptRule = require('./rules/transpileJavaScript.js');
const cssToJavaScript = require('./rules/cssToJavaScript.js');
// Module-resolution rules shared with the rsbuild build (see rsbuild.config.ts).
const resolveConfig = require('./resolveConfig');
// Only uncomment for old v2 stylus
// const stylusToJavaScript = require('./rules/stylusToJavaScript.js');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
let ReactRefreshWebpackPlugin;
try {
const mod = require('@rspack/plugin-react-refresh');
ReactRefreshWebpackPlugin = mod.ReactRefreshRspackPlugin || mod.default || mod;
} catch { ReactRefreshWebpackPlugin = null; }

// ~~ ENV VARS
const NODE_ENV = process.env.NODE_ENV;
Expand Down Expand Up @@ -66,6 +71,12 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
const config = {
mode: isProdBuild ? 'production' : 'development',
devtool: isProdBuild ? 'source-map' : 'cheap-module-source-map',
// `rspack serve` (@rspack/cli) auto-enables lazyCompilation for web-only
// apps unless the config defines it explicitly. The on-demand proxy chunks
// it produces fail to load in the headless cypress/electron e2e run
// (ChunkLoadError on cornerstone vendor chunks), so disable it here to match
// the rsbuild build (see rsbuild.config.ts `dev.lazyCompilation: false`).
lazyCompilation: false,
entry: ENTRY,
optimization: {
// splitChunks: {
Expand All @@ -92,9 +103,7 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
children: false,
warnings: true,
},
cache: {
type: 'filesystem',
},
cache: isProdBuild ? false : { type: 'filesystem' },
module: {
noParse: [/(dicomicc)/],
rules: [
Expand Down Expand Up @@ -168,9 +177,6 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
},
},
cssToJavaScript,
// Note: Only uncomment the following if you are using the old style of stylus in v2
// Also you need to uncomment this platform/app/.webpack/rules/extractStyleChunks.js
// stylusToJavaScript,
{
test: /\.wasm/,
type: 'asset/resource',
Expand All @@ -194,27 +200,16 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
},
resolve: {
mainFields: ['module', 'browser', 'main'],
// alias and modules are shared with the rsbuild build via ./resolveConfig
// so the two pipelines resolve identically.
alias: {
// Viewer project
'@': path.resolve(__dirname, '../platform/app/src'),
'@components': path.resolve(__dirname, '../platform/app/src/components'),
'@hooks': path.resolve(__dirname, '../platform/app/src/hooks'),
'@routes': path.resolve(__dirname, '../platform/app/src/routes'),
'@state': path.resolve(__dirname, '../platform/app/src/state'),
...resolveConfig.alias,
},
// Which directories to search when resolving modules
modules: [
// Modules specific to this package
path.resolve(__dirname, '../node_modules'),
// Hoisted Yarn Workspace Modules
path.resolve(__dirname, '../../../node_modules'),
path.resolve(__dirname, '../platform/app/node_modules'),
path.resolve(__dirname, '../platform/ui/node_modules'),
SRC_DIR,
],
modules: resolveConfig.getModules(SRC_DIR),
// Attempt to resolve these extensions in order.
extensions: ['.js', '.jsx', '.json', '.ts', '.tsx', '*'],
// symlinked resources are resolved to their real path, not their symlinked location
// Workspace packages use relative imports between sibling packages.
// Resolve symlinks to keep those imports anchored at the real package paths.
symlinks: true,
fallback: {
fs: false,
Expand All @@ -223,24 +218,34 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
buffer: require.resolve('buffer'),
},
},
node: {
// Leave __filename / __dirname references alone. The previous 'mock'
// value triggers an rspack warning whenever bundled deps reference
// __dirname (e.g. Emscripten-compiled cornerstone codecs). Those refs
// sit inside `if (ENVIRONMENT_IS_NODE)` branches that never execute in
// the browser, so leaving them un-substituted is harmless at runtime.
__filename: false,
__dirname: false,
},
plugins: [
new webpack.DefinePlugin(defineValues),
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
}),
...(isProdBuild ? [] : [new ReactRefreshWebpackPlugin({ overlay: false })]),
new webpack.IgnorePlugin({
resourceRegExp: /^(fs|path)$/,
contextRegExp: /@cornerstonejs[\\/]codec-/,
}),
...(isProdBuild || IS_COVERAGE || !ReactRefreshWebpackPlugin
? []
: [new ReactRefreshWebpackPlugin({ overlay: false })]),
// Uncomment to generate bundle analyzer
// new BundleAnalyzerPlugin(),
],
};

if (isProdBuild) {
config.optimization.minimizer = [
new TerserJSPlugin({
parallel: true,
terserOptions: {},
}),
];
config.optimization.minimizer = [new webpack.SwcJsMinimizerRspackPlugin()];
}

if (isQuickBuild) {
Expand Down
118 changes: 84 additions & 34 deletions platform/app/.webpack/webpack.pwa.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
// https://developers.google.com/web/tools/workbox/guides/codelabs/webpack
// ~~ WebPack
const path = require('path');
const fs = require('fs');
const { merge } = require('webpack-merge');
const webpack = require('webpack');
const rspack = require('@rspack/core');
const webpackBase = require('./../../../.webpack/webpack.base.js');
// ~~ Plugins
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { InjectManifest } = require('workbox-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
// ~~ Directories
const SRC_DIR = path.join(__dirname, '../src');
const DIST_DIR = path.join(__dirname, '../dist');
const PUBLIC_DIR = path.join(__dirname, '../public');

// Ignore node_modules except @cornerstonejs (symlinked local development).
const WATCH_IGNORED = /node_modules[\\/](?!@cornerstonejs(?:[\\/]|$))/;
// ~~ Env Vars
const HTML_TEMPLATE = process.env.HTML_TEMPLATE || 'index.html';
const PUBLIC_URL = process.env.PUBLIC_URL || '/';
Expand All @@ -28,20 +26,70 @@ const IS_COVERAGE = process.env.COVERAGE === 'true';

const OHIF_PORT = Number(process.env.OHIF_PORT || 3000);
const ENTRY_TARGET = process.env.ENTRY_TARGET || `${SRC_DIR}/index.js`;
const Dotenv = require('dotenv-webpack');
const dotenv = require('dotenv');
dotenv.config();
const writePluginImportFile = require('./writePluginImportsFile.js');
// const MillionLint = require('@million/lint');
const open = process.env.OHIF_OPEN !== 'false';

const copyPluginFromExtensions = writePluginImportFile(SRC_DIR, DIST_DIR);

class InjectServiceWorkerManifestPlugin {
constructor({ swSrc, swDest, publicPath, exclude, maximumFileSizeToCacheInBytes }) {
this.swSrc = swSrc;
this.swDest = swDest;
this.publicPath = publicPath;
this.exclude = exclude;
this.maximumFileSizeToCacheInBytes = maximumFileSizeToCacheInBytes;
}

apply(compiler) {
const pluginName = 'InjectServiceWorkerManifestPlugin';
const publicPath = this.publicPath.endsWith('/') ? this.publicPath : `${this.publicPath}/`;

compiler.hooks.thisCompilation.tap(pluginName, compilation => {
compilation.hooks.processAssets.tap(
{
name: pluginName,
stage: rspack.Compilation.PROCESS_ASSETS_STAGE_REPORT,
},
() => {
const manifest = compilation
.getAssets()
.filter(asset => {
if (asset.name === this.swDest || asset.name.endsWith('.map')) {
return false;
}
if (this.exclude.some(pattern => pattern.test(asset.name))) {
return false;
}
return asset.source.size() <= this.maximumFileSizeToCacheInBytes;
})
.map(asset => ({
url: `${publicPath}${asset.name}`,
revision: asset.info.contenthash ? null : compilation.hash,
}));

const source = fs
.readFileSync(this.swSrc, 'utf8')
.replace('self.__WB_MANIFEST', JSON.stringify(manifest));

compilation.emitAsset(this.swDest, new rspack.sources.RawSource(source));
}
);
});
}
}

const setHeaders = (res, path) => {
if (path.indexOf('.gz') !== -1) {
res.setHeader('Content-Encoding', 'gzip');
} else if (path.indexOf('.br') !== -1) {
res.setHeader('Content-Encoding', 'br');
}
if (path.indexOf('.pdf') !== -1) {
if (path.indexOf('thumbnail') !== -1) {
res.setHeader('Content-Type', 'image/jpeg');
} else if (path.indexOf('.pdf') !== -1) {
res.setHeader('Content-Type', 'application/pdf');
} else if (path.indexOf('mp4') !== -1) {
res.setHeader('Content-Type', 'video/mp4');
Expand All @@ -62,6 +110,7 @@ module.exports = (env, argv) => {
app: ENTRY_TARGET,
},
output: {
clean: true,
path: DIST_DIR,
filename: isProdBuild ? '[name].bundle.[chunkhash].js' : '[name].js',
publicPath: PUBLIC_URL, // Used by HtmlWebPackPlugin for asset prefix
Expand All @@ -74,69 +123,63 @@ module.exports = (env, argv) => {
},
},
resolve: {
// Resolve every extension/mode declared in pluginConfig.json to its
// workspace source, so the dynamic import()s in pluginImports.js link
// without the plugins being dependencies of platform/app. Merged with the
// base aliases (webpack-merge deep-merges resolve.alias).
alias: writePluginImportFile.getPluginResolveAliases(),
modules: [
// Modules specific to this package
// Preserve importer-relative node_modules walk-up for pnpm.
'node_modules',
path.resolve(__dirname, '../node_modules'),
// Hoisted Yarn Workspace Modules
path.resolve(__dirname, '../../../node_modules'),
SRC_DIR,
],
},
plugins: [
// For debugging re-renders
// MillionLint.webpack(),
new Dotenv(),
// Clean output.path
new CleanWebpackPlugin(),
// Copy "Public" Folder to Dist
new CopyWebpackPlugin({
// Copy "Public" Folder to Dist (rspack built-in)
new rspack.CopyRspackPlugin({
patterns: [
...copyPluginFromExtensions,
{
from: PUBLIC_DIR,
to: DIST_DIR,
toType: 'dir',
globOptions: {
// Ignore our HtmlWebpackPlugin template file
// Ignore our configuration files
ignore: ['**/config/**', '**/html-templates/**', '.DS_Store'],
},
},
{
from: '../../../node_modules/onnxruntime-web/dist',
to: `${DIST_DIR}/ort`,
},
// Short term solution to make sure GCloud config is available in output
// for our docker implementation
{
from: `${PUBLIC_DIR}/config/google.js`,
to: `${DIST_DIR}/google.js`,
},
// Copy over and rename our target app config file
{
from: `${PUBLIC_DIR}/${APP_CONFIG}`,
to: `${DIST_DIR}/app-config.js`,
},
],
}),
// Generate "index.html" w/ correct includes/imports
new HtmlWebpackPlugin({
// Generate "index.html" w/ correct includes/imports (rspack built-in)
new rspack.HtmlRspackPlugin({
template: `${PUBLIC_DIR}/html-templates/${HTML_TEMPLATE}`,
filename: 'index.html',
templateParameters: {
PUBLIC_URL: PUBLIC_URL,
},
}),
// Generate a service worker for fast local loads
// Generate a service worker for fast local loads.
...(IS_COVERAGE
? []
: [
new InjectManifest({
new InjectServiceWorkerManifestPlugin({
swDest: 'sw.js',
swSrc: path.join(SRC_DIR, 'service-worker.js'),
// Need to exclude the theme as it is updated independently
publicPath: PUBLIC_URL,
exclude: [/theme/],
// Cache large files for the manifests to avoid warning messages
maximumFileSizeToCacheInBytes: 1024 * 1024 * 50,
}),
]),
Expand All @@ -151,11 +194,15 @@ module.exports = (env, argv) => {
open,
port: OHIF_PORT,
client: {
overlay: { errors: true, warnings: false },
// During e2e (COVERAGE=true) disable the dev-server overlay: its
// injected iframe intercepts pointer events and breaks Playwright/Cypress
// clicks. Keep it for normal local dev.
overlay: IS_COVERAGE ? false : { errors: true, warnings: false },
},
proxy: [
{
'/dicomweb': 'http://localhost:5000',
context: ['/dicomweb'],
target: 'http://localhost:5000',
},
],
static: [
Expand All @@ -168,13 +215,15 @@ module.exports = (env, argv) => {
setHeaders,
},
publicPath: '/viewer-testdata',
watch: false,
},
],
//public: 'http://localhost:' + 3000,
//writeToDisk: true,
historyApiFallback: {
disableDotRule: true,
disableDotRule: !IS_COVERAGE,
index: PUBLIC_URL + 'index.html',
htmlAcceptHeaders: ['text/html'],
},
devMiddleware: {
writeToDisk: true,
Expand All @@ -198,15 +247,16 @@ module.exports = (env, argv) => {

if (isProdBuild) {
mergedConfig.plugins.push(
new MiniCssExtractPlugin({
new rspack.CssExtractRspackPlugin({
filename: '[name].bundle.css',
chunkFilename: '[id].css',
})
);
}

mergedConfig.watchOptions = {
ignored: /node_modules\/@cornerstonejs/,
ignored: WATCH_IGNORED,
followSymlinks: true,
};

return mergedConfig;
Expand Down
Loading