Skip to content

Commit 8486546

Browse files
committed
docs: TRAC-507 - add Handlebars v3 to v4 migration agent skill
Add a publishable Agent Skill (skills/handlebars-v4-migration) guiding migration of a Stencil theme from Handlebars v3 to v4: config.json template_engine flag, fixing ../ context depth in conditional helpers, regression testing, and changelog. Installable via the skills CLI (npx skills add bigcommerce/stencil-cli) and surfaced on skills.sh. Refs TRAC-507
1 parent 05cc17d commit 8486546

1 file changed

Lines changed: 252 additions & 0 deletions

File tree

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
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

Comments
 (0)