Webpack Configuration (Alternative Approach)
Configure Webpack as an alternative to Vite for React-powered Shopify themes. Output directly to shopify-theme/assets/ with the same simple structure.
While this course primarily uses Vite, Webpack remains a solid choice—especially if you’re extending an existing Dawn theme or your team already has Webpack expertise. This lesson covers how to configure Webpack to output directly to shopify-theme/assets/.
When to Choose Webpack
Consider Webpack over Vite when:
- Extending Dawn: Dawn uses Webpack, and staying consistent simplifies things
- Team familiarity: Your team knows Webpack’s configuration style
- Specific plugins: You need Webpack-only plugins for your workflow
- Legacy support: You need broader browser compatibility
Step 1: Install Dependencies
# Core Webpack packagesnpm install -D webpack webpack-cli
# React and TypeScript loadersnpm install -D babel-loader @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript
# TypeScriptnpm install -D typescript @types/react @types/react-dom
# CSS handlingnpm install -D css-loader style-loader mini-css-extract-plugin
# Production optimizationnpm install -D terser-webpack-plugin css-minimizer-webpack-plugin
# Reactnpm install react react-domStep 2: Create Webpack Configuration
Create webpack.config.js in your project root:
const path = require('path');const MiniCssExtractPlugin = require('mini-css-extract-plugin');const TerserPlugin = require('terser-webpack-plugin');const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const isDev = process.env.NODE_ENV !== 'production';
/* * Webpack config for Shopify themes * Same output structure as Vite - direct to shopify-theme/assets/ */module.exports = { // 'development' enables helpful error messages, 'production' enables minification mode: isDev ? 'development' : 'production',
// Single entry point - Webpack bundles all imports from here entry: './src/main.tsx',
output: { filename: 'react-bundle.js', // Output directly to theme - no intermediate dist/ folder path: path.resolve(__dirname, 'shopify-theme/assets'), // CRITICAL: Don't delete other theme assets (images, fonts, etc.) clean: false, },
// Source maps: 'eval-source-map' is fast for dev, 'source-map' is accurate for production devtool: isDev ? 'eval-source-map' : 'source-map',
module: { rules: [ // TypeScript and React - Babel handles both transpilation and JSX { test: /\.(ts|tsx)$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: [ ['@babel/preset-env', { targets: 'defaults' }], // JS compatibility ['@babel/preset-react', { runtime: 'automatic' }], // JSX transform (no React import needed) '@babel/preset-typescript', // TypeScript → JavaScript ], }, }, },
// CSS handling { test: /\.css$/, use: [ // Dev: inject styles into DOM for fast refresh // Prod: extract to separate .css file for caching isDev ? 'style-loader' : MiniCssExtractPlugin.loader, { loader: 'css-loader', options: { modules: { // Only treat .module.css files as CSS Modules auto: true, // Dev: readable class names for debugging // Prod: short hashes for smaller files localIdentName: isDev ? '[name]__[local]--[hash:base64:5]' : '[hash:base64:8]', }, }, }, ], },
// Images: inline small ones as base64, emit large ones as files { test: /\.(png|jpg|jpeg|gif|svg)$/, type: 'asset', parser: { dataUrlCondition: { maxSize: 4 * 1024, // Images under 4KB are inlined }, }, generator: { filename: 'react-images/[name][ext]', }, }, ], },
resolve: { // Allow importing without extensions: import Button from './Button' extensions: ['.tsx', '.ts', '.js', '.jsx'], // Path aliases - must match tsconfig.json paths alias: { '@': path.resolve(__dirname, 'src'), '@components': path.resolve(__dirname, 'src/components'), '@hooks': path.resolve(__dirname, 'src/hooks'), '@stores': path.resolve(__dirname, 'src/stores'), '@utils': path.resolve(__dirname, 'src/utils'), '@types': path.resolve(__dirname, 'src/types'), }, },
plugins: [ // Extract CSS to separate file (not inlined in JS) // Better for caching - CSS can be cached separately from JS new MiniCssExtractPlugin({ filename: 'react-bundle.css', }), ],
optimization: { minimizer: [ // Minify JavaScript new TerserPlugin({ terserOptions: { compress: { drop_console: !isDev, // Remove console.log in production }, }, }), // Minify CSS new CssMinimizerPlugin(), ], },
// Cleaner terminal output stats: { colors: true, modules: false, // Hide module-by-module details children: false, // Hide child compiler output },
watchOptions: { ignored: /node_modules/, // Don't watch node_modules (performance) aggregateTimeout: 300, // Wait 300ms after changes before rebuilding (debounce) },};Key settings:
output.path: 'shopify-theme/assets'— Direct output to themeoutput.clean: false— Preserves other assets in the folder
Step 3: Configure Babel
Create .babelrc for additional configuration (optional, since we inline it above):
{ "presets": [ ["@babel/preset-env", { "targets": "> 0.5%, last 2 versions, not dead", "useBuiltIns": "usage", "corejs": 3 }], ["@babel/preset-react", { "runtime": "automatic" }], "@babel/preset-typescript" ]}If using the separate .babelrc with useBuiltIns, you’ll need core-js:
npm install core-jsStep 4: TypeScript Configuration
The same tsconfig.json from the Vite setup works here:
{ "compilerOptions": { "target": "ES2020", "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, "baseUrl": ".", "paths": { "@/*": ["src/*"], "@components/*": ["src/components/*"], "@hooks/*": ["src/hooks/*"], "@stores/*": ["src/stores/*"], "@utils/*": ["src/utils/*"], "@types/*": ["src/types/*"] } }, "include": ["src"]}Step 5: Package.json Scripts
{ "scripts": { "dev": "webpack --watch --mode development", "build": "webpack --mode production", "typecheck": "tsc --noEmit" }}Step 6: Run the Build
# Development with watchnpm run dev
# Production buildnpm run buildOutput:
asset react-bundle.js 142 KiB [emitted] (name: main)asset react-bundle.css 892 bytes [emitted] (name: main)webpack 5.89.0 compiled successfully in 1245 msCheck the output:
ls shopify-theme/assets/# react-bundle.js react-bundle.cssFiles go directly to your theme—no intermediate folders or copy steps.
Webpack vs Vite Configuration Comparison
Here’s how the same features are configured in each tool:
| Feature | Vite | Webpack |
|---|---|---|
| Entry point | build.rollupOptions.input | entry |
| Output dir | build.outDir | output.path |
| Preserve assets | emptyOutDir: false | clean: false |
| Path aliases | resolve.alias | resolve.alias |
| CSS Modules | Automatic for .module.css | css-loader options |
| TypeScript | Built-in | babel-loader + presets |
| Minification | Built-in (terser) | TerserPlugin |
| Source maps | build.sourcemap | devtool |
| Watch mode | vite build --watch | webpack --watch |
Advanced Webpack Configuration
Code Splitting (Multiple Bundles)
/* * Code Splitting: Create separate bundles per page type * Reduces initial load since users only download code for their current page */module.exports = { entry: { // Each key becomes a separate bundle main: './src/main.tsx', // Always loaded (header, cart drawer) product: './src/entries/product.tsx', // Only on product pages collection: './src/entries/collection.tsx', // Only on collection pages }, output: { // [name] is replaced with entry key: react-main.js, react-product.js, etc. filename: 'react-[name].js', path: path.resolve(__dirname, 'shopify-theme/assets'), clean: false, }, plugins: [ new MiniCssExtractPlugin({ // CSS is also split per entry point filename: 'react-[name].css', }), ],};Environment Variables
const webpack = require('webpack');
module.exports = { plugins: [ new webpack.DefinePlugin({ 'process.env.DEBUG': JSON.stringify(process.env.DEBUG || 'false'), }), ],};Access in code:
if (process.env.DEBUG === 'true') { console.log('Debug mode');}Bundle Analysis
Install the analyzer:
npm install -D webpack-bundle-analyzerAdd to config:
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
module.exports = { plugins: [ new BundleAnalyzerPlugin({ analyzerMode: 'static', openAnalyzer: false, reportFilename: '../bundle-report.html', }), ],};Run and view the report:
npm run buildopen bundle-report.htmlFaster Rebuilds with Caching
module.exports = { cache: { type: 'filesystem', buildDependencies: { config: [__filename], }, }, watchOptions: { ignored: /node_modules/, },};Troubleshooting Webpack
”Module not found” for path aliases
Ensure TypeScript and Webpack aliases match exactly:
resolve: { alias: { '@': path.resolve(__dirname, 'src'), },}"paths": { "@/*": ["src/*"]}Large bundle size
Check what’s included:
npm install -D webpack-bundle-analyzer# Add plugin and run buildCommon culprits:
- Entire lodash library (use
lodash-esor individual imports) - Unused icon libraries
- Moment.js (use
date-fnsinstead)
Other assets getting deleted
Make sure clean is set to false:
output: { path: path.resolve(__dirname, 'shopify-theme/assets'), clean: false, // IMPORTANT!}Complete File Structure
After this lesson with Webpack:
your-project/├── shopify-theme/│ ├── assets/│ │ ├── react-bundle.js ✓ (generated by Webpack)│ │ └── react-bundle.css ✓ (generated by Webpack)│ └── layout/│ └── theme.liquid ✓├── src/│ ├── styles/│ │ └── main.css ✓│ └── main.tsx ✓├── package.json ✓├── tsconfig.json ✓└── webpack.config.js ✓Key Takeaways
- Same folder structure as Vite—only the config file differs
- Output directly to
shopify-theme/assets/—no copy plugins needed - Use
clean: falseto preserve other theme assets - Babel handles TypeScript and React transpilation
- Your React code is identical regardless of build tool choice
In the next lesson, we’ll integrate Shopify CLI with our build process for the complete development workflow.
Finished this lesson?
Mark it complete to track your progress.
Discussion
Loading comments...