Move from Webpack to Vite without the manual config rewrite.
Paste a webpack.config.js and get back a Vite config skeleton, a tiered report (manual, verify, info), and a dependency checklist. It reads your config statically and never runs it, so the boring parts are mapped for you and anything risky is flagged for review. AST-based, not regex.
npm i -D @shiftkit/webpack-to-vite- Mapped: Config translated deterministically.
- Source: No detect-only source traps were surfaced for this config.
- Then: Still run vite build && vite preview once before merging.
Can’t map from config alone. You decide.
Mapped, but behavior may differ. Worth a look.
Bookkeeping. Vite handles it.
Migration checklist
Config translation is the start, not the finish. Work top-down, or skip the file-writing steps with the CLI’s --apply mode (below).
- Save the config. Drop the output above into
vite.config.tsat your project root. - Move to an HTML entry. Vite is HTML-first: create
index.htmlat the root with<script type="module" src="/src/index.tsx">instead of lettingHtmlWebpackPlugingenerate it. If your config used the plugin, the generated skeleton in the “Generated index.html” block above is ready to save. - Install dependencies.See the “Required dependencies” block above.
- Migrate env vars. Rename build-time vars to the
VITE_prefix and read them viaimport.meta.envin source (onlyVITE_vars reach the client). - Fix the source traps. Convert
require()/require.context/worker-loaderusages (see below). - Test the build, not just dev. Run
vite build && vite preview. Vite’s build pipeline can differ from the dev server. - Migrate Jest if present. Use the sister tool, Jest → Vitest.
Skip the file steps: --apply
Prefer the terminal? --apply writes the config and a JSON report to disk (plus the index.html skeleton when your config uses HtmlWebpackPlugin and none exists). It never deletes your webpack config, and it only touches package.json if you opt in with --deps - which adds the required dependencies from the checklist and never removes anything:
npx @shiftkit/webpack-to-vite webpack.config.js --apply --out vite.config.tsAdd --strict in CI to fail the build when any manual-review item is emitted, --json for a machine-readable result, or --target-vite 7 for the rollupOptions fallback.
What gets mapped
The analyzer parses your config with Babel rather than regex, so it understands TypeScript, defineConfig() wrappers, identifier re-exports, webpack-merge, function-form configs (read without executing), and conditional plugin arrays (cond && new Plugin(), .filter(Boolean), .concat(…)). A partial mapping table follows. Anything that is not a clean mapping gets a tiered warning instead of a silent guess.
| Webpack | Maps to | Vite | Notes |
|---|---|---|---|
| resolve.alias | → | resolve.alias [{ find, replacement }] | name$ becomes an exact /^name$/ rule |
| babel/ts/css loaders | → | dropped (native) | Vite transpiles & handles CSS; no type-check |
| @svgr/webpack | → | vite-plugin-svgr | import via ./icon.svg?react |
| vue-loader / svelte-loader | → | framework plugin | @vitejs/plugin-vue etc., wired into the output |
| type: ‘asset/*’ | → | dropped (native) | asset/inline → ?inline, asset/source → ?raw |
| devServer.https | → | server.https | cert objects copy through; true suggests @vitejs/plugin-basic-ssl |
| DefinePlugin | → | define | process.env.* → import.meta.env |
| HtmlWebpackPlugin | → | root index.html | verify: move template, add module script |
| devServer.proxy | → | server.proxy | pathRewrite → rewrite fn |
| output.path / publicPath | → | build.outDir / base | static values only |
| devtool | → | build.sourcemap | source-map / inline / hidden |
Detect-only traps (never auto-rewritten)
These live in your application source, not the config. The analyzer flags them so you migrate them deliberately. It will not rewrite source for you:
require()/module.exports- convert app source to ESM (import/export).require.context()and dynamicimport()with a variable - useimport.meta.glob()with a static glob.process.env.Xin source - migrate toimport.meta.env.VITE_X.worker-loader/new Worker(…)- use the?workerimport suffix ornew Worker(new URL(…), { type: ‘module’ }).module.hot- Vite usesimport.meta.hotwith a similar but distinct API.externalsandModuleFederationPlugin- reviewed by hand; external and federation semantics differ from webpack.
How accurate is the output?
Mappings are deterministic and covered by a fixture suite. The output is a skeletonwith a confidence band and tiered warnings. It never claims source or architecture migration is complete, which is why “Verify before merging” appears even when the config maps cleanly.
FAQ
Does ShiftKit run or upload my webpack config?
No. ShiftKit does not run your webpack config. It parses the config statically in a browser Web Worker (Babel AST, no eval, no new Function, no execution of your config). Your config is never uploaded.
Is this a one-click converter?
No. It is an analyzer. Config translation is only half of a webpack to Vite migration. The risky parts live in your app source (require, process.env, require.context, web workers, Module Federation). The tool emits a Vite 8-oriented vite.config.ts skeleton and a tiered report, and marks everything that needs manual work. It never claims source or architecture migration is done.
Why Vite 8 by default?
Vite 8 is current and ships Rolldown as the unified bundler. The output uses build.rolldownOptions and Vite 8’s built-in resolve.tsconfigPaths. Toggle to Vite 7 to get build.rollupOptions plus the vite-tsconfig-paths plugin instead.
What happens with a function-form config?
The analyzer reads the returned object statically without executing the function. If the config branches on env/argv, it surfaces a manual warning telling you to resolve the conditional config yourself. It never picks a branch for you.
Why a confidence band instead of a percentage?
A percentage would be misleading. Each field either maps cleanly, maps with a caveat, or cannot be mapped from config alone. The band (High confidence / Verify before merging / Manual review required) plus raw counts (e.g. 2 manual · 4 verify · 6 info) tells you exactly what to check before merging.
Can I run this in the terminal or in CI?
Yes. Run "npx @shiftkit/webpack-to-vite webpack.config.js", add --json for a machine-readable result, or --strict to exit non-zero when any manual-review item exists. --apply --out vite.config.ts writes the config plus a JSON report; it never deletes your webpack config and never mutates package.json. A GitHub Action mirrors the flags.
How is the package secured against supply-chain attacks?
Releases publish from GitHub Actions via npm Trusted Publishing (OIDC, no long-lived tokens) with a provenance attestation, so you can verify the tarball was built from the tagged commit. We also recommend "min-release-age=7" in your .npmrc, which blocks freshly-published (and not-yet-caught) malicious versions.
Changelog
Pulled from the package’s CHANGELOG.md at build time. The currently installed version is v0.3.0.
- v0.3.02026-06-13latest
- feat`output.library`/`libraryTarget` — previously dropped without a word — now emits a real `build.lib` block (entry from the detected webpack entry, `name`, and `formats` mapped from `umd`→`umd`, `module`→`es`, `commonjs*`→`cjs`, `var`/`window`/…→`iife`) with a verify warning. Unknown library types fall back to a manual warning.
- featUnknown keys inside `resolve`, `output`, and `module` (e.g. `resolve.mainFields`, `output.assetModuleFilename`, `module.noParse`) now emit named `resolve.unmapped`/`output.unmapped`/`module.unmapped` warnings instead of being dropped in silence — the systemic fix for the audit's silent-drop bug class. `resolve.symlinks` points at Vite's inverse `preserveSymlinks`; benign keys (`output.pathinfo`, `module.strictExportPresence`, …) report as info.
- featStatic `sass-loader`/`less-loader`/`stylus-loader` options (`additionalData`, `prependData`, flattened `sassOptions`/`lessOptions`/`stylusOptions`) are now emitted as a `css.preprocessorOptions.<lang>` block instead of only a warning telling you to move them. `implementation`/`sourceMap`-style webpack-only options are dropped.
- feat`entry: path.resolve(__dirname, 'src/index.js')` (and `path.resolve` values inside object/array entries) is now statically evaluated and mapped instead of falling to the manual `entry.dynamic` warning.
- feat`HtmlWebpackPlugin` now produces a ready-to-paste `index.html` skeleton (module script tag pointing at the detected entry; `title`/`favicon`/`template` read statically). It is returned as `result.indexHtml` and written next to the config by `--apply` when no `index.html` exists.
- feat`devServer.static`/`contentBase` with a static string or `{ directory }` value now emits `publicDir` directly instead of only describing the mapping.
- featNew real-project fixture suite (`tests/real-fixtures.test.ts`): ejected CRA, vue-cli base, webpack 5 asset modules, a UMD library build, and a webpack-merge prod config, each snapshotting the full rendered output, the migration report, and the dependency checklist.
- featCLI: new `--deps` flag (with `--apply`) opts in to updating `package.json` — it adds the *required* dependencies from the checklist to devDependencies and never removes anything. Without `--deps`, `package.json` is untouched as before. This resolves the documented asymmetry with `@shiftkit/jest-to-vitest --apply`.
- featSection values are no longer dropped silently. `plugins: [...].filter(Boolean)` and `[...].concat(cond ? [...] : [])` are unwrapped to the underlying array literal, conditional plugin elements (`cond && new X()`, ternaries) are classified on both branches, and any `plugins`/`output`/`resolve`/`module`/`devServer`/`optimization` value that cannot be read statically now emits a manual `config.dynamic` warning instead of nothing. Previously a config using these idioms converted to an empty skeleton rated "High confidence".
- featwebpack 5 asset modules (`type: 'asset/resource'` etc.) are now recognized: `asset/resource` and `asset` report as natively handled (info), `asset/inline` and `asset/source` flag the `?inline`/`?raw` import-shape changes (verify). Previously these rules were invisible.
- feat`resolve.fallback` now sets `needsNodePolyfills`, emits a manual `resolve.fallback` warning, and suggests `vite-plugin-node-polyfills`. Previously a Node-polyfill config converted to an empty skeleton with no warnings.
- featExact-match aliases containing `/` (e.g. `'@app/core$'`) no longer render a syntactically invalid regex literal; slashes are escaped in the generated `find` pattern.
- feat`devServer.https`/`devServer.server` mapping rewritten: boolean `https: true` is no longer emitted (Vite's `server.https` takes `https.createServer()` options; `@vitejs/plugin-basic-ssl` is suggested instead, new `devServer.https` code), cert-option objects are copied through, and `server: 'http'` is dropped instead of being converted into an HTTPS dev server.
- feat`output.path` keeps nested directories (`build/static`) instead of only the last segment; absolute paths fall back to the last segment with a verify warning.
- featArray entries now use the last element as the build input (webpack exports the last; earlier ones are usually polyfills). Previously the first element was used.
- feat`vue-loader`/`VueLoaderPlugin` (and `svelte-loader`) are classified as replaced by the framework plugin the analyzer already wires in, instead of producing contradictory manual warnings on every standard Vue config.
- featMulti-config arrays now emit `config.multiConfig` instead of the mis-tagged `config.functionForm`.
- featCLI: absolute file paths work for the positional argument and `--out` (previously mangled by `join(cwd, ...)`).
- v0.2.02026-06-11
- featVite 8-first Webpack → Vite migration analyzer with a parse → static-eval → intermediate-model → render pipeline. Static AST only, so your webpack config is never executed.
- feat18 fixtures (12 release-gate + module-federation/css-modules/alias-array-value/html-plugin + stretch) with exact tier/code snapshots, flag assertions, valid-TS output checks, a source-level no-eval/no-`new Function` gate, and CLI `--json`/`--strict` tests.