@bettercms-ai/convert 0.6.0 → 0.8.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,61 @@
1
1
  # @bettercms-ai/convert
2
2
 
3
+ ## 0.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 8a4e961: Bind array literals written inline in the markup, and drill a repeater row's leaf into the
8
+ component that renders it.
9
+
10
+ `{['Offer', 'Structure', 'RTL'].map((item) => <li>…</li>)}` had no lane at all — the list lane
11
+ searches the module for a NAMED array, and an inline literal has no name — so every one of its
12
+ items came back `IN_EXPRESSION` with the copy in plain sight. The `]` immediately before the
13
+ `.map(` is the literal's close, which is what makes it safe without a name: there is exactly one
14
+ loop over it and nothing else in the file can hold a second reference. A value rendered beside an
15
+ icon (`<li><span>✓</span>{item}</li>`) gets a `<span>` of its own instead of declaring on the
16
+ parent, because a publish writing the value over the parent deletes the icon.
17
+
18
+ `demos.map((demo) => <DemoCard title={demo.title} />)` refused the whole group with
19
+ `PROP_TARGET_NOT_FOUND`: the value is rendered in the component's file, not the page's. It now
20
+ follows the same drill the scalar lane has always used — a row's leaf is that journey with an index
21
+ in the path. The call site declares one `bcmsBindings={{ title: `cards[${i}].title` }}` per element
22
+ and the component gets `data-bcms-field={bcmsBindings?.title}`. `{...row}` counts as passing the
23
+ prop. Every leaf placed or none, as before: a drill that misses refuses the group by its own reason.
24
+
25
+ Also: `tsconfig.json` / `jsconfig.json` are now opened by the scan gate. `aliasesFrom` reads them
26
+ and nothing ever handed it one, so every `paths` alias resolved to nothing and every prop drilled
27
+ through an `@components/*` import refused with the component sitting right there.
28
+
29
+ `bindDataLiterals` is async for this; it is exported, and a caller outside this package has to
30
+ await it.
31
+
32
+ - 07f6648: Bind arrays of primitives and tuples rendered through `.map`.
33
+
34
+ `const promise = [['Once', 'One message'], …]` iterated as `promise.map(([title, text], i) => …)`
35
+ is the same shape as a row of objects one rung simpler, and the derive lane describes it as a
36
+ repeater group all the same. The declaration becomes `bcmsTuples(page, [[…paths…], …], [ …the
37
+ original array, verbatim… ])` — `bcmsList` for a list one wide — and each rendered element carries
38
+ the same templated `data-bcms-field` a repeater writes, so a second run reads it back and changes
39
+ nothing. The committed helper is v6; an existing v5 helper upgrades in place.
40
+
41
+ The read maps over the path list rather than over the CMS array, so every row's text is editable
42
+ and the number of rows is still the template's: the receipt adds one `PRIMITIVE_LIST` note per
43
+ array saying a row added in the CMS does not appear and a deleted one falls back to the template's
44
+ own copy. A list whose paths are unrelated scalars, one used anywhere but that single `.map` or
45
+ `export`ed, one rendered inside a larger expression, one whose tuple position changes kind between
46
+ rows, and one the brief covers only part of are all refused by name.
47
+
48
+ On the Shatter acceptance repository this moves 16 paths from `IN_EXPRESSION` to `rewritten`
49
+ (219 → 235 of 735).
50
+
51
+ ## 0.7.0
52
+
53
+ ### Minor Changes
54
+
55
+ - e8b5b1c: Convert the shapes real template sites are made of: data literals rendered through `.map`, headings whose copy is one text node of a mixed element, and images imported from `src/assets`. Root-level `.md` files are no longer read as templates, and a located-but-unplaced path now carries a reason that is true (`IN_EXPRESSION`, or `NOT_IN_SOURCE` with the file named).
56
+
57
+ New helper exports `bcmsRowsAs` and `bcmsImage` (helper v5), and two new pending reasons: `REPEATER_AMBIGUOUS` and `IMAGE_ASSET_UNRESOLVED`.
58
+
3
59
  ## 0.6.0
4
60
 
5
61
  ### Minor Changes
package/README.md CHANGED
@@ -58,10 +58,104 @@ tolerant pass that will only place a literal occurring exactly once between a `>
58
58
  that, an injected `llmFallback` whose output is accepted only if it re-parses and declares exactly
59
59
  the paths that were asked for. The CLI injects none — the agent running it is tier 3.
60
60
 
61
- **Everything it will not do has a name.** `AMBIGUOUS_LITERAL` when two different paths share a
62
- sentence and position cannot separate them (both are skipped; the rest of the file still converts),
63
- `SUBSTRING_ONLY`, `IN_SCRIPT_OR_COMMENT`, `KIND_MISMATCH`, `NOT_IN_SOURCE`, `PARSE_ERROR` — and the
64
- receipt's arithmetic is checked, not hoped for:
61
+ **A prose file is a template only where a framework renders one.** `.md`, `.mdx`, `.njk`, `.hbs`,
62
+ `.ejs`, `.liquid`, `.erb` and `.twig` are opened under a directory named `pages`, `content`, `src`,
63
+ `app`, `routes`, `layouts`, `components`, `templates`, `views`, `_includes` or `_layouts` — and
64
+ never at the repository root or under `docs/`. A README quoting the hero line is documentation
65
+ ABOUT the site, and because a path counts as rewritten only when every located occurrence was, one
66
+ of them pended 176 of 735 paths on a real repository with a reason naming a file nobody would ever
67
+ want bound.
68
+
69
+ **A scalar module literal is converted too.** `const tagline = "…"` rendered as `{tagline}` becomes
70
+ `const tagline = bcms(page, "<path>", "…")`, with the declaration on the element that renders it —
71
+ a literal path, because there is no index to template. Refused as `IN_EXPRESSION` when the
72
+ identifier is rendered more than once, used in an attribute as well, or transformed on the way:
73
+ moving the declaration to a CMS read changes the value everywhere that identifier is used.
74
+
75
+ **Copy that lives in a data literal is converted too.** `deriveSchema` reads the BUILT page, so
76
+ four cards arrive as `cards[0..3]`; the repository renders ONE element from a frontmatter (astro)
77
+ or module-scope (jsx/tsx) array through `.map`. The declaration becomes
78
+
79
+ ```js
80
+ const features = bcmsRowsAs(bcmsHome, "cards", [ …the original array, verbatim… ],
81
+ { title: "h3-field", body: "p-field" });
82
+ ```
83
+
84
+ — the rows come from the CMS and are renamed onto the template's OWN property names, so
85
+ `feature.tone`, `feature.link` and every other property the brief never heard of keep rendering
86
+ (`bcmsRowsAs` merges each CMS row over the row the template shipped with, and a row an editor adds
87
+ inherits row 0's styling). Each row element then declares
88
+ `` data-bcms-field={`cards[${i}].<leaf>`} ``, and the callback gains an index parameter if it had
89
+ none. An imported data module stays `IN_DATA_FILE`.
90
+
91
+ **An array of PRIMITIVES or TUPLES is converted through its paths, not through its name.** The
92
+ same shape one rung simpler: `const deliverables = ["…", "…", "…"]`, or
93
+ `const faqs = [["q", "a"], …]`, iterated by `.map` with the callback rendering the item (or its
94
+ destructured positions) on one element each. There is no row object to key, so every element's
95
+ path is passed to the read:
96
+
97
+ ```js
98
+ const faqs = bcmsTuples(bcmsLanding, [["faqs[0].dt-field", "faqs[0].dd-field"], …],
99
+ [ …the original array, verbatim… ]);
100
+ ```
101
+
102
+ `bcmsList` is the same for a list one wide. Each rendered element declares
103
+ `` data-bcms-field={`faqs[${i}].<leaf>`} `` — the same templated path a repeater writes, so a
104
+ second run reads it back and changes nothing — and the callback gains an index parameter, and a
105
+ jsx row its `key`, if it had none.
106
+
107
+ These arrays render exactly the paths this run wrote. Unlike `bcmsRows`, which reads the array
108
+ itself, `bcmsList`/`bcmsTuples` map over that fixed path list — so every row's TEXT is editable and
109
+ the NUMBER of rows is still the template's: a row added in the CMS has no path and never appears,
110
+ and a row deleted falls back to the copy the template shipped with. The receipt says so out loud,
111
+ once per array, as a `PRIMITIVE_LIST` note. Reading the array itself is the derive lane's job, not a
112
+ fourth shape here.
113
+
114
+ The lane refuses, by name: `IN_EXPRESSION` when the identifier is used anywhere but that one `.map`
115
+ or is `export`ed (the read replaces the value everywhere, including in files this run cannot see),
116
+ when the callback renders the item inside a larger expression, when an element is not a string
117
+ literal, when one tuple position is text in one row and richtext in the next (one element has one
118
+ sink), when the brief's paths do not cover EVERY element of the array, and when the paths are
119
+ unrelated scalars rather than one group's rows — that last one could only be declared as an
120
+ expression nothing downstream can verify; `REPEATER_AMBIGUOUS` when two loops iterate the array.
121
+
122
+ **An array literal written INLINE in the markup is the same lane, with no name between the two
123
+ halves.** `{['Offer', 'Structure', 'RTL'].map((item) => <li>…</li>)}` is what a template reaches
124
+ for whenever the list is four words long — nobody lifts four bullet points into a `const` — and the
125
+ copy then sits inside an expression the matcher is blind to by construction. The `]` immediately
126
+ before the `.map(` IS the literal's close, so there is exactly one loop over it and nothing else in
127
+ the file can hold a second reference to it: no "used anywhere but that one `.map`" test is needed,
128
+ and everything else — the paths, the templated declaration, the index parameter, the refusals — is
129
+ the named lane's. A value rendered BESIDE something else (`<li><span>✓</span>{item}</li>`) gets a
130
+ `<span>` of its own rather than declaring on the parent, because a publish writing the value over
131
+ the parent deletes the icon; the same wrap the scalar lane writes for a mixed element.
132
+
133
+ **A row's leaf handed to a component is drilled, exactly as a scalar prop is.**
134
+ `demos.map((demo) => <DemoCard title={demo.title} />)` renders the value in the COMPONENT's file,
135
+ so the page's call site declares `` bcmsBindings={{ title: `cards[${i}].title` }} `` — one object
136
+ per element, merged when a row hands two leaves to one component — and the component's element gets
137
+ `data-bcms-field={bcmsBindings?.title}` with the prop added to its destructuring and its props
138
+ type. `{...row}` counts as passing the prop: it is how most templates hand a row over. The group
139
+ still binds whole or not at all, so a drill that misses (the component cannot be resolved, nothing
140
+ in it renders the prop, this conversion already rewrites that file, or the call site already
141
+ carries a `bcmsBindings`) refuses the WHOLE group by the drill's own reason.
142
+
143
+ **An image is bound through its import, not through its url.** An image's `original` is the name
144
+ the bundler minted (`/_astro/hero.do77EGgx_ZNfVA8.jpg`) — it exists in no file, and the asset
145
+ itself is not a source candidate, so a text search can never find it. What survives the build is
146
+ the BASE NAME, and the page says it itself: `import hero from "../assets/hero.jpg"`. The element
147
+ rendering that binding — `<img>`, `<source>`, `<video poster>`, or `<Image>`/`<Picture>` from
148
+ `astro:assets`, which forward unknown props to the `<img>` they render — gets
149
+ `src={bcmsImage(page, "<path>", hero)}` and the declaration beside it.
150
+
151
+ **Everything it will not do has a name, and the name is TRUE.** `AMBIGUOUS_LITERAL` when two
152
+ different paths share a sentence and position cannot separate them (both are skipped; the rest of
153
+ the file still converts), `SUBSTRING_ONLY`, `IN_SCRIPT_OR_COMMENT`, `KIND_MISMATCH`,
154
+ `REPEATER_AMBIGUOUS` (one array declaration, two loops over it — the row has two homes),
155
+ `IMAGE_ASSET_UNRESOLVED`, `PARSE_ERROR`. `NOT_IN_SOURCE` is reserved for what it says: a path
156
+ `locate` found in a file is `IN_EXPRESSION` when it sits in the module region or an expression
157
+ node, and where `NOT_IN_SOURCE` is still the answer the row names the file it was found in. And
158
+ the receipt's arithmetic is checked, not hoped for:
65
159
 
66
160
  ```
67
161
  paths.declared === paths.rewritten + paths.alreadyDeclared + paths.pending.length
@@ -428,6 +522,22 @@ and removing them would mean editing statements no section asked about.
428
522
 
429
523
  ## Not here yet
430
524
 
525
+ GROWABLE lists. An array of primitives or tuples is bound (above) but renders the number of rows
526
+ the template shipped with, because the read maps over a fixed path list rather than over the CMS
527
+ array — and when the brief gave its elements unrelated scalar paths (`li-field`, `li-rtl`, …)
528
+ rather than one group's rows, it is not bound at all: the only declaration that could carry a
529
+ different path per row is an expression, and one nothing downstream can verify is worse than the
530
+ honest `IN_EXPRESSION`. Both are the same fix on the derive side — minting the group — not a
531
+ further shape here. Svelte and vue data literals are not converted
532
+ either; all three lanes are written against the two dialects that keep their module and their
533
+ markup in one file with an AST this package already holds.
534
+
535
+ SHARED CHROME the brief still calls page copy. A header rendered by one component that nine routes
536
+ each declare a `nav[0].label` for is nine targets on one element, and there is no honest binding: a
537
+ read names ONE route's snapshot. That is `promoteSharedChrome`'s job on the derive side — once the
538
+ field is `scope: "layout"` the codemod collapses the nine to one identity and binds it. Until then
539
+ those paths are `AMBIGUOUS_LITERAL`, which is true.
540
+
431
541
  `promoteSharedChrome`, and prop drilling through a svelte or vue component (both parse, and both
432
542
  bind their own copy; only `findPropTargets` is jsx/astro-only, so a drilled prop there is
433
543
  `PROP_TARGET_NOT_FOUND` rather than a wrong edit).