|
| 1 | +--- |
| 2 | +name: Handlebars v3 to v4 Migration |
| 3 | +description: >- |
| 4 | + Use this skill when migrating a BigCommerce Stencil theme from Handlebars v3 to v4, |
| 5 | + upgrading the template engine, fixing context depth (../) issues in templates. |
| 6 | + Also triggers when the user asks about "handlebars upgrade", |
| 7 | + "template engine v4", "stencil handlebars migration", "../ not resolving correctly", |
| 8 | + "handlebars_v4 config.json", or "handlebars breaking changes". |
| 9 | + Also triggers when updating a CHANGELOG after a Handlebars migration. |
| 10 | + Always use this skill when the user mentions upgrading or migrating their theme's template engine. |
| 11 | +--- |
| 12 | + |
| 13 | +# Handlebars v3 to v4 Migration |
| 14 | + |
| 15 | +This skill guides you through migrating a BigCommerce Stencil theme from Handlebars v3 to v4. |
| 16 | +There are four required steps — work through them in order. |
| 17 | + |
| 18 | +--- |
| 19 | + |
| 20 | +## Step 1: Update config.json |
| 21 | + |
| 22 | +Every Stencil theme has a `config.json` at the repo root. Find the `"version"` key and add the |
| 23 | +engine flag immediately after it: |
| 24 | + |
| 25 | +```json |
| 26 | +{ |
| 27 | + "version": "...", |
| 28 | + "template_engine": "handlebars_v4", |
| 29 | + ... |
| 30 | +} |
| 31 | +``` |
| 32 | + |
| 33 | +To revert, remove that line or set it to `"handlebars_v3"`. |
| 34 | + |
| 35 | +--- |
| 36 | + |
| 37 | +## Step 2: Fix context depth (`../`) in templates |
| 38 | + |
| 39 | +### What changed |
| 40 | + |
| 41 | +**General rule:** in v4, a block helper creates a context frame **only if it actually changes the context**. In v3 this was inconsistent — conditional helpers also created frames even though they don't change context, so templates used extra `../` to compensate. In v4 that was fixed: conditional helpers (`{{#if}}`, `{{#unless}}`, and platform-level helpers like `{{#or}}`, `{{#and}}`, `{{#compare}}`, `{{#inArray}}`, `{{#any}}`) no longer create frames. This means existing `../` inside conditional blocks may now point one level too high. |
| 42 | + |
| 43 | +| Helper | v3 | v4 | |
| 44 | +|--------|----|----| |
| 45 | +| `{{#if}}` / `{{#unless}}` | created frame | **no frame** — `../` must be removed | |
| 46 | +| Built-in or platform **conditional** helpers (`{{#or}}`, `{{#and}}`, `{{#compare}}`, etc.) | created frame | **no frame** — same rule as `#if` | |
| 47 | +| `{{#each}}` / `{{#with}}` | creates frame | creates frame — no change needed | |
| 48 | +| Platform **iterator** helpers (`{{#for}}`, `{{#enumerate}}`) | creates frame | creates frame — no change needed | |
| 49 | + |
| 50 | +### Find all affected templates |
| 51 | + |
| 52 | +Search the theme for every `../` occurrence: |
| 53 | + |
| 54 | +```bash |
| 55 | +grep -rn "\.\.\/" templates/ --include="*.html" |
| 56 | +grep -rn "@\.\.\/" templates/ --include="*.html" |
| 57 | +``` |
| 58 | + |
| 59 | +The second grep finds `@../` — Handlebars data variables from a parent context (e.g. `@../index`, `@../key`, `@../first`, `@../last`). These are special variables set automatically by `{{#each}}`. They follow the same rule as `../`: if `@../index` appears inside a conditional block (`{{#if}}`, `{{#unless}}`, etc.) that is nested inside `{{#each}}`, remove the `../` → `@index`. If it appears directly in the `{{#each}}` body with no conditional wrapper, leave it unchanged. |
| 60 | + |
| 61 | +Review **every hit** in context of the helper it sits inside — some will need fixes, some won't. |
| 62 | + |
| 63 | +### Pattern — `../` inside conditional block helpers (remove the `../`) |
| 64 | + |
| 65 | +Because conditional block helpers (`#if`, `#unless`, `#or`, `#and`, etc.) no longer create a frame, `../` is now one level too high. |
| 66 | + |
| 67 | +**Before (v3)** |
| 68 | +```handlebars |
| 69 | +{{#each items}} |
| 70 | + {{#if isActive}} |
| 71 | + {{../title}} |
| 72 | + {{/if}} |
| 73 | +{{/each}} |
| 74 | +``` |
| 75 | + |
| 76 | +**After (v4)** |
| 77 | +```handlebars |
| 78 | +{{#each items}} |
| 79 | + {{#if isActive}} |
| 80 | + {{title}} |
| 81 | + {{/if}} |
| 82 | +{{/each}} |
| 83 | +``` |
| 84 | + |
| 85 | +--- |
| 86 | + |
| 87 | +> *The required fix for Step 2 is complete. Optionally, continue below to refactor the same locations using block params.* |
| 88 | +
|
| 89 | +### Optional: Refactor to block params |
| 90 | + |
| 91 | +> **Sources:** |
| 92 | +> - Handlebars v4.0.0 release notes — context depth change: [handlebars-lang/handlebars.js — release-notes.md](https://github.com/handlebars-lang/handlebars.js/blob/master/release-notes.md) |
| 93 | +> - Block params syntax reference: [handlebarsjs.com — Block Helpers: Block Parameters](https://handlebarsjs.com/guide/block-helpers.html#block-parameters) |
| 94 | +
|
| 95 | +After removing `../`, the same locations can be further refactored to name the loop item explicitly with block params. This is optional — removing `../` is fully correct on its own. Block params make the intent clearer when a loop body is complex. |
| 96 | + |
| 97 | +**This applies only when** the removed `../` referred to a **property of the loop item itself** (not the parent/root context). |
| 98 | + |
| 99 | +**Before (after mandatory fix):** |
| 100 | +```handlebars |
| 101 | +{{#each products}} |
| 102 | + {{#if isActive}} |
| 103 | + <h2>{{title}}</h2> |
| 104 | + {{/if}} |
| 105 | +{{/each}} |
| 106 | +``` |
| 107 | + |
| 108 | +**After (block params refactor):** |
| 109 | +```handlebars |
| 110 | +{{#each products as |product|}} |
| 111 | + {{#if product.isActive}} |
| 112 | + <h2>{{product.title}}</h2> |
| 113 | + {{/if}} |
| 114 | +{{/each}} |
| 115 | +``` |
| 116 | + |
| 117 | +**Block params do NOT apply** when the original `../` was a deliberate exit to parent/root context — those were left unchanged in the mandatory fix and remain correct as-is. |
| 118 | + |
| 119 | +#### Identify candidates for block params |
| 120 | + |
| 121 | +Use the same files identified during the mandatory fix. For each location where `../` was removed, apply the two-check decision: |
| 122 | + |
| 123 | +**Check 1 — confirm the removal happened inside a conditional block body.** |
| 124 | +Only locations where `../` appeared inside a conditional helper (`{{#if}}`, `{{#unless}}`, `{{#or}}`, `{{#and}}`, `{{#compare}}`, etc.) that is itself inside a `{{#each}}` are candidates. A `../` removed directly from the `{{#each}}` body with no conditional wrapper is not a candidate. |
| 125 | + |
| 126 | +```handlebars |
| 127 | +{{#each items}} |
| 128 | + {{foo}} ← was ../foo directly in #each body — skip, not a candidate |
| 129 | +
|
| 130 | + {{#if condition}} |
| 131 | + {{bar}} ← was ../bar inside conditional block body — candidate, proceed to Check 2 |
| 132 | + {{/if}} |
| 133 | +{{/each}} |
| 134 | +``` |
| 135 | + |
| 136 | +**Check 2 — apply the decision rule** to each location identified in Check 1: |
| 137 | + |
| 138 | +| Question | Answer → Action | |
| 139 | +|---|---| |
| 140 | +| Did the removed `../prop` refer to a field on the **loop item**? | Yes → apply block params | |
| 141 | +| Did the removed `../prop` refer to a field on the **parent/root context**? | Yes → skip, block params do not apply | |
| 142 | + |
| 143 | +> **Universal rule — ask one question:** |
| 144 | +> **"Does every element in this array have this property?"** |
| 145 | +> |
| 146 | +> - **Yes** → it belongs to the loop item → block params apply. |
| 147 | +> *(Examples: a product's `title`, an order's `status`, a variant's `price` — any field that varies per item)* |
| 148 | +> - **No** → it lives outside the array, in a wider context → skip. |
| 149 | +> *(Examples: global config, store settings, page-level data, parameters passed to a partial — anything that is the same regardless of which item you are on)* |
| 150 | +> |
| 151 | +> The property name is not a reliable signal. The question is purely structural: |
| 152 | +> open the page's data object, find the array being iterated, and ask whether |
| 153 | +> the property in question lives inside each element or outside the array entirely. |
| 154 | +
|
| 155 | +#### Offer block params to the user |
| 156 | + |
| 157 | +After identifying candidates, present them to the user and ask: |
| 158 | + |
| 159 | +> "Found N places where block params could be applied. This makes templates more explicit and avoids relying on context depth counting inside conditional blocks. |
| 160 | +> Official reference: [handlebarsjs.com/guide/block-helpers.html#block-parameters](https://handlebarsjs.com/guide/block-helpers.html#block-parameters) |
| 161 | +> |
| 162 | +> Apply block params to these locations? (yes / no — the mandatory fix already applied is sufficient either way)" |
| 163 | +
|
| 164 | +If the user says **yes**, refactor each `{{#each items}}` that has the pattern to `{{#each items as |item|}}` and replace bare `prop` references with `item.prop` inside conditional blocks. |
| 165 | +If the user says **no**, no further changes needed. |
| 166 | + |
| 167 | +--- |
| 168 | + |
| 169 | +## Step 3: Regression testing |
| 170 | + |
| 171 | +### Capture baseline snapshots (before the upgrade) |
| 172 | + |
| 173 | +```bash |
| 174 | +curl -L "https://store.example.com/" > baseline-home.html |
| 175 | +curl -L "https://store.example.com/category/widgets/" > baseline-category.html |
| 176 | +curl -L "https://store.example.com/widgets/widget-1/" > baseline-product.html |
| 177 | +curl -L "https://store.example.com/cart" > baseline-cart.html |
| 178 | +``` |
| 179 | + |
| 180 | +Repeat for all pages at risk: |
| 181 | +- Home page |
| 182 | +- Category page (with pagination) |
| 183 | +- Product page (with options/modifiers) |
| 184 | +- Cart / Checkout |
| 185 | +- Search results |
| 186 | +- Account: login + orders |
| 187 | +- Any custom templates (blog, brand, custom pages) |
| 188 | + |
| 189 | +### Capture candidate snapshots (after the upgrade) |
| 190 | + |
| 191 | +Apply the changes from Steps 1–2, install the candidate theme, then re-run the same curls, |
| 192 | +saving to `candidate-*.html`. |
| 193 | + |
| 194 | +### Diff and triage |
| 195 | + |
| 196 | +```bash |
| 197 | +diff -u baseline-home.html candidate-home.html | less |
| 198 | +``` |
| 199 | + |
| 200 | +Some differences are expected and safe to ignore: |
| 201 | +- Script timestamps / "moment" variables |
| 202 | +- CDN cache-busting query parameters (`?t=...`) |
| 203 | +- Auto-generated IDs |
| 204 | +- Injected bootstrap content |
| 205 | + |
| 206 | +Focus on differences in rendered text, link/image URLs, and template-driven content — these are the areas most affected by v4 changes. |
| 207 | + |
| 208 | +### Visual regression |
| 209 | + |
| 210 | +HTML diffs alone won't catch layout regressions. Take screenshots of the same page set |
| 211 | +(baseline vs candidate) at desktop and mobile viewport sizes and compare side-by-side. |
| 212 | +Pay particular attention to areas driven by `../` depth resolution: navigation menus, product grids, category trees. |
| 213 | + |
| 214 | +--- |
| 215 | + |
| 216 | +## Step 4: Update the changelog (if present) |
| 217 | + |
| 218 | +Check for a changelog file at the repo root: |
| 219 | + |
| 220 | +```bash |
| 221 | +ls CHANGELOG* CHANGES* HISTORY* 2>/dev/null |
| 222 | +``` |
| 223 | + |
| 224 | +Common filenames: `CHANGELOG.md`, `CHANGELOG`, `CHANGES.md`, `HISTORY.md`. |
| 225 | + |
| 226 | +If one exists, add an entry describing the migration. Match the existing format: |
| 227 | + |
| 228 | +- **Keep a Changelog** (uses `## [Unreleased]` / `## [x.y.z]` headings): add under `### Changed` in the `[Unreleased]` section. |
| 229 | +- **Date-based**: add a new entry at the top with today's date. |
| 230 | +- **Freeform**: follow the existing style. |
| 231 | + |
| 232 | +Example entry: |
| 233 | + |
| 234 | +```markdown |
| 235 | +### Changed |
| 236 | +- Migrated template engine from Handlebars v3 to v4 (`"template_engine": "handlebars_v4"` in `config.json`) |
| 237 | +- Removed unnecessary `../` references inside conditional block helpers |
| 238 | +``` |
| 239 | + |
| 240 | +If no changelog file is found, skip this step. |
| 241 | + |
| 242 | +--- |
| 243 | + |
| 244 | +## Quick checklist |
| 245 | + |
| 246 | +- [ ] `config.json` updated: `"template_engine": "handlebars_v4"` |
| 247 | +- [ ] All `../` and `@../` occurrences reviewed |
| 248 | +- [ ] Unnecessary `../` inside conditional helpers (`#if`, `#unless`, `#or`, `#and`, `#compare`, `#inArray`, `#any`) removed |
| 249 | +- [ ] Baseline HTML snapshots captured |
| 250 | +- [ ] Candidate HTML snapshots captured and diffed |
| 251 | +- [ ] Visual regression pass completed on key pages |
| 252 | +- [ ] Changelog updated (if a changelog file exists) |
0 commit comments