@bettercms-ai/convert 0.2.1 → 0.3.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 CHANGED
@@ -1,5 +1,110 @@
1
1
  # @bettercms-ai/convert
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Componentize: a section the repository has ALREADY componentised is REGISTERED, not refused.
8
+
9
+ A page whose `index.astro` imports `Hero.astro` and renders `<Hero />` has made the cut
10
+ this lane exists to make. There is nothing in the page to extract, and refusing left the
11
+ CMS naming a section no component in the repo rendered. The existing file is now
12
+ registered under the section's slug — the registry imports it where it lives — and the
13
+ page keeps its call site. It has to be ONE file, one the page IMPORTS, and one the
14
+ registry's own dialect can render; anything else is `SECTION_ROOT_AMBIGUOUS` with the
15
+ reason named.
16
+
17
+ New receipt bucket, in the arithmetic:
18
+ `plan sections === extracted + inlineOnly + alreadyExtracted + registered + pending.length`.
19
+ `registered` is orthogonal to placement — nothing was written for those sections, so
20
+ counting them `extracted` would claim authorship of bytes this package never produced.
21
+
22
+ A page with a registered section is ALL OR NOTHING: `<Sections>` renders every placement
23
+ the page has, so the run is replaced by one `<Sections page="…" />` only when every
24
+ section of the page resolved and every call site is a bare tag in that run. Otherwise the
25
+ call sites stay exactly where their author put them — replacing a subset would render a
26
+ registered section twice, once in place and once out of the registry. The registry
27
+ read-back also resolves ANY relative import now, not only `./Name`: without that a second
28
+ run dropped the registered entry and the page rendered blank.
29
+
30
+ - Repeaters: a row that holds a list of its own becomes a loop inside the loop.
31
+
32
+ `cards[i].items[j].label` is emitted as a nested `.map` in astro and jsx, reading its rows
33
+ off the ROW — `bcmsRows(card, "items", [ …this card's own items… ])` — so a bullet added in
34
+ the dashboard renders. The committed helper needed nothing: `bcmsRows` already passes a
35
+ nested array through untouched and `bcmsSection` hands a section its group whole.
36
+
37
+ The bindings are `` data-bcms-field={`cards[${i}].items[${j}].label`} `` and the shape is
38
+ `cards[*].items[*].label`; the loop now REPORTS the bindings it wrote, both loop variables
39
+ included, instead of the caller re-deriving a literal the file does not contain. ONE level
40
+ of nesting — what the plan mints and what the component prop validator accepts — so a leaf
41
+ indexed twice stays `REPEATER_FIXED_LENGTH`, and so does the html dialect, which has no
42
+ loop form at all.
43
+
44
+ - A hand-written declaration counts as `alreadyDeclared`, never `NO_ORIGINAL`.
45
+
46
+ An element that names the target (`data-bcms-field`, `data-bcms-layout-field`, an address
47
+ in `data-bcms-props`) but gets its copy some other way is DECLARED: the publisher injects
48
+ into it and the editor makes it click-to-edit. It is simply not reading through the
49
+ committed helper. The receipt gains one row per already-declared path —
50
+ `paths.declarations[] { route, scope, path, kind, file, readsFromCms }` — and the
51
+ arithmetic is unchanged. The evidence has to be on the file the ROUTER says serves that
52
+ route: without a read there is nothing in a declaration that says which route it is for,
53
+ and two routes legitimately carry `hero.title`.
54
+
55
+ README: the "declared by hand" paragraph, and the address grammar in one place —
56
+ `data-bcms-field` / `data-bcms-layout-field="layout:<section>:<field>"`, attribute values
57
+ through `data-bcms-props="<address>|<kind>|<attr>"`, and `data-bcms-block` on a section root.
58
+
59
+ - Review hardening for all three of the above.
60
+
61
+ **Componentize.** A section the page renders BOTH inline and through a component it imports is
62
+ `SECTION_ROOT_AMBIGUOUS`: cutting the inline copy left `<Sections>` rendering the band out of
63
+ the registry while the untouched `<Hero />` beside it rendered it again. So is a section two
64
+ imported components both render, and a namespace the page imports but never calls. A registered
65
+ component is imported in the shape its module actually exports — default, `import { Hero }`, or
66
+ `import * as Bands` plus the member the call site names — because a default import of a named
67
+ export binds `undefined`. Contiguity now requires whitespace or comments between the roots, not
68
+ merely consecutive element siblings: `<Hero />Read this first<Features />` had no element in the
69
+ way and the run splice deleted the sentence. The registry read-back goes through the dialect's
70
+ parser instead of a line-shaped regex, so a formatter cannot make it forget the components it
71
+ names — and when the tables genuinely cannot be reconstructed the run refuses with the new
72
+ `REGISTRY_UNREADABLE` rather than rebuilding a registry without them.
73
+
74
+ **Repeaters.** A nested list the repository ships ONE row of still becomes a loop; the plan
75
+ already says `items` is an array, and reading one bullet as "not a list" is how the second one an
76
+ editor adds never renders.
77
+
78
+ **Declarations.** The route rule now covers LOCATED paths too — a hand-written declaration on
79
+ another route's page file, or on an element two routes both claim, is not evidence for either —
80
+ and every declarations row names the file it was found on. `aliasesFrom` reads JSONC (block
81
+ comments, trailing commas), because a tsconfig this package could not parse silently declared no
82
+ aliases and every aliased import then read as "not imported"; one it still cannot read is a
83
+ `TSCONFIG_UNREADABLE` note on the receipt.
84
+
85
+ - A second review round, over the same three.
86
+
87
+ **The registry read-back.** An import must resolve to a file that is actually there —
88
+ composing the path from our own naming convention meant a component somebody DELETED still
89
+ "resolved", and the run re-emitted a registry importing it. Both tables are now parsed
90
+ property by property, with strings and comments skipped by the brace matcher, so a one-line
91
+ table, a wrapped one and a comment containing a `}` are all read whole; anything that cannot
92
+ be reconstructed in full is `REGISTRY_UNREADABLE` rather than a quiet partial rebuild.
93
+
94
+ **Bindings.** `import Default, { Hero } from "./bands"` binds one module twice; the binding
95
+ the page actually CALLS is the one registered, instead of whichever came first.
96
+
97
+ **Declarations.** The route gate now also applies to a declaration that reads from the CMS —
98
+ a home page rendering a teaser of `/about`'s headline reads `/about`'s snapshot legitimately,
99
+ and counting it left `/about`'s own page hardcoded and unreported. "Another route's page" means
100
+ a route THE BRIEF names: `routeOfFile` reads the repository's convention and the two disagree
101
+ about a dynamic route (`/[slug]` vs `/post/[slug]`), and refusing on a name the brief never
102
+ issued rejected every dynamic route's own page.
103
+
104
+ **Configs.** A tsconfig whose `baseUrl`/`paths` are valid JSON of the wrong type threw a
105
+ `TypeError` out of a pure function; the shape is checked and a bad one is
106
+ `TSCONFIG_UNREADABLE`, which the componentize receipt now carries too.
107
+
3
108
  ## 0.2.0
4
109
 
5
110
  ### Minor Changes
package/README.md CHANGED
@@ -71,6 +71,49 @@ A path counts as rewritten only when EVERY located occurrence of it was. A value
71
71
  the home page and a shared header is two places a reader sees it, and converting one of them reads
72
72
  as a finished job.
73
73
 
74
+ **A declaration this package did not write still counts.** An element that names the target —
75
+ `data-bcms-field`, `data-bcms-layout-field` or an address in `data-bcms-props` — but gets its copy
76
+ some other way (a literal somebody left there, the project's own data module) is DECLARED: the
77
+ publisher injects into that element and the editor makes it click-to-edit. It is simply not reading
78
+ through the committed helper. Those land in `alreadyDeclared` with a receipt row of their own,
79
+ `readsFromCms: false`, rather than in `NO_ORIGINAL`:
80
+
81
+ ```jsonc
82
+ "declarations": [{ "route": "/", "scope": "page", "path": "hero.title",
83
+ "kind": "text", "file": "src/pages/index.astro", "readsFromCms": false }]
84
+ ```
85
+
86
+ The evidence may never be ANOTHER ROUTE'S PAGE FILE — one the brief itself names, since
87
+ `routeOfFile` reads the repository's convention and the two legitimately disagree about a dynamic
88
+ route — and it may never be an element two routes both claim. The same rule gates a declaration
89
+ that DOES read from the CMS: a home page rendering a teaser of `/about`'s headline reads `/about`'s
90
+ snapshot legitimately, and counting it left `/about`'s own page hardcoded and unreported. Without a read there is nothing in a declaration that says which route it is for, and two
91
+ routes legitimately carry `hero.title` — so "some file declares that string" would mark the other
92
+ route bound and leave it hardcoded forever. A path with no original at all is stricter still: the
93
+ only evidence available is the file's identity, so it has to be the one file the router says serves
94
+ that route. A layout path has no route by construction, and any file may carry it.
95
+
96
+ **A tsconfig is JSONC.** Comments and trailing commas are read (that is what `tsc --init` writes),
97
+ because a config this package cannot parse declares no `paths` aliases as far as it is concerned —
98
+ and then every `@/components/Hero` resolves to nothing and the refusal that follows names the wrong
99
+ cause. Its alias fields are shape-checked too: `"paths": "src/*"` is valid JSON and the wrong type,
100
+ and reading it threw a `TypeError` out of a pure function naming nothing. One it still cannot read
101
+ is a receipt NOTE rather than a silence — on the componentize receipt as well, which resolves a
102
+ page's `<Hero />` through exactly those aliases:
103
+
104
+ ```jsonc
105
+ "notes": [{ "code": "TSCONFIG_UNREADABLE", "file": "tsconfig.json", "message": "…" }]
106
+ ```
107
+
108
+ **The address grammar, in one place.** Page copy is `data-bcms-field="<path>"` plus
109
+ `data-bcms-kind="<kind>"`. The shared chrome is a DIFFERENT attribute carrying a prefixed address,
110
+ `data-bcms-layout-field="layout:<section>:<field>"`, so the block lane's own `[data-bcms-field]`
111
+ query cannot reach a chrome marker. A value that lives in an ATTRIBUTE (a promoted nav link's
112
+ `href`) has no field attribute of its own and rides `data-bcms-props="<address>|<kind>|<attr>;…"` —
113
+ where `<address>` carries the same `layout:` prefix when it is chrome. A section root additionally
114
+ carries `data-bcms-block="<blockId>"`, which is what the editor hit-tests for section chrome and
115
+ reorder.
116
+
74
117
  ## As a library
75
118
 
76
119
  ```ts
@@ -92,6 +135,17 @@ proposer re-derives them from its own scan rather than believing the receipt. A
92
135
  become a loop — non-consecutive siblings, one row shaped differently, the html dialect — is still
93
136
  declared and read per index and reported `REPEATER_FIXED_LENGTH`.
94
137
 
138
+ **A row that holds a list of its own becomes a loop inside the loop.** `cards[i].items[j].label` is
139
+ a nested `.map` in astro and jsx, reading its rows off the ROW — `bcmsRows(card, "items", […])`,
140
+ the same helper one level down, because `bcmsRows` passes a nested array through untouched and
141
+ `bcmsSection` hands a section its group whole. The binding is
142
+ `` data-bcms-field={`cards[${i}].items[${j}].label`} `` and the shape is `cards[*].items[*].label`.
143
+ ONE level: that is what the plan mints and what the component prop validator accepts, so a leaf
144
+ indexed twice keeps the length the template gives it. One inner row is enough to make the loop —
145
+ the plan already says `items` is an array, and reading a card that ships a single bullet as "not a
146
+ list" is how the second bullet an editor adds never renders. The html dialect has no loop form at all, so
147
+ it stays fixed-length there — every index declared, the number of rows the template's.
148
+
95
149
  **A prop-drilled literal is converted in two hops, in one proposal.** `<Hero title="…" />` becomes
96
150
  `title={bcms(…)}` plus `bcmsBindings={{ title: "hero.title" }}`, and the `<h1>{title}</h1>` inside
97
151
  `Hero` — resolved through the file's own imports, relative or through the project's tsconfig
@@ -142,7 +196,7 @@ modify src/pages/index.astro
142
196
  modify bcms-content/home.json
143
197
  add bcms-content/components.json
144
198
 
145
- 5 sections extracted (0 inline-only, 0 already extracted, 0 pending)
199
+ 5 sections extracted (0 registered, 0 inline-only, 0 already extracted, 0 pending)
146
200
  ```
147
201
 
148
202
  `--plan <file>` is required and is only read in this mode. `--dry-run`, `--receipt`, `--strict` and
@@ -166,8 +220,22 @@ add bcms-content/components.json
166
220
  ran, with nothing in the diff to explain it. The fallback is decided on the **raw** block list:
167
221
  a page whose blocks all name components this repo does not have has still been *arranged*, and
168
222
  putting the shipped bands back would undo an editor's work on the next build.
223
+ - **Nothing at all, for a section the repository has ALREADY componentised.** A page that renders
224
+ `<Hero />` from a file it imports has made this cut itself: the existing file is REGISTERED under
225
+ the section's slug — the registry imports it where it lives, **in the shape its module actually
226
+ exports**: a default import, `import { Hero }` for a named export, or `import * as Bands` plus the
227
+ member the page's own call site names — and the page keeps calling it. When one module is bound
228
+ twice (`import Default, { Hero } from "./bands"`), the binding the page actually CALLS is the one
229
+ registered. It has to be a file this page imports, the only one it imports that renders the
230
+ group, and one the registry's dialect can render; a section the page ALSO renders inline is two
231
+ answers and is refused. Everything else is `SECTION_ROOT_AMBIGUOUS` with the reason spelled out.
169
232
  - **The page** — the contiguous run of section roots replaced by one `<Sections page="<slug>" />`,
170
- with the import added.
233
+ with the import added. Contiguous means consecutive element siblings **and** nothing but
234
+ whitespace or comments in the bytes between them: `<Hero />Read this first<Features />` has no
235
+ element in the way, and one splice over that run would delete the sentence. A page with a REGISTERED section is all or nothing: `<Sections>` renders
236
+ every placement the page has, so the run is replaced only when every one of the page's sections
237
+ resolved and every call site is a bare tag in that run. Otherwise the call sites stay exactly
238
+ where their author put them — replacing a subset would render a registered section twice.
171
239
  - **The stubs** — `blocks: []` added to each page's snapshot (keys it already has are kept), and
172
240
  `bcms-content/components.json` = `[]`.
173
241
 
@@ -211,11 +279,12 @@ import travels as `import * as X`, not as a named one.
211
279
 
212
280
  | reason | |
213
281
  |---|---|
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. |
282
+ | `SECTION_ROOT_AMBIGUOUS` | the group's fields sit under no single element, one section's root contains another's, the page renders it BOTH inline and through a component it imports, two imported components render it, or it is rendered somewhere this cannot register — a file this page does not import, a component of another dialect, a namespace the page never calls. The message says which. |
215
283
  | `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
284
  | `SECTION_FREE_IDENTIFIERS` | the markup uses a name the component file cannot be given. The identifier is named in the message. |
217
285
  | `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
286
  | `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. |
219
288
  | `ROUTE_FILE_AMBIGUOUS` | two files serve one route. |
220
289
  | `DIALECT_UNSUPPORTED` | the page is svelte, vue or html. Nothing is written for it. |
221
290
  | `HELPER_CONFLICT` | `src/bcms-content.ts` is a module this package did not write. `--overwrite-helper` proceeds. |
@@ -236,7 +305,14 @@ brand-new page untouched.
236
305
  The registry and the sections library are rebuilt WHOLE each run, so they are first read back:
237
306
  components an earlier run wrote are reused rather than rewritten, and the shipped order of every
238
307
  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.
308
+ registry that had never heard of the others, and they rendered blank. The read-back goes through
309
+ the dialect's own PARSER, so a formatter that rewrote our imports (single quotes, a wrapped line)
310
+ changes nothing; the two tables are brace-matched with strings and comments skipped and every
311
+ property is parsed structurally, so a one-line table, a wrapped one, or a comment containing a `}`
312
+ are all read whole. An import must resolve to a file that is actually there — composing the path
313
+ from our own naming convention re-emitted a registry importing a component somebody had deleted.
314
+ When any of that cannot be reconstructed the run refuses by name rather than rebuilding from what
315
+ it happens to see.
240
316
 
241
317
  A run whose roots DO share a container but have a foreign sibling between them is not refused: each
242
318
  root becomes its own `<Hero …/>` where its markup was, counted `inlineOnly`. The markup is
@@ -245,17 +321,20 @@ componentised and the ORDER stays the repository's — looping that page would m
245
321
  The arithmetic is checked rather than hoped for:
246
322
 
247
323
  ```
248
- plan sections === extracted + inlineOnly + alreadyExtracted + pending.length
324
+ plan sections === extracted + inlineOnly + alreadyExtracted + registered + pending.length
249
325
  ```
250
326
 
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,
327
+ `registered` is orthogonal to placement: nothing was written for those sections — the file was
328
+ somebody's before the run and stays theirs — so counting them `extracted` would claim authorship of
329
+ bytes this package never produced. `alreadyExtracted` is the bucket the plan document does not
330
+ name, and it is what makes the round trip observable: a second run over an already-componentised tree reports `extracted: 0,
253
331
  pending: []` **and** still accounts for every section. Our own output is recognised by a marker
254
332
  comment, never by its path — `src/components/bcms/` is a directory anybody may put a component in.
255
333
 
256
334
  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.
335
+ the now-unused `bcms`/snapshot imports P2 added, and a page whose registered call sites became one
336
+ `<Sections>` keeps the imports of the components it used to call. They are harmless at build time
337
+ and removing them would mean editing statements no section asked about.
259
338
 
260
339
  ## Not here yet
261
340