Skip to content

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.


FileReplacesPurpose
vite.config.jswebpack.common.js + webpack.dev.js + webpack.prod.jsSingle build config
config/viteHelper.jsconfig/index.jsHash generation + cleanup plugin
config/sftpConfig.jsHardcoded creds in gulpfile.jsSFTP credentials per environment
.env.virtualโ€”Dev server (HMR) environment vars
.env.localโ€”Local build environment vars
.env.productionโ€”Production build environment vars
  • webpack.common.js, webpack.dev.js, webpack.prod.js
  • config/index.js
  • functions/project/enqueues/frontend.php, backend.php, define-hash.php
  • functions/project/local/ (directory โ€” file moves up)
  • src/js/entries/ (directory โ€” file moves up)
  • src/scss/entries/ (directory โ€” file renamed)
BeforeAfter
src/js/entries/Project.jssrc/js/Project.js
src/scss/entries/frontend/pages/_project.scsssrc/scss/style.scss
functions/project/enqueues/define-hash.phpfunctions/project/hash.php
functions/project/local/local-variable.phpfunctions/project/local-variable.php
functions/project/enqueues/ (frontend + backend)functions/project/enqueues.php (unified)
functions/project/extend-sidebar/post-types.phpfunctions/project/post-types.php
functions/project/extend-sidebar/taxonomies.phpfunctions/project/taxonomies.php
assets/ (root)public/assets/
IS_VIRTUAL_ENV (PHP constant)IS_VITE_DEVELOPMENT
enqueue-hash (PHP constant)hash

Remove Webpack devDependencies:

Terminal window
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-merge

Remove dependencies no longer needed:

Terminal window
npm uninstall rename-webpack-plugin sass-loader vue-loader webp-loader @terrahq/lazy

Install Vite dependencies:

Terminal window
npm install --save-dev vite vite-plugin-live-reload cross-env minimist ssh2-sftp-client
npm install --save @vitejs/plugin-vue dotenv blazy

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=true
VITE_BASE=./
VITE_LOG_LEVEL=info
VITE_WP_PATH=http://localhost/{wp-install}/wp-content/themes/{theme-name}/public

.env.local โ€” local build:

VITE_TERRA_VIRTUAL=false
VITE_BASE=http://localhost/{wp-install}/wp-content/themes/{theme-name}/dist/
VITE_LOG_LEVEL=silent
VITE_WP_PATH=.

.env.production โ€” production build:

VITE_TERRA_VIRTUAL=false
VITE_BASE=/wp-content/themes/{theme-name}/dist/
VITE_LOG_LEVEL=silent
VITE_WP_PATH=.

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.js resolve.alias into resolve.alias. Same syntax. Add node_modules alias 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โ€™s DefinePlugin. Only needed if source code references process.env.
  • loadPaths: Required for Sass to resolve ./node_modules/ from project root instead of relative to each file.
  • @import vs @use in additionalData: Must use @import. @use is file-scoped and variables donโ€™t propagate through the import chain.
  • silenceDeprecations: Dart Sass 2.x deprecates @import in favor of @use. The warning is cosmetic โ€” @import still works and is required in additionalData.
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]);
},
};
}

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 };

Main changes:

  • Use minimist to parse arguments instead of detecting npm_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) and remove tasks
  • 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/*
{
"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"
}
}

Before (Webpack):

src/js/entries/Project.js โ† main entry, imports SCSS
src/js/Main.js โ† Main class
src/js/app-backend.js โ† backend entry

After (Vite):

src/js/Project.js โ† move from entries/ to js root, remove SCSS import
src/js/ProjectStyles.js โ† NEW: CSS extraction entry
src/js/Main.js โ† unchanged
src/js/app-backend.js โ† unchanged

Create 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 dynamic import() 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.scss

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.

Delete:

  • functions/project/enqueues/frontend.php
  • functions/project/enqueues/backend.php
  • functions/project/enqueues/define-hash.php

Create functions/project/hash.php:

<?php
define('hash', 'abc');
?>

The hash is auto-updated by viteHelper.js on every build.

Create functions/project/enqueues.php (unified frontend + backend):

<?php
add_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 Only
add_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_ENVIS_VITE_DEVELOPMENT
constant("enqueue-hash")hash (simple constant)
Port 9000Port 9090
wp_enqueue_script for virtual<script type="module"> via wp_head
Separate frontend.php + backend.phpSingle enqueues.php
SCSS bundled by WebpackSCSS loaded directly in virtual (Vite processes on-the-fly)

Update functions/project/local-variable.php:

<?php
if (!defined('IS_VITE_DEVELOPMENT')) {
define('IS_VITE_DEVELOPMENT', true); // true = virtual, false = build
}
if (!defined('DEV_IDENTIFIER')) {
define('DEV_IDENTIFIER', 'engine');
}
?>

Reduce the root functions.php to three includes:

<?php
require_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.

Centralize all project requires:

<?php
// Primary
require 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';
// Blocks
require get_template_directory() . '/functions/project/blocks/block-callout.php';
// ... rest of blocks
// Custom ACF, APIs, etc.
// ... project-specific requires
// Utilities
require 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.php
  • functions/project/extend-sidebar/taxonomies.php โ†’ functions/project/taxonomies.php
  • functions/project/local/local-variable.php โ†’ functions/project/local-variable.php
  • Extract inline functions from index.php (get_spacing, remove_editor, etc.) into individual files under functions/project/utilities/
Terminal window
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/.

If the project has fonts referenced from CSS, move them to public/assets/fonts/ so Vite copies them to dist correctly.

Verify that _paths.scss uses the $base-path variable injected by Vite via additionalData.

/node_modules
/dist

Delete everything that was replaced:

  • webpack.common.js, webpack.dev.js, webpack.prod.js
  • config/index.js
  • functions/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 into cleanHouse.php)
  • src/js/entries/ (directory โ€” file moved up)
  • src/scss/entries/ (directory โ€” file renamed)

  • npm run virtual starts dev server on port 9090 with HMR
  • PHP changes trigger browser reload automatically
  • SCSS changes apply with hot reload
  • npm run local generates build in /dist
  • npm run build generates production build in /dist
  • Hash in functions/project/hash.php auto-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

ProblemCauseFix
@use in additionalData causes undefined vars@use has per-file scope, doesnโ€™t propagate through @import chainsUse @import instead of @use in additionalData
./node_modules/ imports fail in SCSSSass resolves ./ relative to the file, not project rootAdd loadPaths: ['.'] in scss options
Fonts/images 404 in virtualAbsolute url('/src/...') resolves against WordPress host, not ViteAdd server.origin: 'http://localhost:9090'
process.env undefinedWebpack DefinePlugin replaced it, Vite doesnโ€™t by defaultAdd define: { 'process.env': JSON.stringify(...) } in vite.config
Relative imports break after moving entry file../Module.js resolves from new locationUpdate relative paths in moved files
Sass @import deprecation warningsDart Sass 2.x deprecates @importCosmetic only, add silenceDeprecations: ['import'] to suppress
Fonts 404 in buildFonts not in public/assets/fonts/Move fonts there + verify assetFileNames in vite.config
HMR not workingIS_VITE_DEVELOPMENT is false or port 9090 is busyCheck local-variable.php and free the port
Hash not updating on buildhash.php doesnโ€™t exist or has wrong formatviteHelper.js expects pattern define('hash', '...');
cross-env not foundNot installednpm install --save-dev cross-env
Vite dist structure differs from WebpackWebpack: 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...