Frontend Upgrade Operations

SkillAI & models

Upgrade a server-rendered Craft CMS/Twig front end: Laravel Mix or Webpack to Vite via craft-vite (manifest, DDEV dev server, HMR, Tailwind v3, legacy builds, Twig asset URLs) and Vue 2 to Vue 3 (@vue/compat, Vuex to Pinia, plugin swaps, Vue islands in Twig). Use when a site still has webpack.mix.js, mix-manifest.json or Vue 2, or when deciding if a small Vue widget should become Alpine.js or vanilla JS instead.

Instructions available. Your AI can read the instructions. Execution depends on the setup they require.

Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.

Then ask your AI: use the Frontend Upgrade Operations skill

What this skill tells your AI

The instructions your AI receives, as published by 0xdarkmatter/claude-mods in skills/frontend-upgrade-ops/SKILL.md and read by ahel’s review.

The two front-end upgrades a Craft CMS (or any Twig/PHP) agency keeps meeting: an asset pipeline still on Laravel Mix or raw Webpack, and widgets still on Vue 2. Both are frozen - Mix's last release was 6.0.49 in June 2022 and Laravel calls it "a legacy package that is no longer actively maintained"; Vue 2 reached end of life on 31 December 2023. This skill is the playbook for moving a server-rendered site to Vite 8 + craft-vite and Vue 3, or for deciding that a widget should not be Vue at all.

Version facts verified as of 2026-10-05 against npm, Packagist, vite.dev, v3-migration.vuejs.org and nystudio107.com/docs/vite. Each reference cites its sources.

Start here: what does this repo need?

You findDoRead
webpack.mix.js, mix-manifest.json, mix() in TwigReplace Mix with Vite + craft-vitemix-webpack-to-vite.md, then craft-vite-twig.md
A hand-written webpack.config.jsSame move; translate loaders and plugins one by onemix-webpack-to-vite.md
craft-vite already, but the dev server fails behind DDEVCORS / allowedHosts / one port in four placescraft-vite-twig.md
"vue": "^2 in package.jsonDecide per widget: migrate or replace (table below)this file, then vue2-to-vue3.md
Mix and Vue 2Bundler first, framework second - never one deploySequencing
vuex, vue-router 3, Vue 2-only pluginsPinia; check every plugin for a Vue 3 path before estimatingvue-state-and-plugins.md
Vue templates written in Twig (inline-template, in-DOM)Convert to SFC islands; mind the compiler build and template injectionvue-islands-in-twig.md
Vue 3 + Vite alreadyOut of scope here - steady-state Vue is vue-ops, steady-state Craft is craftcms-ops-

Migrate Vue, or replace it?

Most agency Vue 2 code is not an app; it is a handful of widgets. Migrating a widget that a dozen lines of Alpine would do costs more than replacing it. Decide per island, before estimating the migration.

The widget is...VerdictWhy
Toggle, accordion, tabs, mobile nav, show/hideNative HTML or Alpine.js<details>, <dialog>, popover cover much of it with zero JS; the rest is a boolean in x-data written straight into Twig
A form with validation and a submit spinnerAlpine + native constraint validationNo component tree, no store; Alpine keeps the markup in Twig where editors expect it
A thin wrapper around a framework-free library (slider, date picker, map, lightbox)Vanilla JS or Alpine calling the library directlyThe Vue wrapper was the only Vue 2-specific part
Renders server data and never changes itDelete it; render in TwigIt's a template, not an app
Filtering/sorting a list with derived counts, several child componentsVue 3Computed state and components earn their weight
Several islands sharing state (cart, saved items, auth)Vue 3 + PiniaA shared store across apps is what Vue + Pinia does well
Routing inside a section (multi-step tool, dashboard)Vue 3 (+ vue-router)Alpine has no answer for this
Built on a Vue 2 UI kit with no Vue 3 releaseRewrite budget, Vue 3 or Alpine, decided by the rows aboveThe kit, not Vue, is the migration

Rules of thumb for the grey zone: under ~150 lines with one level of state, Alpine wins; any use of Vuex, a router, or more than two child components, Vue 3 wins. A site that ends up with both is normal - see the Alpine coexistence rules in vue-islands-in-twig.md.

Sequencing both upgrades

Each numbered step is a separate deploy with its own parity check. Combining the bundler swap with the framework swap is the classic mistake: when a page breaks you cannot tell which change did it.

0. Inventory      rg the repo (below); list page types that render JS widgets
1. Vue 2.7.16     if on Vue 2.6 - backports Composition API + <script setup>
2. Mix -> Vite    still on Vue 2: Vite 7.3 + @vitejs/plugin-vue2
                  (plugin-vue2's peer range ends at vite ^7.0.0 as of 2026-10-05;
                   Vite 7.3 still gets important fixes + security patches)
3. Replace        widgets the table above says shouldn't be Vue -> Alpine/vanilla
4. Vuex -> Pinia 2 (optional, still on Vue 2.7; Pinia 3+ dropped Vue 2)
5. Vue 3          @vue/compat on Vite 8 + @vitejs/plugin-vue; burn down warnings
6. Drop compat    then Pinia 4, then remove @vitejs/plugin-legacy if analytics allow

No Vue on the site? Steps 1, 3-6 vanish: go straight to Vite 8.

If Vue 2 must die first (a security finding, a hosting rule), the order flips: Vue 3 via @vue/compat on Webpack is supported by the migration guide, then Vite. It is the slower path - two configurations of Vue 3 tooling instead of one.

# Step 0 - inventory in one pass
fd -H "webpack.mix.js|webpack.config.js|mix-manifest.json|vite.config" -E node_modules
jq '{vue:.dependencies.vue, mix:.devDependencies["laravel-mix"], webpack:.devDependencies.webpack, vite:.devDependencies.vite, vuex:.dependencies.vuex}' package.json
rg -n "mix\(|craft\.vite|data-vue-island|inline-template|new Vue\(" templates/ src/ | head -50

Cutover checklist: Mix/Webpack to Vite

Work top to bottom; each line links to the detail.

  • Branch from a green build; record a parity baseline - screenshots of every page type + the network panel's JS/CSS list on the Mix build, plus lab LCP/CLS/TBT for the key templates (median of 5 runs, method in web-perf-ops)
  • npm pkg set type="module" (or name configs .mjs/.cjs explicitly)
  • vite.config.js: base, build.manifest, outDir: web/dist, entries in build.rolldownOptions.input (config)
  • Every webpack.mix.js call mapped (translation table)
  • Source rewrites: require, require.context, .vue extensions, process.env, MIX_ to VITE_, Sass ~ imports (rewrites)
  • postcss.config.js + tailwind.config.js load; content globs still find templates/**/*.twig - stay on Tailwind v3 for this deploy
  • composer require nystudio107/craft-vite (5.x Craft 5, 4.x Craft 4); config/vite.php with manifestPath = @webroot/dist/.vite/manifest.json
  • DDEV: the same port in web_extra_exposed_ports, server.port, server.origin, devServerPublic; cors + allowedHosts for .ddev.site (DDEV)
  • Twig: every mix() / hard-coded /js/, /css/ tag replaced with craft.vite.script(...); main CSS with asyncCss = false unless critical CSS exists
  • Inline scripts that used bundle globals at parse time fixed (module scripts are deferred) (why)
  • Twig-referenced images resolved via craft.vite.asset() or left in web/
  • Legacy build: decided from analytics, @vitejs/plugin-legacy only if needed (legacy)
  • HMR works for CSS + JS; Twig edits reload (vite-plugin-restart)
  • vite build in CI/deploy; web/dist/ gitignored; no hashed files committed
  • Parity check against the baseline: every page type, fonts, icons, console clean, and the lab metrics - async CSS and modulepreload move LCP and CLS (web-perf-ops has the LCP/CLS fixes)
  • Deploy keeps the previous build until full-page caches are purged
  • Delete webpack.mix.js, mix-manifest.json, old web/js + web/css, laravel-mix and webpack-only devDependencies - in a follow-up commit

Vue 2 to 3 at a glance

The full workflow, the breaking-change table and testing live in vue2-to-vue3.md. The shape:

  1. Alias vue to @vue/compat, set compatConfig: { MODE: 2 } in @vitejs/plugin-vue.
  2. Port new Vue({ el }) to createApp(...).mount(el) - per island.
  3. Fix compile errors, rename transition classes, upgrade Vuex/router, then runtime warnings one compat ID at a time (compatConfig: { MODE: 3 } per clean component).
  4. Global MODE: 3, zero warnings, remove compat.

Three Twig-site landmines worth knowing before you start:

  • inline-template is removed - Twig-authored templates need a scoped slot, a <script type="text/html"> template, or conversion to an SFC island.
  • Removing compat removes the template compiler. @vue/compat resolves to the full build, plain vue to runtime-only; in-DOM islands go blank unless vue is aliased to vue/dist/vue.esm-bundler.js.
  • In-DOM templates execute {{ }} from content - client-side template injection. SFC islands with props avoid it (security).

Common gotchas

GotchaPrevention
Bundler and framework upgraded in one deploySequencing above; one parity check per step
Tailwind v4 "while we're at it"Separate migration with its own visual diff (tailwind-ops)
cors: true / allowedHosts: true copied from a sampleRegex + .ddev.site; true opens the dev server to DNS rebinding
Each island calls createPinia()One exported instance, app.use(pinia) everywhere
@vitejs/plugin-vue2 on Vite 8Its peer range ends at Vite 7; finish Vue 3 before Vite 8, or stay on Vite 7.3 meanwhile
Secrets moved from MIX_ to VITE_ varsVITE_* is inlined into public JS - keep secrets server-side
Old Mix files deleted in the cutover deployDelete in a follow-up once nothing references them
Estimating Vue 3 before checking pluginsOne Vue 2-only plugin can be the critical path - inventory first

Reference files

FileContentsLines
references/mix-webpack-to-vite.mdMix API to Vite map, vite.config.js for Craft, source rewrites, Tailwind v3/PostCSS/Sass, legacy builds, output~200
references/craft-vite-twig.mdcraft-vite install + config/vite.php, DDEV dev server/HMR, Twig tags, asset URLs, deploy, troubleshooting~220
references/vue2-to-vue3.mdMigration build under Vite, warning burn-down, Twig-relevant breaking changes, removing compat, testing~190
references/vue-state-and-plugins.mdVuex to Pinia (2 vs 4), shared store across islands, Vue 2 plugin replacement table~170
references/vue-islands-in-twig.mdIsland bootstrap, props from Twig, in-DOM templates, template injection, Alpine/Sprig/cache coexistence~190

Staleness verifier

The versions above move. scripts/check-frontend-upgrade-facts.py guards them against silent drift; the catalogue is assets/frontend-upgrade-facts.json.

# Structural (PR CI, no network): every catalogued package/fact is still named in the
# prose, and SKILL.md keeps a dated "as of" note.
python scripts/check-frontend-upgrade-facts.py --offline   # exit 0 consistent, 10 drift

# Live (freshness job, never blocks a PR): npm + Packagist majors vs documented majors.
python scripts/check-frontend-upgrade-facts.py --live      # exit 10 a major moved, 7 unreachable

A --live drift means the world moved (say, Vite 9 or Pinia 5): re-verify the affected reference against its cited source, then bump documented_major - never just the number.

See also

SkillWhen to combine
migrate-opsGeneric upgrade strategy, rollback, codemods, other frameworks
vue-opsSteady-state Vue 3: Composition API, Pinia, Vue Router, testing
craftcms-opsSteady-state Craft 5: Twig, element queries, Matrix-as-entries
ddev-opsDDEV itself: Node pinning and EOL majors, exposed ports and daemons, Mutagen and node_modules
web-perf-opsBefore/after Core Web Vitals for the cutover; LCP/INP/CLS fixes once on Vite
tailwind-opsThe separate Tailwind v3 to v4 migration
playwright-opsPage-type smoke tests for the parity checks
security-opsReviewing in-DOM template injection and VITE_* exposure
package-manager-opsThe install side: one lockfile, npm ci in CI and deploy, the Node pin, node-sass to Dart Sass, Bower to npm

Signals

GitHub stars
43
Forks
7
Last commit
Sep 2026

ahel review

  • K1binfo
    installs-packages (in references/craft-vite-twig.md)
  • K1binfo
    installs-packages (in references/mix-webpack-to-vite.md)
  • K1binfo
    installs-packages (in references/vue-state-and-plugins.md)
  • K1binfo
    installs-packages (in references/vue2-to-vue3.md)

Automated review, not a security audit. Ruleset v1+k2.

Advanced
Item type
skill
Key
frontend-upgrade-ops
Source
github.com/0xdarkmatter/claude-mods