Webpack to Vite Migration
Consolidated guide based on the migrations of prop-wp-theme and rcs-wp-theme. Use as reference for migrating any Terra WordPress project.
Quick reference
Section titled โQuick referenceโNew config files to create
Section titled โNew config files to createโ| File | Replaces | Purpose |
|---|---|---|
vite.config.js | webpack.common.js + webpack.dev.js + webpack.prod.js | Single build config |
config/viteHelper.js | config/index.js | Hash generation + cleanup plugin |
config/sftpConfig.js | Hardcoded creds in gulpfile.js | SFTP credentials per environment |
.env.virtual | โ | Dev server (HMR) environment vars |
.env.local | โ | Local build environment vars |
.env.production | โ | Production build environment vars |
Files to delete
Section titled โFiles to deleteโwebpack.common.js,webpack.dev.js,webpack.prod.jsconfig/index.jsfunctions/project/enqueues/frontend.php,backend.php,define-hash.phpfunctions/project/local/(directory โ file moves up)src/js/entries/(directory โ file moves up)src/scss/entries/(directory โ file renamed)
Key renames
Section titled โKey renamesโ| Before | After |
|---|---|
src/js/entries/Project.js | src/js/Project.js |
src/scss/entries/frontend/pages/_project.scss | src/scss/style.scss |
functions/project/enqueues/define-hash.php | functions/project/hash.php |
functions/project/local/local-variable.php | functions/project/local-variable.php |
functions/project/enqueues/ (frontend + backend) | functions/project/enqueues.php (unified) |
functions/project/extend-sidebar/post-types.php | functions/project/post-types.php |
functions/project/extend-sidebar/taxonomies.php | functions/project/taxonomies.php |
assets/ (root) | public/assets/ |
IS_VIRTUAL_ENV (PHP constant) | IS_VITE_DEVELOPMENT |
enqueue-hash (PHP constant) | hash |
Step by step
Section titled โStep by stepโ1. Dependencies
Section titled โ1. DependenciesโRemove Webpack devDependencies:
npm uninstall @babel/cli @babel/core @babel/plugin-transform-runtime @babel/preset-env babel-loader clean-webpack-plugin colors css-loader cssnano file-loader glsl-shader-loader mini-css-extract-plugin postcss postcss-import postcss-loader postcss-preset-env sass-resources-loader style-loader webpack webpack-bundle-analyzer webpack-cli webpack-dev-server webpack-mergeRemove dependencies no longer needed:
npm uninstall rename-webpack-plugin sass-loader vue-loader webp-loader @terrahq/lazyInstall Vite dependencies:
npm install --save-dev vite vite-plugin-live-reload cross-env minimist ssh2-sftp-clientnpm install --save @vitejs/plugin-vue dotenv blazy2. Create .env files
Section titled โ2. Create .env filesโCreate three .env files in the theme root. Replace {wp-install} and {theme-name} with your project values.
.env.virtual โ dev server with HMR:
VITE_TERRA_VIRTUAL=trueVITE_BASE=./VITE_LOG_LEVEL=infoVITE_WP_PATH=http://localhost/{wp-install}/wp-content/themes/{theme-name}/public.env.local โ local build:
VITE_TERRA_VIRTUAL=falseVITE_BASE=http://localhost/{wp-install}/wp-content/themes/{theme-name}/dist/VITE_LOG_LEVEL=silentVITE_WP_PATH=..env.production โ production build:
VITE_TERRA_VIRTUAL=falseVITE_BASE=/wp-content/themes/{theme-name}/dist/VITE_LOG_LEVEL=silentVITE_WP_PATH=.3. Create vite.config.js
Section titled โ3. Create vite.config.jsโDelete webpack.common.js, webpack.dev.js, webpack.prod.js and create:
import { defineConfig } from 'vite'import liveReload from 'vite-plugin-live-reload'const { resolve } = require('path')import path from 'path';import { promises as fs } from 'fs';import vue from '@vitejs/plugin-vue'import { config } from 'dotenv';import { generateAndUpdateHash, removeFilesPlugin } from './config/viteHelper';
config({ path: resolve(__dirname, `.env.${process.env.NODE_ENV}`) });
console.log(`Launching Wordpress Vite under ${process.env.NODE_ENV}!`);
// Generate hash for build filenames (production + local only)if (process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'local') { var hash = generateAndUpdateHash(3);}
export default defineConfig({ plugins: [ vue(), liveReload(__dirname + '/**/*.php'), process.env.NODE_ENV === 'virtual' ? removeFilesPlugin() : null ],
root: '', base: process.env.VITE_BASE, logLevel: process.env.VITE_LOG_LEVEL,
// Webpack had DefinePlugin to replace process.env. Vite doesn't do this by default. // Add if your source code references process.env: define: { 'process.env': JSON.stringify(process.env.NODE_ENV) },
css: { preprocessorOptions: { scss: { // IMPORTANT: use @import, NOT @use here. // @use has per-file scope and does NOT propagate vars through @import chains. additionalData: ` @import "@scss/paths.scss"; $base-path: "${process.env.VITE_WP_PATH}"; `, // Required so Sass resolves ./node_modules/ imports from project root loadPaths: ['.'], // Dart Sass 2.x deprecates @import โ cosmetic only, suppress warnings silenceDeprecations: ['import'], } } },
build: { outDir: resolve(__dirname, './dist'), emptyOutDir: true, target: 'es2018', rollupOptions: { input: { Project: resolve(__dirname + '/src/js/Project.js'), Appbackend: resolve(__dirname + '/src/js/app-backend.js'), ProjectStyles: resolve(__dirname + '/src/js/ProjectStyles.js'), }, output: { entryFileNames: `[name].${hash}.js`, chunkFileNames: `[name].${hash}.js`, assetFileNames: (assetInfo) => { if (/\.(woff2?|ttf|eot)$/i.test(assetInfo.name)) { return `fonts/[name].[ext]`; } if (/\.(png|jpe?g|gif|svg|webp)$/i.test(assetInfo.name)) { return `assets/[name].[ext]`; } return `[name].${hash}.[ext]`; } }, }, minify: true, write: true, },
server: { cors: true, strictPort: true, port: 9090, https: false, // CRITICAL: without this, url() in CSS (fonts, images) resolves against // the WordPress host instead of Vite, causing 404s in virtual mode. origin: 'http://localhost:9090', hmr: { host: 'localhost' }, },
resolve: { alias: { vue: 'vue/dist/vue.esm-bundler.js', "@scss": path.resolve(__dirname, "./src/scss"), "@js": path.resolve(__dirname, "./src/js"), "@vue": path.resolve(__dirname, "./src/js/vue"), "@vuejs": path.resolve(__dirname, "./src/js/vue"), "@modules": path.resolve(__dirname, "./src/js/modules"), "@utilities": path.resolve(__dirname, "./src/js/utilities"), "@jsHandler": path.resolve(__dirname, "./src/js/handler"), "@services": path.resolve(__dirname, "./src/js/services"), "@dynamicImport": path.resolve(__dirname, "./src/js/modules/dynamic"), "@img": path.resolve(__dirname, "./src/img"), } }})Config notes:
- Aliases: Copy all aliases from
webpack.common.jsresolve.aliasintoresolve.alias. Same syntax. Addnode_modulesalias if SCSS uses absolute imports like./node_modules/@terrahq/gc/.... server.origin: Without this,url()in CSS resolves against WordPress host during dev, not Vite.define: Replaces WebpackโsDefinePlugin. Only needed if source code referencesprocess.env.loadPaths: Required for Sass to resolve./node_modules/from project root instead of relative to each file.@importvs@useinadditionalData: Must use@import.@useis file-scoped and variables donโt propagate through the import chain.silenceDeprecations: Dart Sass 2.x deprecates@importin favor of@use. The warning is cosmetic โ@importstill works and is required inadditionalData.
4. Create config/viteHelper.js
Section titled โ4. Create config/viteHelper.jsโimport path, { resolve } from "path";import { promises as fs } from "fs";import fsSync from "fs";
export function generateRandomHash(length) { let result = ""; const characters = "abcdefghijklmnopqrstuvwxyz0123456789"; for (let i = 0; i < length; i++) { result += characters.charAt(Math.floor(Math.random() * characters.length)); } return result;}
export function updateHash(hash) { const hashFileRoute = resolve(process.cwd(), "functions/project/hash.php"); if (fsSync.existsSync(hashFileRoute)) { const content = fsSync.readFileSync(hashFileRoute, "utf-8"); const updatedContent = content.replace( /define\(\s*['"]hash['"]\s*,\s*['"][a-zA-Z0-9]*['"]\s*\);/, `define('hash', '${hash}');` ); fsSync.writeFileSync(hashFileRoute, updatedContent, "utf-8"); }}
export function generateAndUpdateHash(length = 3) { const hash = generateRandomHash(length); updateHash(hash); return hash;}
export function removeFilesPlugin() { return { name: "remove-files", closeBundle: async () => { const distPath = resolve(__dirname, "dist"); const files = await fs.readdir(distPath); const imageFiles = files.filter((file) => [".jpg", ".png", ".gif", ".webp", ".svg"].some((ext) => file.endsWith(ext)) ); const deleteFilesPromises = imageFiles.map((file) => fs.unlink(resolve(distPath, file)) ); const deleteFoldersPromises = ["assets", "fonts"].map(async (folder) => { const folderPath = resolve(distPath, folder); try { await fs.rm(folderPath, { recursive: true }); } catch (err) { if (err.code !== "ENOENT") throw err; } }); await Promise.all([...deleteFilesPromises, ...deleteFoldersPromises]); }, };}5. Create config/sftpConfig.js
Section titled โ5. Create config/sftpConfig.jsโExtract hardcoded SFTP credentials from gulpfile.js into a separate config:
const devSFTPConfig = { host: "{project}dev.sftp.wpengine.com", user: "{project}dev-{user}", port: "2222", pass: "{password}", remotePath: "/wp-content/themes/{theme-name}"};
const stageSFTPConfig = { host: "{project}stg.sftp.wpengine.com", user: "{project}stg-{user}", port: "2222", pass: "{password}", remotePath: "/wp-content/themes/{theme-name}"};
const prodSFTPConfig = { host: "{project}prd.sftp.wpengine.com", user: "{project}prd-{user}", port: "2222", pass: "{password}", remotePath: "/wp-content/themes/{theme-name}"};
const filesToExclude = [ "!functions/project/hash.php", "!functions/project/local-variable.php", "!config/**/*", "!node_modules/**/*", "!src/**/*", "!.env.production", "!.env.virtual", "!gulpfile.js", "!package-lock.json", "!package.json", "!readme.md", "!vite.config.js", "!documentation/**/*", "!*.zip", "!*.tgz",];
module.exports = { devSFTPConfig, stageSFTPConfig, prodSFTPConfig, filesToExclude };6. Refactor gulpfile.js
Section titled โ6. Refactor gulpfile.jsโMain changes:
- Use
minimistto parse arguments instead of detectingnpm_lifecycle_event - Import configs from
config/sftpConfig.js - Rename tasks:
deploy_phpโdphp,deploy_static_assetsโddist,deploy_hashโddisthash,deploy_fullโdall - Add
ds(deploy single file/folder) andremovetasks - Environment is determined by flags (
--dev,--stage,--production) instead of task name prefix
Vite outputs everything flat in dist/ (no dist/js/ and dist/css/ subdirectories like Webpack). Update gulpfile paths accordingly:
dist/css/fonts/*โdist/fonts/*dist/css/img/*โdist/img/*dist/js/*.js+dist/css/*.cssโdist/*
7. Update package.json scripts
Section titled โ7. Update package.json scriptsโ{ "scripts": { "virtual": "cross-env SASS_JS_API=modern NODE_ENV=virtual vite", "local": "cross-env SASS_JS_API=modern NODE_ENV=local vite build", "build": "cross-env SASS_JS_API=modern NODE_ENV=production vite build", "dev-php": "gulp dphp --dev", "dev-dist": "gulp ddist --dev", "dev-disthash": "gulp ddisthash --dev", "dev-all": "gulp dall --dev", "dev-file": "gulp ds --dev --path", "dev-remove": "gulp remove --dev --path", "stg-php": "gulp dphp --stage", "stg-dist": "gulp ddist --stage", "stg-disthash": "gulp ddisthash --stage", "stg-all": "gulp dall --stage", "stg-file": "gulp ds --stage --path", "stg-remove": "gulp remove --stage --path", "prd-php": "gulp dphp --production", "prd-dist": "gulp ddist --production", "prd-disthash": "gulp ddisthash --production", "prd-all": "gulp dall --production", "prd-file": "gulp ds --production --path", "prd-remove": "gulp remove --production --path" }}8. Restructure JS entry points
Section titled โ8. Restructure JS entry pointsโBefore (Webpack):
src/js/entries/Project.js โ main entry, imports SCSSsrc/js/Main.js โ Main classsrc/js/app-backend.js โ backend entryAfter (Vite):
src/js/Project.js โ move from entries/ to js root, remove SCSS importsrc/js/ProjectStyles.js โ NEW: CSS extraction entrysrc/js/Main.js โ unchangedsrc/js/app-backend.js โ unchangedCreate src/js/ProjectStyles.js:
import "@scss/style.scss";This file exists because Vite needs a JS entry to generate CSS as a separate asset.
Move src/js/entries/Project.js โ src/js/Project.js:
- Remove the SCSS import (now in ProjectStyles.js)
- Remove
/* webpackChunkName: "..." */comments from all dynamicimport()calls - Update any relative paths that broke from the move (e.g.
../Main.jsโ./Main.js)
Rename main SCSS file:
src/scss/entries/frontend/pages/_project.scss โ src/scss/style.scss9. Remove /* webpackChunkName */ from dynamic imports
Section titled โ9. Remove /* webpackChunkName */ from dynamic importsโSearch and remove all webpack chunk name comments across the project:
// BEFORE:import(/* webpackChunkName: "CheckBanner" */ "@utilities/CheckBanner")
// AFTER:import("@utilities/CheckBanner")Vite handles code splitting automatically without these hints.
10. Refactor PHP: enqueues and hash
Section titled โ10. Refactor PHP: enqueues and hashโDelete:
functions/project/enqueues/frontend.phpfunctions/project/enqueues/backend.phpfunctions/project/enqueues/define-hash.php
Create functions/project/hash.php:
<?phpdefine('hash', 'abc');?>The hash is auto-updated by viteHelper.js on every build.
Create functions/project/enqueues.php (unified frontend + backend):
<?phpadd_action('wp_enqueue_scripts', function() { if (defined('IS_VITE_DEVELOPMENT') && IS_VITE_DEVELOPMENT === true) { // Virtual mode: load from Vite dev server function vite_head_module_hook() { echo '<script type="module" crossorigin src="http://localhost:9090/src/js/Project.js"></script>'; echo '<script type="module" crossorigin src="http://localhost:9090/src/scss/style.scss"></script>'; } add_action('wp_head', 'vite_head_module_hook'); } else { // Production mode: load built assets with hash wp_enqueue_script('project-build', get_template_directory_uri() . '/dist/Project.' . hash . '.js', [], null, true); add_filter('script_loader_tag', function ($tag, $handle, $src) { if ($handle === 'project-build') { $tag = '<script type="module" src="' . esc_url($src) . '"></script>'; } return $tag; }, 10, 3); wp_enqueue_style('project-build', get_template_directory_uri() . '/dist/ProjectStyles.' . hash . '.css'); }});
// Admin Backend Onlyadd_action('admin_enqueue_scripts', function() { wp_enqueue_style('admin-backend-style', get_template_directory_uri() . '/dist/Appbackend.' . hash . '.css');});?>Key changes in enqueues:
| Before (Webpack) | After (Vite) |
|---|---|
IS_VIRTUAL_ENV | IS_VITE_DEVELOPMENT |
constant("enqueue-hash") | hash (simple constant) |
Port 9000 | Port 9090 |
wp_enqueue_script for virtual | <script type="module"> via wp_head |
Separate frontend.php + backend.php | Single enqueues.php |
| SCSS bundled by Webpack | SCSS loaded directly in virtual (Vite processes on-the-fly) |
Update functions/project/local-variable.php:
<?phpif (!defined('IS_VITE_DEVELOPMENT')) { define('IS_VITE_DEVELOPMENT', true); // true = virtual, false = build}if (!defined('DEV_IDENTIFIER')) { define('DEV_IDENTIFIER', 'engine');}?>11. Simplify functions.php
Section titled โ11. Simplify functions.phpโReduce the root functions.php to three includes:
<?phprequire_once get_template_directory() . '/functions/project/local-variable.php';require_once get_template_directory() . '/functions/default/index.php';require_once get_template_directory() . '/functions/project/index.php';?>Move all require statements that were in functions.php into functions/project/index.php.
12. Reorganize functions/project/index.php
Section titled โ12. Reorganize functions/project/index.phpโCentralize all project requires:
<?php// Primaryrequire get_template_directory() . '/functions/project/local-variable.php';require get_template_directory() . '/functions/project/hash.php';require get_template_directory() . '/functions/project/enqueues.php';require get_template_directory() . '/functions/project/post-types.php';require get_template_directory() . '/functions/project/taxonomies.php';
// Blocksrequire get_template_directory() . '/functions/project/blocks/block-callout.php';// ... rest of blocks
// Custom ACF, APIs, etc.// ... project-specific requires
// Utilitiesrequire get_template_directory() . '/functions/project/utilities/get-spacing.php';require get_template_directory() . '/functions/project/utilities/remove-editor.php';// ... rest of utilities?>Move files to new locations:
functions/project/extend-sidebar/post-types.phpโfunctions/project/post-types.phpfunctions/project/extend-sidebar/taxonomies.phpโfunctions/project/taxonomies.phpfunctions/project/local/local-variable.phpโfunctions/project/local-variable.php- Extract inline functions from
index.php(get_spacing,remove_editor, etc.) into individual files underfunctions/project/utilities/
13. Move static assets to public/
Section titled โ13. Move static assets to public/โmv assets/ public/assets/Vite serves static files from public/. Everything in the old root assets/ directory moves to public/assets/.
Also place inline SVGs as individual files in public/assets/img/.
14. Move fonts to public/assets/fonts/
Section titled โ14. Move fonts to public/assets/fonts/โIf the project has fonts referenced from CSS, move them to public/assets/fonts/ so Vite copies them to dist correctly.
15. Update _paths.scss
Section titled โ15. Update _paths.scssโVerify that _paths.scss uses the $base-path variable injected by Vite via additionalData.
16. Update .gitignore
Section titled โ16. Update .gitignoreโ/node_modules/dist17. Clean up obsolete files
Section titled โ17. Clean up obsolete filesโDelete everything that was replaced:
webpack.common.js,webpack.dev.js,webpack.prod.jsconfig/index.jsfunctions/project/enqueues/(entire directory)functions/project/local/(directory โ file moved up)functions/reference/(if applicable โ reference code not in use)functions/default/custom/(if functions were consolidated intocleanHouse.php)src/js/entries/(directory โ file moved up)src/scss/entries/(directory โ file renamed)
Verification checklist
Section titled โVerification checklistโnpm run virtualstarts dev server on port 9090 with HMR- PHP changes trigger browser reload automatically
- SCSS changes apply with hot reload
npm run localgenerates build in/distnpm run buildgenerates production build in/dist- Hash in
functions/project/hash.phpauto-updates on build - Scripts load as
type="module"in production - Fonts copy correctly to
dist/fonts/ - Dynamic imports do code splitting without
webpackChunkName - Gulp deploy tasks (
dev-php,dev-dist, etc.) work - Backend CSS (
Appbackend.{hash}.css) loads in admin - No remaining references to Webpack in the codebase
- CSS
url()for fonts/images resolves correctly in both virtual and build
Gotchas
Section titled โGotchasโ| Problem | Cause | Fix |
|---|---|---|
@use in additionalData causes undefined vars | @use has per-file scope, doesnโt propagate through @import chains | Use @import instead of @use in additionalData |
./node_modules/ imports fail in SCSS | Sass resolves ./ relative to the file, not project root | Add loadPaths: ['.'] in scss options |
| Fonts/images 404 in virtual | Absolute url('/src/...') resolves against WordPress host, not Vite | Add server.origin: 'http://localhost:9090' |
process.env undefined | Webpack DefinePlugin replaced it, Vite doesnโt by default | Add define: { 'process.env': JSON.stringify(...) } in vite.config |
| Relative imports break after moving entry file | ../Module.js resolves from new location | Update relative paths in moved files |
Sass @import deprecation warnings | Dart Sass 2.x deprecates @import | Cosmetic only, add silenceDeprecations: ['import'] to suppress |
| Fonts 404 in build | Fonts not in public/assets/fonts/ | Move fonts there + verify assetFileNames in vite.config |
| HMR not working | IS_VITE_DEVELOPMENT is false or port 9090 is busy | Check local-variable.php and free the port |
| Hash not updating on build | hash.php doesnโt exist or has wrong format | viteHelper.js expects pattern define('hash', '...'); |
cross-env not found | Not installed | npm install --save-dev cross-env |
| Vite dist structure differs from Webpack | Webpack: dist/js/ + dist/css/, Vite: flat dist/ | Update gulpfile paths: dist/css/fonts/* โ dist/fonts/*, etc. |
Knowledge Check
Test your understanding of this section
Loading questions...