@bettercms-ai/convert 0.1.0 → 0.2.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 ADDED
@@ -0,0 +1,90 @@
1
+ # @bettercms-ai/convert
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - `--componentize`: every derived SECTION becomes a component the CMS can reorder.
8
+
9
+ The repo half of the componentize lane. Given the brief and the plan from
10
+ `get_componentize_plan`, it takes each section's lowest common ancestor in the page's
11
+ tier-1 AST as the section ROOT and lifts it into
12
+ `src/components/bcms/<Name>.<astro|tsx>` — markup verbatim, P2's `data-bcms-field`
13
+ and `bcms(...)` reads included, plus `data-bcms-block={blockId}` for the editor to
14
+ hit-test. The page's contiguous run of roots becomes one `<Sections page="<slug>" />`.
15
+
16
+ `src/lib/bcms-sections.ts` reads `blocks` off the committed snapshots and
17
+ `bcms-content/components.json`; `Sections.<astro|tsx>` is the registry. **When the CMS
18
+ list is empty the registry renders the order the repository shipped** — the committed
19
+ stub, a fresh clone and every not-yet-componentised page all look like that, and
20
+ rendering nothing there would blank the site on the first build.
21
+
22
+ Which file a route is comes from the ROUTER's convention, not from a search for the field
23
+ group: `hero` is on every landing page, and picking the file by hit count let one route's
24
+ codemod rewrite another route's page. Everything the markup uses travels with it — the
25
+ page's imports, re-based, and its module-scope declarations, transitively.
26
+
27
+ Every refusal has a name — `SECTION_ROOT_AMBIGUOUS`, `SECTION_NOT_CONTIGUOUS`,
28
+ `SECTION_FREE_IDENTIFIERS`, `COMPONENT_CONFLICT`, `REGISTRY_CONFLICT`, `ROUTE_FILE_AMBIGUOUS`,
29
+ `DIALECT_UNSUPPORTED`, `HELPER_CONFLICT`, `SNAPSHOT_INVALID`, `NOT_IN_SOURCE` — and the
30
+ arithmetic is asserted:
31
+ `plan sections === extracted + inlineOnly + alreadyExtracted + pending.length`.
32
+
33
+ Re-running is safe in both directions: completion is decided per ROUTE from that page's own
34
+ bytes, and the registry and sections library are read back before being rebuilt, so a run that
35
+ componentizes one new page reuses the existing components and keeps every other page's shipped
36
+ order.
37
+ A second run over an already-componentised tree writes nothing and reports
38
+ `extracted: 0, pending: []`, recognising its own output by a marker comment rather than
39
+ by a path.
40
+
41
+ jsx and astro only. svelte, vue and html pages are `DIALECT_UNSUPPORTED` and nothing is
42
+ written for them.
43
+
44
+ - Helper v3: `bcmsSection(page, bind, overrides)` — `overrides ?? pageGroup(bind) ??` the
45
+ fallbacks the template already carries. An existing v1 or v2 helper this package wrote
46
+ upgrades in place; anything else is still `HELPER_CONFLICT`. `overrides` wins whenever it is a
47
+ record, an EMPTY one included: `{}` is a section an editor cleared, and treating it as absent
48
+ put the page's copy back into it.
49
+
50
+ - A plan spanning two frameworks gets its own components, shipped-order table and `Sections` per
51
+ dialect. One registry would import an `.astro` component into a `.tsx` module, and the second
52
+ dialect's pages would import a `<Sections>` nothing wrote.
53
+
54
+ - **The brief and the plan are validated at ingress.** A page slug becomes
55
+ `bcms-content/<slug>.json`, so `../package` named the repository's own manifest — from inside
56
+ `--root`, where the CLI's boundary check cannot see it. Slugs, group keys and proposed slugs must
57
+ each be one safe path segment, routes must be absolute URL paths, and every consumed field is
58
+ typed; a failure refuses the whole run with `BRIEF_INVALID` / `PLAN_INVALID` before any write.
59
+ `convertSources` checks the same brief for the same reason.
60
+
61
+ ### Patch Changes
62
+
63
+ - Route matching reads each FILE's route rather than generating candidate paths, so Next route
64
+ groups (`app/(marketing)/about/page.tsx`) and parallel-route segments resolve; two files serving
65
+ one route is `ROUTE_FILE_AMBIGUOUS` rather than a silent pick.
66
+
67
+ - `importsIn` distinguishes default, named and NAMESPACE imports. `import * as Icons` was recorded
68
+ as a named one, so a lane re-emitting it wrote `import { Icons }` — a binding nobody exports.
69
+
70
+ - Astro shorthand and spread attributes carry an expression range on the neutral tree. The compiler
71
+ reports `{title}` and `{...props}` at the identifier, so a reader looking for the first `{` inside
72
+ the attribute's span found none and treated the attribute as holding no code.
73
+
74
+ - The CLI's snapshot read treats only ENOENT as "there are none". Every other failure was read as an
75
+ empty directory, and the run then wrote a stub over content it had been unable to open.
76
+
77
+ - The astro parser remaps `@astrojs/compiler`'s **byte** offsets to UTF-16 units. The
78
+ compiler is a Go program behind WebAssembly and counts bytes, so one em dash in a page's
79
+ copy shifted every offset after it by two: elements downstream stopped matching their own
80
+ closing tag, lost their `inner`, and the values after that dash silently did not convert.
81
+
82
+ - Every parser now reports an element's `end` on the neutral tree, so a subtree can be
83
+ lifted out of a page without re-deriving where it stops.
84
+
85
+ ## 0.1.0
86
+
87
+ ### Minor Changes
88
+
89
+ - First release. The P2 codemod: an imported site's text-matched bindings become declared
90
+ ones, in one `magic-string` pass per file, with a receipt of everything it could not do.
package/README.md CHANGED
@@ -116,8 +116,153 @@ reading `.slug` off one is `undefined` on every request. A `"use client"` page c
116
116
  and a signature this cannot edit is not guessed at: both are `DYNAMIC_PARAMS_UNAVAILABLE` and are
117
117
  left alone. A repeater on a dynamic route passes the same parameter to `bcmsRows`.
118
118
 
119
+ ## Componentize
120
+
121
+ The second mode. P2 above makes a page's copy editable in place; `--componentize` makes its
122
+ SECTIONS movable — each band becomes a component the CMS can reorder, duplicate and swap, while its
123
+ copy stays exactly where P2 put it.
124
+
125
+ The division of labour is the one that already works by hand: **the CMS owns STRUCTURE, the repo
126
+ owns RENDERING.** The server decides which field groups are sections (`get_componentize_plan` →
127
+ `componentize_sections`); this decides which bytes render them.
128
+
129
+ ```bash
130
+ # after get_componentize_plan and componentize_sections
131
+ npx @bettercms-ai/convert --componentize --brief brief.json --plan componentize.json --root .
132
+ git diff
133
+ ```
134
+
135
+ ```
136
+ add src/components/bcms/Hero.astro
137
+ add src/components/bcms/Features.astro
138
+ add src/components/bcms/FAQ.astro
139
+ add src/components/bcms/Sections.astro
140
+ add src/lib/bcms-sections.ts
141
+ modify src/pages/index.astro
142
+ modify bcms-content/home.json
143
+ add bcms-content/components.json
144
+
145
+ 5 sections extracted (0 inline-only, 0 already extracted, 0 pending)
146
+ ```
147
+
148
+ `--plan <file>` is required and is only read in this mode. `--dry-run`, `--receipt`, `--strict` and
149
+ `--overwrite-helper` mean what they mean above; `--strict` exits 2 on any pending SECTION.
150
+
151
+ ### What is written
152
+
153
+ - **`src/components/bcms/<Name>.<astro|tsx>`** — the section root's markup **verbatim**, P2's
154
+ `data-bcms-field` and `bcms(...)` reads included, plus `data-bcms-block={blockId}` on the root
155
+ (the editor hit-tests that for section chrome and reorder). Its copy comes from a new
156
+ `bcmsSection(page, bind, overrides)` in the committed helper: `overrides ?? pageGroup(bind) ??`
157
+ the fallbacks the template already carries. One component serves every page whose group has the
158
+ same shape, which is what the plan's `shapeHash` is for.
159
+ - **`src/lib/bcms-sections.ts`** — `pageSections(pageSlug)` over the committed snapshots'
160
+ `blocks` plus `bcms-content/components.json`, returning `[{ blockId, slug, bind, overrides }]`.
161
+ An instance naming a component this repo does not have is **skipped, never a hole**.
162
+ - **`src/components/bcms/Sections.<astro|tsx>`** — the registry, slug → component. **When the CMS
163
+ list is empty it falls back to the order the repository shipped, and that is load-bearing:** an
164
+ empty `blocks` is what a fresh clone, the committed stub and every not-yet-componentised page all
165
+ look like, so rendering nothing there would blank the site on the first build after the codemod
166
+ ran, with nothing in the diff to explain it. The fallback is decided on the **raw** block list:
167
+ a page whose blocks all name components this repo does not have has still been *arranged*, and
168
+ putting the shipped bands back would undo an editor's work on the next build.
169
+ - **The page** — the contiguous run of section roots replaced by one `<Sections page="<slug>" />`,
170
+ with the import added.
171
+ - **The stubs** — `blocks: []` added to each page's snapshot (keys it already has are kept), and
172
+ `bcms-content/components.json` = `[]`.
173
+
174
+ ### Which file is a route?
175
+
176
+ The router's convention, not a search. Every candidate file says which route IT serves —
177
+ `src/pages/about.astro`, `app/about/page.tsx`, `src/routes/about/+page.svelte` all answer `/about`,
178
+ and so does `src/app/(marketing)/about/page.tsx`, because a Next route group and a parallel-route
179
+ segment are not in the URL. Asking the SOURCE instead ("which file mentions this field group?")
180
+ answers a different question: `hero` is on every landing page, and one route's codemod then rewrote
181
+ another route's file. A route no file serves is `NOT_IN_SOURCE`; a route TWO files serve is
182
+ `ROUTE_FILE_AMBIGUOUS` — the repository's own router has to pick, and a codemod that picks
183
+ differently edits a page nobody serves.
184
+
185
+ ### What it will not accept
186
+
187
+ The brief and the plan are validated at ingress, before a byte is written. **A page slug becomes a
188
+ path** — `bcms-content/<slug>.json` — so `../package` names the repository's own manifest, from
189
+ inside `--root`, where the CLI's boundary check cannot see it. Slugs, group keys and proposed slugs
190
+ must each be one safe path segment (`[A-Za-z0-9_-]{1,64}`), routes must be absolute URL paths, and
191
+ every field the codemod consumes is typed. A value that fails refuses the whole run with
192
+ `BRIEF_INVALID` or `PLAN_INVALID` — a throw, not a receipt, because there is no partial run to
193
+ report on.
194
+
195
+ ### What travels with the markup
196
+
197
+ Every name the section's CODE uses — expression attributes, expression nodes, component tags; never
198
+ its TEXT — is accounted for. The page's imports are carried and re-based onto the new file, and the
199
+ page's module-scope declarations are carried **transitively**: `{plans.map(…)}` brings `const plans`,
200
+ and `plans` brings the `const currency` it reads. A name nothing can supply — a local of the page's
201
+ own component body, an expression this version cannot parse — is `SECTION_FREE_IDENTIFIERS`, which
202
+ is a refusal that names the identifier rather than a component file that does not build.
203
+
204
+ Scope is LEXICAL: `{rows.map((row) => …)}` binds `row` for the markup inside that expression and
205
+ for nothing else, so a later sibling `{row}` is free and says so. Astro's shorthand and spread
206
+ attributes (`{title}`, `{...rest}`) are read through the parser's own expression span rather than
207
+ by looking for a `{` — the compiler reports both at the identifier, four characters in. A namespace
208
+ import travels as `import * as X`, not as a named one.
209
+
210
+ ### What it refuses, and what it says
211
+
212
+ | reason | |
213
+ |---|---|
214
+ | `SECTION_ROOT_AMBIGUOUS` | the group's fields sit under no single element, one section's root contains another's, or the group is rendered somewhere other than this route's page file. |
215
+ | `SECTION_NOT_CONTIGUOUS` | the page's section roots share no container at all. **The components are still written**; the page is left exactly as its author wrote it. |
216
+ | `SECTION_FREE_IDENTIFIERS` | the markup uses a name the component file cannot be given. The identifier is named in the message. |
217
+ | `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. |
218
+ | `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. |
219
+ | `ROUTE_FILE_AMBIGUOUS` | two files serve one route. |
220
+ | `DIALECT_UNSUPPORTED` | the page is svelte, vue or html. Nothing is written for it. |
221
+ | `HELPER_CONFLICT` | `src/bcms-content.ts` is a module this package did not write. `--overwrite-helper` proceeds. |
222
+ | `SNAPSHOT_INVALID` | `bcms-content/<page>.json` is not JSON this can add `blocks` to. Refused before a byte is written for that route. |
223
+ | `NOT_IN_SOURCE` | no file we were given renders the group's fields, or the route names no page file. |
224
+
225
+ A plan spanning TWO frameworks gets two of everything that is dialect-shaped: its own components,
226
+ its own shipped-order table and its own `Sections`, per dialect. Only `src/lib/bcms-sections.ts` is
227
+ shared, because it is plain TypeScript over the same snapshots.
228
+
229
+ ### Running it again
230
+
231
+ Completion is decided per ROUTE, from that page's own bytes — it imports our registry and calls
232
+ `<Sections page="<its slug>" />`, or it calls our components in place. Not from "some file
233
+ somewhere carries our marker", which is true of every group that has ever been extracted and left a
234
+ brand-new page untouched.
235
+
236
+ The registry and the sections library are rebuilt WHOLE each run, so they are first read back:
237
+ components an earlier run wrote are reused rather than rewritten, and the shipped order of every
238
+ page already served is carried forward. Without that, componentizing one new page emitted a
239
+ registry that had never heard of the others, and they rendered blank.
240
+
241
+ A run whose roots DO share a container but have a foreign sibling between them is not refused: each
242
+ root becomes its own `<Hero …/>` where its markup was, counted `inlineOnly`. The markup is
243
+ componentised and the ORDER stays the repository's — looping that page would move the sibling too.
244
+
245
+ The arithmetic is checked rather than hoped for:
246
+
247
+ ```
248
+ plan sections === extracted + inlineOnly + alreadyExtracted + pending.length
249
+ ```
250
+
251
+ `alreadyExtracted` is the fourth bucket the plan document does not name, and it is what makes the
252
+ round trip observable: a second run over an already-componentised tree reports `extracted: 0,
253
+ pending: []` **and** still accounts for every section. Our own output is recognised by a marker
254
+ comment, never by its path — `src/components/bcms/` is a directory anybody may put a component in.
255
+
256
+ Known leftovers, stated rather than hidden: a page whose every read moved into a component keeps
257
+ the now-unused `bcms`/snapshot imports P2 added. They are harmless at build time and removing them
258
+ would mean editing statements no section asked about.
259
+
119
260
  ## Not here yet
120
261
 
121
262
  `promoteSharedChrome`, and prop drilling through a svelte or vue component (both parse, and both
122
263
  bind their own copy; only `findPropTargets` is jsx/astro-only, so a drilled prop there is
123
264
  `PROP_TARGET_NOT_FOUND` rather than a wrong edit).
265
+
266
+ Componentize: svelte and vue extraction, and Phase B of the lane — copy owned by the INSTANCE
267
+ (`overrides`) with the page-lane bindings rewritten to the block lane. `bcmsSection` already
268
+ prefers `overrides`, so the repo half of Phase B is a data change rather than another codemod.