@bettercms-ai/convert 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +232 -0
- package/README.md +97 -16
- package/dist/cli.js +582 -68
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +51 -4
- package/dist/index.js +564 -64
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,237 @@
|
|
|
1
1
|
# @bettercms-ai/convert
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
The workspace lockfile records this version: a lock still naming 0.3.0 makes a frozen install
|
|
6
|
+
reject the checkout, which is what broke the migrator image on #616.
|
|
7
|
+
|
|
8
|
+
### Minor Changes
|
|
9
|
+
|
|
10
|
+
- Componentize Phase B: an extracted section declares the lane its copy actually lives in.
|
|
11
|
+
|
|
12
|
+
An element carries exactly ONE `data-bcms-field`, and which address it names is now decided
|
|
13
|
+
when the component RENDERS rather than when the codemod ran. A placement bound to a page
|
|
14
|
+
field group declares the page path, as before. A placement that owns its copy — `overrides`
|
|
15
|
+
and no `bind` — declares the block lane, `<blockId>__overrides.<propKey>`, which is where
|
|
16
|
+
the editor commits an instance's own value.
|
|
17
|
+
|
|
18
|
+
🔴 The extracted files are BYTE-IDENTICAL in both copy models. That is the point: moving a
|
|
19
|
+
page's copy onto its instances is a data change on the server, not a second cut of the
|
|
20
|
+
repository, and one component renders a Phase A placement on one page and a Phase B
|
|
21
|
+
instance on the next. Each of P2's bindings is wrapped in the placement's own chooser,
|
|
22
|
+
`bcmsFields({ blockId, bind, overrides }, "<prefix>")`, so the page path is still verbatim
|
|
23
|
+
in the file. The prefix that turns a page path into a prop key comes from the plan's
|
|
24
|
+
`fields[].key`/`path` map, not from the group key.
|
|
25
|
+
|
|
26
|
+
The root's `data-bcms-block` is now REPLACED when the repository already wrote one, instead
|
|
27
|
+
of a second being appended beside it — two instances claiming one element.
|
|
28
|
+
|
|
29
|
+
- `--copy bind|instance` (default `bind`), and the `COPY_MODE_MISMATCH` refusal.
|
|
30
|
+
|
|
31
|
+
An assertion rather than a switch, since the output does not depend on it. It catches the
|
|
32
|
+
one order-of-operations mistake that would otherwise be silent: `--copy instance` run in the
|
|
33
|
+
repository before the server has moved the copy leaves a tree that looks converted and
|
|
34
|
+
renders every section out of page fields the dashboard is about to hide. A plan whose
|
|
35
|
+
placements all still carry `bind` refuses every section by name, with nothing written. The
|
|
36
|
+
receipt carries `copy`, the same word the server's receipt uses.
|
|
37
|
+
|
|
38
|
+
The registry's shipped fallback now omits `bind` for an instance-owned placement, for the
|
|
39
|
+
same reason: binding it would make a fresh clone render a section out of hidden fields.
|
|
40
|
+
|
|
41
|
+
- The extracted-component format is VERSIONED (`section v2`), and older ones are upgraded.
|
|
42
|
+
|
|
43
|
+
🔴 Without this Phase B never reaches a repository that already ran 0.3. Completion is
|
|
44
|
+
decided on the marker and a v1 component carries one, so every already-componentized page
|
|
45
|
+
was reported done, its components kept their page-path bindings, and `--copy instance`
|
|
46
|
+
there placed five sections while moving no address at all. Each v1 component is now brought
|
|
47
|
+
to the current format in place — every `data-bcms-field` wrapped in the chooser, the chooser
|
|
48
|
+
declared and imported, the marker restamped — once, before any page is called done. A
|
|
49
|
+
component the plan says nothing about is left alone; one that cannot be upgraded is
|
|
50
|
+
`SECTION_UNMIGRATABLE` rather than a silent skip.
|
|
51
|
+
|
|
52
|
+
- An already-componentized page's shipped placements are restated from the current plan.
|
|
53
|
+
|
|
54
|
+
A bind run followed by an instance run kept `bind` on every placement in the registry's
|
|
55
|
+
fallback table, so a fresh clone rendered sections out of page fields the dashboard had
|
|
56
|
+
already hidden — and the reverse transition never put `bind` back. The `bind` is now
|
|
57
|
+
restated on every run, in the order the registry already holds; nothing is extracted, and
|
|
58
|
+
the registry is rewritten only when those bytes actually change. Repeated placements of one
|
|
59
|
+
component — two groups sharing a `shapeHash`, which is what `shapeHash` is FOR — are matched
|
|
60
|
+
POSITIONALLY, registry order against plan order per slug: keying them by slug collapsed them,
|
|
61
|
+
so a page with one group still bound and another moved wrote both from the second.
|
|
62
|
+
|
|
63
|
+
- The v1 → v2 upgrade reuses names the file already has, or refuses.
|
|
64
|
+
|
|
65
|
+
These files have been sitting in somebody's repository. Blindly adding
|
|
66
|
+
`import { bcmsFields }` and `const bcmsField = …` to a module that already declares either
|
|
67
|
+
produced a duplicate binding — a file that does not compile — reported as a successful
|
|
68
|
+
upgrade. An import of the helper's own `bcmsFields` is reused under whatever local name it
|
|
69
|
+
has; a `bcmsField` the file demonstrably builds from our helper is left alone and only its
|
|
70
|
+
bindings are wrapped; anything else is `SECTION_UNMIGRATABLE`.
|
|
71
|
+
|
|
72
|
+
🔴 A NAME IS NOT A PROVENANCE. Reusing anything *called* `bcmsFields` trusted an import from
|
|
73
|
+
any module at all — `import { bcmsFields } from "./their-utils"` is a different function with
|
|
74
|
+
the same name — and any coexisting `bcmsField` vouched for itself. The specifier must RESOLVE
|
|
75
|
+
to the helper module this package writes (through the project's own aliases), and `bcmsField`
|
|
76
|
+
must be declared as `const bcmsField = <that exact local>(…)`, read structurally off the
|
|
77
|
+
module's own top-level AST.
|
|
78
|
+
|
|
79
|
+
- Which group an upgraded component was cut from is read off the FILE, not off the plan.
|
|
80
|
+
|
|
81
|
+
One component serves every page whose group has its shape, and its markup carries the paths of
|
|
82
|
+
the one page it was cut FROM. Taking the plan's first placement for that slug gave a component
|
|
83
|
+
reading `hero.*` the prefix `landingHero.` as soon as that placement came first: every wrapped
|
|
84
|
+
address matched nothing, the block lane never fired, and the file was stamped current for good.
|
|
85
|
+
The candidate prefixes are matched against the addresses the file actually declares, and a slug
|
|
86
|
+
whose placements cannot be told apart that way is `SECTION_UNMIGRATABLE`. Server-pending
|
|
87
|
+
placements count as prefix EVIDENCE — which group the bytes were cut from is a fact about the
|
|
88
|
+
past, and a refused placement may be the only thing that explains them — while still being
|
|
89
|
+
excluded from placement and copy-mode decisions.
|
|
90
|
+
|
|
91
|
+
- Fresh extraction allocates collision-free names for the chooser and its import.
|
|
92
|
+
|
|
93
|
+
The markup being cut is the PAGE's, and a page is free to have declared `bcmsField` itself.
|
|
94
|
+
Declaring ours unconditionally — and listing the name as `provided`, so `carryFor` neither
|
|
95
|
+
carried the page's declaration nor reported it free — silently rebound every use of theirs
|
|
96
|
+
inside the extracted section to our chooser. Both names now go through the same allocator as
|
|
97
|
+
every other identifier this codemod introduces, reserved against the module's bindings AND
|
|
98
|
+
against every identifier-shaped token in the bytes being lifted: in a TSX page that declaration
|
|
99
|
+
lives inside the component function, where a module-level check cannot see it, so the capture
|
|
100
|
+
happened anyway. ALL of the names, not the two base ones — reserving only `bcmsField` sent the
|
|
101
|
+
allocator to `bcmsField_1`, which a section referencing both also uses, and the capture simply
|
|
102
|
+
moved one suffix along.
|
|
103
|
+
|
|
104
|
+
- The prop map is checked before this repository is consulted at all, AND against the
|
|
105
|
+
addresses it has to convert.
|
|
106
|
+
|
|
107
|
+
Checking it where the markup is cut let an ALREADY-componentized page bypass it: a later plan
|
|
108
|
+
spelling `{ key: "title", path: "hero.heading" }` was reported complete while the component it
|
|
109
|
+
names went on addressing `overrides.heading`, a key the instance does not hold. The map is a
|
|
110
|
+
property of the plan, so it is now answered from the plan, before completion is even asked.
|
|
111
|
+
|
|
112
|
+
🔴 AND A MAP CAN BE INTERNALLY CONSISTENT AND STILL WRONG ABOUT THE SECTION.
|
|
113
|
+
`{ groupKey: "hero", key: "title", path: "other.title" }` derives one clean prefix, `other.`,
|
|
114
|
+
which matches nothing the markup declares — so every element quietly stayed on the page lane
|
|
115
|
+
while the run reported the section done. The derived prefix is now verified against every
|
|
116
|
+
address the section actually declares — and a field address this cannot read statically
|
|
117
|
+
(`{computedPath}`, `{prefix + ".title"}`) counts as one it cannot convert, because contributing
|
|
118
|
+
nothing to the check is how such an element stayed on the page lane at render time. A
|
|
119
|
+
repeater's template literal is readable and is not affected.
|
|
120
|
+
|
|
121
|
+
- Migration refusals are about ONE component, and a `.tsx` chooser is found where it lives.
|
|
122
|
+
|
|
123
|
+
Keyed by slug alone, an unmigratable `Hero.astro` marked the perfectly good `Hero.tsx`
|
|
124
|
+
unmigratable in a mixed repository — a page refused for a fact about a file its route does not
|
|
125
|
+
render. And a `.tsx` component declares its chooser INSIDE the function, which is where this
|
|
126
|
+
package puts it: a program-level binding check saw nothing there and inserted a second
|
|
127
|
+
`const bcmsField` into the same block. Both scopes are read; the refusal is keyed by dialect
|
|
128
|
+
and slug.
|
|
129
|
+
|
|
130
|
+
Reading a registry back also resolves its extensionless imports to a file its OWN dialect can
|
|
131
|
+
render. Two dialects share a component name whenever they share a slug, and a general resolver
|
|
132
|
+
— whose extension order is this package's, not the project's — bound the `.tsx` registry's
|
|
133
|
+
`hero` to the astro file, after which everything was about the wrong component.
|
|
134
|
+
|
|
135
|
+
- `readRegistry` validates each `SHIPPED` row instead of trusting the array.
|
|
136
|
+
|
|
137
|
+
`[null]` is valid JSON, and the first thing to read it back dereferenced `placement.slug` and
|
|
138
|
+
threw a `TypeError` out of a pure function that names nothing. A row must be an object with a
|
|
139
|
+
string `slug` and, when present, string `bind`/`blockId`; anything else is `REGISTRY_UNREADABLE`,
|
|
140
|
+
which is what every other unreadable row in that file already was — as is a `REGISTRY` naming
|
|
141
|
+
no components (this module is only emitted when there is at least one, so a table with no rows
|
|
142
|
+
is one whose rows were lost) and a `SHIPPED` slug the `REGISTRY` does not name. An EMPTY
|
|
143
|
+
`SHIPPED` is still read: a run whose only page was `SECTION_NOT_CONTIGUOUS` writes the
|
|
144
|
+
components and no order at all, so refusing that would strand the repository. A single page
|
|
145
|
+
whose placement list is EMPTY is refused, though — `every` is vacuously true of it, and the
|
|
146
|
+
fallback exists precisely so a build with no CMS blocks renders the bands the repository ships.
|
|
147
|
+
|
|
148
|
+
- Two more edges: a root carrying SEVERAL `data-bcms-block` attributes keeps exactly one (only
|
|
149
|
+
the first was being rewritten, so the extracted element still came out with two), and a v1
|
|
150
|
+
component with no `data-bcms-field` at all is upgraded on its marker alone rather than refused
|
|
151
|
+
for an ambiguity it cannot have — every candidate prefix matched it vacuously.
|
|
152
|
+
|
|
153
|
+
### Patch Changes
|
|
154
|
+
|
|
155
|
+
- No synthesized group fallback: an extracted component reads `bcmsSection(page, bind, overrides)`.
|
|
156
|
+
|
|
157
|
+
🔴 `bind ?? "<groupKey>"` belonged to Phase A, where every placement was bound and the default
|
|
158
|
+
could only restate what the placement already said. Under Phase B a placement that OWNS its
|
|
159
|
+
copy carries no bind on purpose — and the fallback put the page's field group straight back
|
|
160
|
+
underneath it, so a key the instance had not been given rendered the page's stale copy out of
|
|
161
|
+
fields the dashboard has already hidden. A placement that names no group has none, and every
|
|
162
|
+
`bcms(...)` then renders what the template shipped with. The v1 → v2 upgrade rewrites the old
|
|
163
|
+
form away rather than stamping it as current — through the module's own AST, by offset and
|
|
164
|
+
through the file's OWN local name for the export, because an exact-text rewrite missed a call a
|
|
165
|
+
formatter had reflowed and a name-matched one missed an aliased
|
|
166
|
+
`import { bcmsSection as sectionCopy }`; the marker then said the question had been settled. A
|
|
167
|
+
module with no readable program is refused, and so is one where nothing was rewritten while a
|
|
168
|
+
`??` fallback sits in a call this cannot identify as the helper's.
|
|
169
|
+
|
|
170
|
+
- Only provably safe field expressions count as an address.
|
|
171
|
+
|
|
172
|
+
Starting with a quote is not a proof: `{"hero.title".replace("hero", "footer")}` names
|
|
173
|
+
`footer.title` at render time, and reading its first characters as the address accepted a
|
|
174
|
+
section whose element then quietly stayed on the page lane — the failure the check exists to
|
|
175
|
+
catch, reached through the check itself. Two shapes are provable: a string literal and NOTHING
|
|
176
|
+
else, and a template literal whose every interpolation is an INDEX (`[${i}]`, what a repeater
|
|
177
|
+
writes, which cannot change the prefix). Everything else — `{computedPath}`, `{"a" + x}`,
|
|
178
|
+
`` {`${prefix}.title`} `` — is opaque, and opaque is `PROP_MAP_INCONSISTENT`.
|
|
179
|
+
|
|
180
|
+
- `COPY_MODE_MISMATCH` ignores sections the SERVER already refused.
|
|
181
|
+
|
|
182
|
+
A `copy: "instance"` section carrying `pending` is not a placement this run would make, but it
|
|
183
|
+
counted as evidence that the plan had moved its copy — so `--copy instance` passed the gate on
|
|
184
|
+
a plan whose only instance-owned section was server-pending, and the run wrote the BIND
|
|
185
|
+
conversion for the sections it could act on. Which is the exact tree the flag exists to stop.
|
|
186
|
+
|
|
187
|
+
- A bound address is RE-BASED onto its own placement's group.
|
|
188
|
+
|
|
189
|
+
The markup is cut from ONE page and carries that page's paths, and the whole point of
|
|
190
|
+
`shapeHash` is that a second page renders the same component bound to a group of its own.
|
|
191
|
+
Handing the embedded path through unchanged declared `hero.title` on a page whose field is
|
|
192
|
+
`landingHero.title` — an address that page does not have, so the editor wrote into nothing
|
|
193
|
+
and the meter counted a binding that was never there.
|
|
194
|
+
|
|
195
|
+
- `INLINE_NEEDS_INSTANCE`: a page that cannot be looped cannot own its copy.
|
|
196
|
+
|
|
197
|
+
The in-place call site — `<Hero bind="hero" page={pageSnapshot("home")} />` — is written by
|
|
198
|
+
the codemod from the plan and carries no `blockId` and no `overrides`; it never consults the
|
|
199
|
+
CMS's blocks at all, which is exactly what makes the inline form componentisation *without*
|
|
200
|
+
the page builder. Emitting it for a placement whose copy has MOVED rendered the page's field
|
|
201
|
+
group and declared the page lane for an address that no longer holds anything. The component
|
|
202
|
+
is still written; the page is left as its author wrote it.
|
|
203
|
+
|
|
204
|
+
- `HELPER_CONFLICT` and `COPY_MODE_MISMATCH` now return an EMPTY file list.
|
|
205
|
+
|
|
206
|
+
On an already-componentized repository the registry read-back seeds the component table, so
|
|
207
|
+
the emit fired even though every section was pending: the helper was upgraded, the sections
|
|
208
|
+
library rebuilt and the registry rewritten for a run the operator was told did nothing. With
|
|
209
|
+
a hand-edited helper it was worse — the v1 → v2 upgrade rewrote components to
|
|
210
|
+
`import { bcmsFields }` from a module that does not export it. A refusal that modifies the
|
|
211
|
+
tree is not a refusal, so both return before reading or emitting any generated artefact.
|
|
212
|
+
|
|
213
|
+
- Two new refusals where the codemod used to guess.
|
|
214
|
+
|
|
215
|
+
`PROP_MAP_INCONSISTENT`: a plan whose `fields[].path` is not one common prefix plus its
|
|
216
|
+
`key` — `{ path: "hero.heading", key: "title" }` — fell back to `<groupKey>.`, so the block
|
|
217
|
+
address became `overrides.heading` for a prop the instance holds as `title`, and every edit
|
|
218
|
+
went to a key nothing reads. `DUPLICATE_FIELD_BINDING`: an element already carrying two
|
|
219
|
+
`data-bcms-field` had both rewritten, leaving the extracted element still violating the
|
|
220
|
+
one-attribute invariant.
|
|
221
|
+
|
|
222
|
+
- Helper v4: `bcmsSection` merges OVERRIDES-FIRST, per key.
|
|
223
|
+
|
|
224
|
+
`overrides[key] ?? group[key]` rather than "overrides, or else the group". The case that
|
|
225
|
+
matters is the one in between — a section whose copy is half moved — where a key the
|
|
226
|
+
instance does not hold has to keep rendering the page's value rather than nothing. `??`,
|
|
227
|
+
never `||`: an empty string is a value an editor typed. A table prop stored in the shape
|
|
228
|
+
the dashboard edits (`{ rows: [...] }`) is unwrapped to its list, so `bcmsRows` sees an
|
|
229
|
+
array instead of falling back to the rows the template shipped with.
|
|
230
|
+
|
|
231
|
+
New export `bcmsFields`, which is what an extracted section's bindings read through. An
|
|
232
|
+
existing v1–v3 helper is upgraded in place as usual; one this package did not write is
|
|
233
|
+
still `HELPER_CONFLICT`.
|
|
234
|
+
|
|
3
235
|
## 0.3.0
|
|
4
236
|
|
|
5
237
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -112,7 +112,14 @@ query cannot reach a chrome marker. A value that lives in an ATTRIBUTE (a promot
|
|
|
112
112
|
`href`) has no field attribute of its own and rides `data-bcms-props="<address>|<kind>|<attr>;…"` —
|
|
113
113
|
where `<address>` carries the same `layout:` prefix when it is chrome. A section root additionally
|
|
114
114
|
carries `data-bcms-block="<blockId>"`, which is what the editor hit-tests for section chrome and
|
|
115
|
-
reorder
|
|
115
|
+
reorder — and inside an extracted section that root's own placement decides which lane its elements
|
|
116
|
+
address: the page path while the copy is the page's, `data-bcms-field="<blockId>__overrides.<key>"`
|
|
117
|
+
once the copy is the instance's. The page-lane address is re-based onto **that placement's** group:
|
|
118
|
+
the markup was cut from one page and carries its paths, and a second page renders the same
|
|
119
|
+
component bound to a group of its own, where `hero.title` is a field that does not exist.
|
|
120
|
+
**One `data-bcms-field` per element, always.** Two would leave the publisher and the editor reading
|
|
121
|
+
different attributes off one node, and which of them won would depend on the dialect — JSX takes
|
|
122
|
+
the last, Astro the first.
|
|
116
123
|
|
|
117
124
|
## As a library
|
|
118
125
|
|
|
@@ -173,8 +180,10 @@ left alone. A repeater on a dynamic route passes the same parameter to `bcmsRows
|
|
|
173
180
|
## Componentize
|
|
174
181
|
|
|
175
182
|
The second mode. P2 above makes a page's copy editable in place; `--componentize` makes its
|
|
176
|
-
SECTIONS movable — each band becomes a component the CMS can reorder, duplicate and swap
|
|
177
|
-
copy
|
|
183
|
+
SECTIONS movable — each band becomes a component the CMS can reorder, duplicate and swap. Where the
|
|
184
|
+
copy lives is a separate question, and the plan answers it: **Phase A** leaves it in the page's
|
|
185
|
+
field groups (`props.bind`), **Phase B** moves it onto the instance (`props.overrides`). The same
|
|
186
|
+
extracted files serve both.
|
|
178
187
|
|
|
179
188
|
The division of labour is the one that already works by hand: **the CMS owns STRUCTURE, the repo
|
|
180
189
|
owns RENDERING.** The server decides which field groups are sections (`get_componentize_plan` →
|
|
@@ -196,20 +205,45 @@ modify src/pages/index.astro
|
|
|
196
205
|
modify bcms-content/home.json
|
|
197
206
|
add bcms-content/components.json
|
|
198
207
|
|
|
199
|
-
5 sections extracted (0 registered, 0 inline-only, 0 already extracted, 0 pending)
|
|
208
|
+
5 sections extracted (0 registered, 0 inline-only, 0 already extracted, 0 pending), copy: bind
|
|
200
209
|
```
|
|
201
210
|
|
|
202
211
|
`--plan <file>` is required and is only read in this mode. `--dry-run`, `--receipt`, `--strict` and
|
|
203
212
|
`--overwrite-helper` mean what they mean above; `--strict` exits 2 on any pending SECTION.
|
|
204
213
|
|
|
214
|
+
`--copy bind|instance` (default `bind`) says which copy model the plan is expected to describe. It
|
|
215
|
+
is an **assertion, not a switch** — the extracted files are byte-identical either way — and it
|
|
216
|
+
exists to catch one order-of-operations mistake that would otherwise be silent: running
|
|
217
|
+
`--copy instance` in the repository BEFORE the server has moved the copy leaves a tree that looks
|
|
218
|
+
converted and renders every section out of page fields the dashboard is about to hide. A plan whose
|
|
219
|
+
placements all still carry `bind` says so, and every section is refused `COPY_MODE_MISMATCH` with
|
|
220
|
+
nothing written. The receipt carries the mode as `copy`, the same word the server's receipt uses.
|
|
221
|
+
|
|
205
222
|
### What is written
|
|
206
223
|
|
|
207
224
|
- **`src/components/bcms/<Name>.<astro|tsx>`** — the section root's markup **verbatim**, P2's
|
|
208
|
-
`
|
|
209
|
-
|
|
210
|
-
`bcmsSection(page, bind, overrides)` in the
|
|
211
|
-
|
|
212
|
-
|
|
225
|
+
`bcms(...)` reads and its page paths included, plus `data-bcms-block={blockId}` on the root (the
|
|
226
|
+
editor hit-tests that for section chrome and reorder; a root that already carried one has it
|
|
227
|
+
REPLACED — and any further ones removed — never joined). Its copy comes from `bcmsSection(page, bind, overrides)` in the
|
|
228
|
+
committed helper — `overrides[key] ?? pageGroup(bind)[key] ??` the fallbacks the template already
|
|
229
|
+
carries. **The placement's own `bind`, with no synthesized default:** `bind ?? "<groupKey>"`
|
|
230
|
+
belonged to Phase A, where every placement was bound and the default could only restate what the
|
|
231
|
+
placement already said; under Phase B it put the page's field group back underneath an instance
|
|
232
|
+
that owns its copy, so a key the instance had not been given rendered stale page copy out of
|
|
233
|
+
fields the dashboard has already hidden. A placement that names no group has none, and every
|
|
234
|
+
`bcms(...)` renders what the template shipped with — and each of P2's bindings is wrapped in the placement's own chooser,
|
|
235
|
+
`bcmsFields({ blockId, bind, overrides }, "<prefix>")`, so the page path is still verbatim in the
|
|
236
|
+
file and the ADDRESS is decided when the component renders. Both names it introduces are
|
|
237
|
+
allocated (`bcmsField_1`, and so on) against the page's own top-level bindings **and against
|
|
238
|
+
every identifier-shaped token in the bytes being lifted** — all of them, since a section using
|
|
239
|
+
`bcmsField` *and* `bcmsField_1` would otherwise just move the capture one suffix along — because
|
|
240
|
+
a page that declared `bcmsField` would
|
|
241
|
+
otherwise have every use of theirs inside the extracted section silently rebound to our chooser
|
|
242
|
+
— and in a TSX page that declaration lives inside the component function, where a module-level
|
|
243
|
+
check cannot see it. Reserving the name puts the reference back on the honest path, which is
|
|
244
|
+
`SECTION_FREE_IDENTIFIERS` naming the identifier that could not travel. One component serves every page whose group has the same shape,
|
|
245
|
+
which is what the plan's `shapeHash` is for — and one component serves both copy models, which is
|
|
246
|
+
what makes moving a page's copy a data change rather than another cut.
|
|
213
247
|
- **`src/lib/bcms-sections.ts`** — `pageSections(pageSlug)` over the committed snapshots'
|
|
214
248
|
`blocks` plus `bcms-content/components.json`, returning `[{ blockId, slug, bind, overrides }]`.
|
|
215
249
|
An instance naming a component this repo does not have is **skipped, never a hole**.
|
|
@@ -219,7 +253,10 @@ add bcms-content/components.json
|
|
|
219
253
|
look like, so rendering nothing there would blank the site on the first build after the codemod
|
|
220
254
|
ran, with nothing in the diff to explain it. The fallback is decided on the **raw** block list:
|
|
221
255
|
a page whose blocks all name components this repo does not have has still been *arranged*, and
|
|
222
|
-
putting the shipped bands back would undo an editor's work on the next build.
|
|
256
|
+
putting the shipped bands back would undo an editor's work on the next build. A shipped placement
|
|
257
|
+
carries `bind` only while the plan says its copy is the page's: an instance-owned one has none,
|
|
258
|
+
because binding it there would make a fresh clone render a section out of fields the dashboard
|
|
259
|
+
has already hidden.
|
|
223
260
|
- **Nothing at all, for a section the repository has ALREADY componentised.** A page that renders
|
|
224
261
|
`<Hero />` from a file it imports has made this cut itself: the existing file is REGISTERED under
|
|
225
262
|
the section's slug — the registry imports it where it lives, **in the shape its module actually
|
|
@@ -284,11 +321,16 @@ import travels as `import * as X`, not as a named one.
|
|
|
284
321
|
| `SECTION_FREE_IDENTIFIERS` | the markup uses a name the component file cannot be given. The identifier is named in the message. |
|
|
285
322
|
| `COMPONENT_CONFLICT` | a file already sits at `src/components/bcms/<Name>` and this package did not write it. Decided on our marker, never on the path — that is a name a person may reasonably have chosen first. |
|
|
286
323
|
| `REGISTRY_CONFLICT` | the same, for `Sections.astro`/`Sections.tsx` or `src/lib/bcms-sections.ts`. Both are rebuilt whole on every run, so an unmarked file there would be destroyed. `--overwrite-helper` proceeds. |
|
|
287
|
-
| `REGISTRY_UNREADABLE` | the registry IS ours and its `REGISTRY`/`SHIPPED` tables cannot be read back — a component it names is GONE from the tree, a row is not a property this can read, or the tables are missing. Every property is parsed structurally and all of them must convert; a partial read is what silently drops a page's bands. It is rebuilt whole every run, so re-emitting it from what this run happened to see would blank those pages. No flag overrides this one: deleting the file is a decision only a person can take. |
|
|
324
|
+
| `REGISTRY_UNREADABLE` | the registry IS ours and its `REGISTRY`/`SHIPPED` tables cannot be read back — a `REGISTRY` naming no components at all (this module is only emitted when there is at least one, so a table with no rows is one whose rows were lost), a `SHIPPED` row that is not an object with a string `slug` (and string `bind`/`blockId` when present), a page whose placement list is EMPTY (a row is only written for a page that has some, and the fallback rendering that page blank is the worst thing this could accept), or a `SHIPPED` slug the `REGISTRY` does not name. `[null]` is valid JSON and threw a `TypeError` out of a pure function the first time anything dereferenced it. An EMPTY `SHIPPED` is read, deliberately: a run whose only page was `SECTION_NOT_CONTIGUOUS` writes the components and no order at all, so refusing that would strand the repository — a component it names is GONE from the tree, a row is not a property this can read, or the tables are missing. Every property is parsed structurally and all of them must convert; a partial read is what silently drops a page's bands. It is rebuilt whole every run, so re-emitting it from what this run happened to see would blank those pages. No flag overrides this one: deleting the file is a decision only a person can take. |
|
|
288
325
|
| `ROUTE_FILE_AMBIGUOUS` | two files serve one route. |
|
|
289
326
|
| `DIALECT_UNSUPPORTED` | the page is svelte, vue or html. Nothing is written for it. |
|
|
290
|
-
| `HELPER_CONFLICT` | `src/bcms-content.ts` is a module this package did not write. `--overwrite-helper` proceeds. |
|
|
327
|
+
| `HELPER_CONFLICT` | `src/bcms-content.ts` is a module this package did not write. Refused **for the whole run, with an empty file list**: `bcmsSection` and `bcmsFields` are what every extracted *and* every upgraded component reads through, so pending it per page while the rest of the run proceeded rewrote v1 components to import an export the helper does not provide. `--overwrite-helper` proceeds. |
|
|
291
328
|
| `SNAPSHOT_INVALID` | `bcms-content/<page>.json` is not JSON this can add `blocks` to. Refused before a byte is written for that route. |
|
|
329
|
+
| `COPY_MODE_MISMATCH` | `--copy instance` was asked of a plan whose placements are all still bound to page field groups. Refused before this repository is even opened — **the run returns an empty file list**, because a refusal that upgrades the helper and rewrites the registry is not a refusal. The message says to run `componentize_sections { copy: "instance" }` on the server first. The reverse pairing is NOT a refusal: `--copy bind` over an instance plan still produces a tree that renders, so refusing it would only stop a run that works. |
|
|
330
|
+
| `PROP_MAP_INCONSISTENT` | the plan's `fields[].path` is not `<one common prefix> + fields[].key` — `{ path: "hero.heading", key: "title" }`, or two fields that disagree about the prefix. A page path cannot be turned into a prop key losslessly, so the block-lane address would name a key the instance does not hold; falling back to `<groupKey>.` sent every edit to something nothing reads. Checked **before this repository is consulted at all**, so an already-componentized page is held to it too — doing it where the markup is cut reported such a page complete while its component went on addressing the wrong key. And checked again against the addresses it has to CONVERT: `{ groupKey: "hero", key: "title", path: "other.title" }` is internally consistent and wrong about the section, whose markup declares `hero.title` — a prefix that converts none of what the section actually declares would leave every element on the page lane whatever the placement said. A field address this cannot read statically counts as one it cannot convert, and is named in the message. Only two shapes are provable — a string literal and NOTHING else, and a template literal whose every interpolation is a LONE IDENTIFIER used as an index (`[${i}]`, what a repeater writes, which cannot change the prefix; `[${getKey()}]` names whatever that returns and is not). `{computedPath}`, `{"a" + x}` and `{"hero.title".replace("hero","footer")}` are all opaque: starting with a quote is not a proof, since that last one names `footer.title` at render time. A field carrying only one of the two says nothing about the mapping and is skipped, not refused: a Phase A plan spells no `key` at all. |
|
|
331
|
+
| `DUPLICATE_FIELD_BINDING` | one element in the section carries more than one `data-bcms-field`. There is no single address to rewrite, and rewriting both leaves the extracted element still violating the one-attribute invariant. |
|
|
332
|
+
| `INLINE_NEEDS_INSTANCE` | the page cannot be looped and the placement's copy has MOVED. The in-place call site the codemod writes — `<Hero bind="hero" page={pageSnapshot("home")} />` — comes from the plan and carries no `blockId` and no `overrides`: it never consults the CMS's blocks at all, which is what makes the inline form componentisation *without* the page builder. Emitting it would render the page's field group (the copy the dashboard is about to hide) and declare the page lane for an address that no longer holds anything. The component is still written; the page is left as its author wrote it. |
|
|
333
|
+
| `SECTION_UNMIGRATABLE` | a component an OLDER version of this package wrote cannot be brought to the current format: it no longer parses, it no longer declares its placement, its element carries two field attributes, **the prefix its markup was cut with cannot be recovered** (see below), or it already binds `bcmsField`/`bcmsFields` to something this package did not write — adding a second declaration is a file that does not compile, reported as a successful upgrade. A name is not a provenance: an import is reused only when its specifier RESOLVES to the helper module this package writes, and a `bcmsField` only when it is declared as `const bcmsField = <that exact local>(…)` — read structurally, in the module scope **and** in the component function's own body, which is where a `.tsx` component declares it. It is left exactly as it is, and the section is reported rather than counted `alreadyExtracted` on a format it is not on. |
|
|
292
334
|
| `NOT_IN_SOURCE` | no file we were given renders the group's fields, or the route names no page file. |
|
|
293
335
|
|
|
294
336
|
A plan spanning TWO frameworks gets two of everything that is dialect-shaped: its own components,
|
|
@@ -302,6 +344,41 @@ Completion is decided per ROUTE, from that page's own bytes — it imports our r
|
|
|
302
344
|
somewhere carries our marker", which is true of every group that has ever been extracted and left a
|
|
303
345
|
brand-new page untouched.
|
|
304
346
|
|
|
347
|
+
**The marker carries a FORMAT VERSION, and anything older is upgraded before it is called done.**
|
|
348
|
+
A 0.3 component (`section v1`: page-path bindings, no render-time lane) carries a marker too, so
|
|
349
|
+
reading the marker alone reported every already-componentized page finished and left its components
|
|
350
|
+
on the page lane forever — `--copy instance` there placed five sections and moved no address at
|
|
351
|
+
all. Each older component is instead brought to the current format in place: every
|
|
352
|
+
`data-bcms-field` wrapped in the chooser, the chooser declared and imported, the marker restamped.
|
|
353
|
+
The Phase A `bind ?? "<group>"` fallback is rewritten away in the same pass — through the module's own AST, by offset and through the file's OWN local name for the helper's export, so neither a call a formatter has reflowed nor one made through an aliased import is missed. A module with no readable program is refused, and so is one where nothing was rewritten while a `??` fallback sits in a call this cannot identify as the helper's. An upgrade never leaves that behaviour under a current marker. It happens once; the next run sees the current version and writes nothing. A component that binds nothing needs no prefix and cannot be ambiguous about one — the marker is its whole upgrade. A component the plan
|
|
354
|
+
says nothing about is left alone — nothing asked about it, and a v1 file is still a correct Phase A
|
|
355
|
+
component. One that cannot be upgraded is `SECTION_UNMIGRATABLE`, never silently skipped — and that refusal is about ONE component, keyed
|
|
356
|
+
by dialect as well as slug, so an unmigratable `Hero.astro` says nothing about the `Hero.tsx`
|
|
357
|
+
beside it. (Each registry also resolves its own extensionless imports to a file its dialect can
|
|
358
|
+
render, which is what makes two same-named components in one directory readable at all.)
|
|
359
|
+
|
|
360
|
+
**Which group an upgraded component was cut from is read off the FILE, not off the plan.** One
|
|
361
|
+
component serves every page whose group has its shape, and its markup carries the paths of the one
|
|
362
|
+
page it was cut from; taking the plan's first placement for that slug gave a component reading
|
|
363
|
+
`hero.*` the prefix `landingHero.` as soon as that placement came first, so every wrapped address
|
|
364
|
+
matched nothing, the block lane never fired, and the file was stamped current for good. The
|
|
365
|
+
candidate prefixes — one per placement of the slug, **server-pending ones included**, because which
|
|
366
|
+
group the bytes were cut from is a fact about the past that a refused placement may be the only
|
|
367
|
+
thing to explain — are matched against the addresses the file actually declares (a repeater's contributes everything before its first interpolation), and a slug
|
|
368
|
+
whose placements cannot be told apart that way is refused rather than guessed at.
|
|
369
|
+
|
|
370
|
+
**An already-componentized page still has a shipped entry, and the plan may have moved its copy.**
|
|
371
|
+
Its placements' `bind` is restated from the current plan on every run — in the order the registry
|
|
372
|
+
already holds, since a page whose markup was cut has no roots left to read an order off — so a bind
|
|
373
|
+
run followed by an instance run drops `bind` from the fallback table and the reverse puts it back.
|
|
374
|
+
Repeated placements of one component (two groups sharing a `shapeHash`, which is what `shapeHash` is
|
|
375
|
+
FOR) are matched **positionally**, registry order against plan order per slug: keying them by slug
|
|
376
|
+
collapsed them, and a page with one group still bound and another moved wrote both from the second.
|
|
377
|
+
Every plan occurrence holds its slot whether or not this run can act on it — one it cannot (refused,
|
|
378
|
+
server-pending) consumes its position and keeps the earlier record, because a missed update is
|
|
379
|
+
recoverable and a wrong one is a page rendering someone else's copy.
|
|
380
|
+
Nothing is extracted there, and the registry is rewritten only if those bytes actually change.
|
|
381
|
+
|
|
305
382
|
The registry and the sections library are rebuilt WHOLE each run, so they are first read back:
|
|
306
383
|
components an earlier run wrote are reused rather than rewritten, and the shipped order of every
|
|
307
384
|
page already served is carried forward. Without that, componentizing one new page emitted a
|
|
@@ -316,7 +393,8 @@ it happens to see.
|
|
|
316
393
|
|
|
317
394
|
A run whose roots DO share a container but have a foreign sibling between them is not refused: each
|
|
318
395
|
root becomes its own `<Hero …/>` where its markup was, counted `inlineOnly`. The markup is
|
|
319
|
-
componentised and the ORDER stays the repository's — looping that page would move the sibling too.
|
|
396
|
+
componentised and the ORDER stays the repository's — looping that page would move the sibling too. A placement whose copy the plan has moved cannot take that form and is
|
|
397
|
+
`INLINE_NEEDS_INSTANCE`: the in-place call site carries no `blockId` and no `overrides`.
|
|
320
398
|
|
|
321
399
|
The arithmetic is checked rather than hoped for:
|
|
322
400
|
|
|
@@ -342,6 +420,9 @@ and removing them would mean editing statements no section asked about.
|
|
|
342
420
|
bind their own copy; only `findPropTargets` is jsx/astro-only, so a drilled prop there is
|
|
343
421
|
`PROP_TARGET_NOT_FOUND` rather than a wrong edit).
|
|
344
422
|
|
|
345
|
-
Componentize: svelte and vue extraction
|
|
346
|
-
|
|
347
|
-
|
|
423
|
+
Componentize: svelte and vue extraction. Phase B — copy owned by the INSTANCE — is here for both
|
|
424
|
+
dialects this lane extracts: the block-lane address is chosen at render time, so moving a page's
|
|
425
|
+
copy onto its instances is a data change rather than another cut of the repository. What Phase B
|
|
426
|
+
does NOT reach is the `inlineOnly` shape: a page whose roots could not be looped keeps calling
|
|
427
|
+
`<Hero bind="hero" …/>` in place, which never consults the CMS's blocks at all, so its copy stays
|
|
428
|
+
the page's whatever the plan says.
|