Skip to content

feat(core): read variations out of the WooCommerce cart form (phase 5) - #18

Merged
hami9 merged 2 commits into
mainfrom
feat/phase-5-variations
Aug 25, 2026
Merged

feat(core): read variations out of the WooCommerce cart form (phase 5)#18
hami9 merged 2 commits into
mainfrom
feat/phase-5-variations

Conversation

@hami9

@hami9 hami9 commented Aug 25, 2026

Copy link
Copy Markdown
Owner

What and why

Phase 5 has been partial since it was written, and this closes the gap.

Layers A and B already produce variations when the shop answers its own API or publishes a schema.org ProductGroup. Neither is guaranteed. What is effectively guaranteed on a WooCommerce variable product is the add-to-cart form, which carries every variation as JSON in one attribute:

<form class="variations_form" data-product_variations='[{…},{…}]'>

That blob is WC_Product_Variable::get_available_variations() — per-variation price, sale price, SKU, stock, image, weight and dimensions — already in the page, needing no extra request, and surviving a shop with its REST API switched off. Reading it is the difference between exporting a variable product as one priceless row and exporting it correctly.

variations.ts was written in an earlier session and never wired in. This connects it to Layer B, adds the fixture and the tests, and moves phase 5 to done.

The decision worth reviewing

Where the form sits in the reading order. mergeDraftInto deliberately refuses to reconcile two sources' variant lists — two sources listing variations in different orders or granularities cannot be merged without guessing, and a guess duplicates rows in the export. So whichever source is reached first owns the variants outright.

The form goes second, after JSON-LD. That means a page already publishing a ProductGroup keeps behaving exactly as it did, and the form fills the far more common case where JSON-LD described the parent and said nothing about its variations. Putting it first would have silently changed the answer for every page that had both — which is the kind of change that passes CI and shows up as a wrong export weeks later.

The trap it exists to handle

§7.6. The form keys attributes by term slug (attribute_pa_weight: "500g") while the parent's options are term names (۵۰۰ گرم). WooCommerce silently drops every variation on import unless the two match character-for-character, so the <select> is read first as the translation table and the slugs mapped back through it. This is the same trap the Store API adapter hit in phase 3.

How it was verified

npm run check passes. 831 tests, 18 of them new.

Tested at both stages, because they fail differently:

  • Unit (packages/core/test/extract/variations.test.ts) — the reading itself: slug→name mapping, one axis value per variation, per-variation price/SKU/stock, out-of-stock carried as out-of-stock, and the two degraded cases (WooCommerce writing data-product_variations="false" when its template opts out, and unreadable JSON) reported rather than swallowed.
  • Pipeline (packages/exporters/test/pipeline.test.ts) — the CSV, which is where §7.6 actually bites. Parent followed immediately by its own variations (§7.4), linked by SKU and never by ID (§7.2), one attribute value per variation row with every one spelled exactly as the parent lists it (§7.6), parent price cells empty rather than zero (§7.5), attributes local by default (§7.7), every SKU unique.

The fixture is a Persian variable product whose JSON-LD describes only the parent — deliberately, so the tests prove the form fills a real gap rather than a contrived one.

One regression check included explicitly: readVariationForm now runs on every Layer B page, so there is a test asserting a page with no variation form is completely undisturbed.

Note for the reviewer

While writing these I asserted parentSku immediately after extraction and it failed. That was the test being wrong, not the code — §7.3 has assignSkus run at the export step, because it needs to see a whole scan at once to guarantee uniqueness across pages. The test now runs assignSkus first and asserts the link where it is actually made. Worth knowing if you read that test and wonder why it is not simpler.

Checklist

  • The commits follow Conventional Commits — they decide the next version number
  • npm run check passes (format, lint, typecheck, tests)
  • New behaviour has tests
  • No currency unit is inferred, and no field is invented to fill a blank

🤖 Generated with Claude Code

hami9 and others added 2 commits August 25, 2026 11:21
Chrome 151 accepts the flag on the command line, prints no error, and
does nothing. The browser starts and the extension is simply absent.

The reason this is worth a section rather than a footnote is that every
symptom points at a broken build: no toolbar icon, chrome://extensions
listing only Chrome's own component extensions, and the extension's URL
returning ERR_BLOCKED_BY_CLIENT, which reads like it crashed rather than
like it was never loaded. That is half an hour spent debugging a
package that was fine, which is exactly the kind of thing publishing.md
already exists to stop — it is the same lesson as the note above it
about web-ext and AMO's validator disagreeing.

Also records the asymmetry that catches people between the two routes
that do work: loading unpacked in Chrome wants the directory, Firefox's
temporary add-on wants the manifest file inside it.

The consequence worth knowing before phase 15 rather than during it: any
job that wants to drive a browser with this extension pre-loaded — CI,
an end-to-end test, the app work — cannot use a command-line flag and
needs a real automation harness instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Phase 5 has been "partial" since it was written, and this is the gap.
Layers A and B already produce variations when the shop answers its own
API or publishes a `ProductGroup`. Neither is guaranteed. What is
effectively guaranteed on a WooCommerce variable product is the
add-to-cart form, which carries every variation as JSON in one
attribute — `WC_Product_Variable::get_available_variations()`, with
per-variation price, SKU, stock, image, weight and dimensions, already
in the page, needing no extra request and surviving a shop with its REST
API switched off.

`variations.ts` was written earlier and never wired in. This connects it
to Layer B, adds the fixture and the tests, and closes the phase.

**Where it sits in the reading order is the decision worth recording.**
`mergeDraftInto` deliberately refuses to reconcile two sources' variant
lists — two sources listing variations in different orders or
granularities cannot be merged without guessing, and a guess duplicates
rows in the export. So whichever source is reached first owns them
outright. The form goes *second*, after JSON-LD: a page that already
publishes a `ProductGroup` keeps behaving exactly as it did, and the
form fills the far more common case where JSON-LD described the parent
and said nothing about its variations. Putting it first would have
silently changed the answer for every page that had both.

The trap it exists to handle is §7.6. The form keys attributes by term
slug — `500g` — while the parent's options are term names — `۵۰۰ گرم`.
WooCommerce silently drops every variation on import unless the two
match character-for-character, so the `<select>` is read first as the
translation table and the slugs are mapped back through it.

Tests cover both stages, because they fail differently. The unit tests
assert the reading; the pipeline tests assert the CSV, which is where
§7.6 actually bites — parent followed by its own variations, linked by
SKU and never by ID, one attribute value per variation row and every one
of them spelled exactly as the parent lists it, parent price cells empty
rather than zero, and attributes local by default.

18 new tests, 831 total.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@hami9
hami9 merged commit 3765487 into main Aug 25, 2026
1 check passed
@hami9
hami9 deleted the feat/phase-5-variations branch August 25, 2026 07:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant