@bettercms-ai/convert 0.7.0 → 0.9.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,86 @@
1
1
  # @bettercms-ai/convert
2
2
 
3
+ ## 0.9.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 94a00c3: `--forms`: point an imported site's own `<form>` at the CMS form behind it.
8
+
9
+ After an import the release hook derives a draft form row per `<form>` in the built site, so the
10
+ Forms tab fills up while the repository's markup still posts wherever it always did — to nothing,
11
+ or to somebody else's endpoint. Nothing joined the two halves, because the id that does is the
12
+ platform's and lives in the brief.
13
+
14
+ `npx @bettercms-ai/convert --forms --brief brief.json --root . --receipt receipt.json` reads the
15
+ brief's new `forms` array and writes, in one `magic-string` pass: `action`, `method`,
16
+ `data-bcms-form`, `data-bcms-form-success`, a `data-bcms-form-field` per field (on the control's
17
+ wrapper where it has one), the CMS key as each control's `name`, the `<p class="bcms-form-msg">`
18
+ the runtime writes into, and the submit script — the same bytes the hosted renderer ships, held
19
+ byte-for-byte by a test, because the `cf-turnstile-response` hoist in the middle of it is what
20
+ keeps enabling Turnstile from 403ing every submission.
21
+
22
+ The match is strict: a `<form>` is the CMS form's only when the names its controls post cover
23
+ every field key, and exactly one `<form>` answers. Everything else is receipted by name —
24
+ `FORM_NOT_IN_SOURCE`, `FORM_AMBIGUOUS`, `FIELD_UNMATCHED`, `ACTION_IS_EXPRESSION`,
25
+ `DIALECT_UNSUPPORTED`, `PARSE_ERROR` — because a form wired to the wrong id sends a customer's
26
+ leads into another form's inbox, silently, forever. A form already carrying `data-bcms-form` is
27
+ `alreadyWired`, so a second run writes nothing.
28
+
29
+ The receipt rides on `ConversionReceipt.forms` and claims no brief path, so
30
+ `submit_conversion_receipt` takes it unchanged. A form the brief lists as a DRAFT is wired AND
31
+ noted: it rejects every submission with a 403 until somebody publishes it in the Forms tab.
32
+
33
+ Also: `--version` now reports the package's real version. The constant had been stale at `0.4.0`
34
+ for four releases.
35
+
36
+ ## 0.8.0
37
+
38
+ ### Minor Changes
39
+
40
+ - 8a4e961: Bind array literals written inline in the markup, and drill a repeater row's leaf into the
41
+ component that renders it.
42
+
43
+ `{['Offer', 'Structure', 'RTL'].map((item) => <li>…</li>)}` had no lane at all — the list lane
44
+ searches the module for a NAMED array, and an inline literal has no name — so every one of its
45
+ items came back `IN_EXPRESSION` with the copy in plain sight. The `]` immediately before the
46
+ `.map(` is the literal's close, which is what makes it safe without a name: there is exactly one
47
+ loop over it and nothing else in the file can hold a second reference. A value rendered beside an
48
+ icon (`<li><span>✓</span>{item}</li>`) gets a `<span>` of its own instead of declaring on the
49
+ parent, because a publish writing the value over the parent deletes the icon.
50
+
51
+ `demos.map((demo) => <DemoCard title={demo.title} />)` refused the whole group with
52
+ `PROP_TARGET_NOT_FOUND`: the value is rendered in the component's file, not the page's. It now
53
+ follows the same drill the scalar lane has always used — a row's leaf is that journey with an index
54
+ in the path. The call site declares one `bcmsBindings={{ title: `cards[${i}].title` }}` per element
55
+ and the component gets `data-bcms-field={bcmsBindings?.title}`. `{...row}` counts as passing the
56
+ prop. Every leaf placed or none, as before: a drill that misses refuses the group by its own reason.
57
+
58
+ Also: `tsconfig.json` / `jsconfig.json` are now opened by the scan gate. `aliasesFrom` reads them
59
+ and nothing ever handed it one, so every `paths` alias resolved to nothing and every prop drilled
60
+ through an `@components/*` import refused with the component sitting right there.
61
+
62
+ `bindDataLiterals` is async for this; it is exported, and a caller outside this package has to
63
+ await it.
64
+
65
+ - 07f6648: Bind arrays of primitives and tuples rendered through `.map`.
66
+
67
+ `const promise = [['Once', 'One message'], …]` iterated as `promise.map(([title, text], i) => …)`
68
+ is the same shape as a row of objects one rung simpler, and the derive lane describes it as a
69
+ repeater group all the same. The declaration becomes `bcmsTuples(page, [[…paths…], …], [ …the
70
+ original array, verbatim… ])` — `bcmsList` for a list one wide — and each rendered element carries
71
+ the same templated `data-bcms-field` a repeater writes, so a second run reads it back and changes
72
+ nothing. The committed helper is v6; an existing v5 helper upgrades in place.
73
+
74
+ The read maps over the path list rather than over the CMS array, so every row's text is editable
75
+ and the number of rows is still the template's: the receipt adds one `PRIMITIVE_LIST` note per
76
+ array saying a row added in the CMS does not appear and a deleted one falls back to the template's
77
+ own copy. A list whose paths are unrelated scalars, one used anywhere but that single `.map` or
78
+ `export`ed, one rendered inside a larger expression, one whose tuple position changes kind between
79
+ rows, and one the brief covers only part of are all refused by name.
80
+
81
+ On the Shatter acceptance repository this moves 16 paths from `IN_EXPRESSION` to `rewritten`
82
+ (219 → 235 of 735).
83
+
3
84
  ## 0.7.0
4
85
 
5
86
  ### Minor Changes
package/README.md CHANGED
@@ -88,6 +88,58 @@ inherits row 0's styling). Each row element then declares
88
88
  `` data-bcms-field={`cards[${i}].<leaf>`} ``, and the callback gains an index parameter if it had
89
89
  none. An imported data module stays `IN_DATA_FILE`.
90
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
+
91
143
  **An image is bound through its import, not through its url.** An image's `original` is the name
92
144
  the bundler minted (`/_astro/hero.do77EGgx_ZNfVA8.jpg`) — it exists in no file, and the asset
93
145
  itself is not a source candidate, so a text search can never find it. What survives the build is
@@ -468,14 +520,59 @@ the now-unused `bcms`/snapshot imports P2 added, and a page whose registered cal
468
520
  `<Sections>` keeps the imports of the components it used to call. They are harmless at build time
469
521
  and removing them would mean editing statements no section asked about.
470
522
 
523
+ ## Forms
524
+
525
+ The third mode, and the shortest. The release-time derive lane reads every `<form>` in the built
526
+ site and creates a DRAFT form per distinct shape, so the Forms tab fills on import while the
527
+ repository's own markup goes on posting wherever it always did. `--forms` joins the two halves.
528
+
529
+ ```bash
530
+ # after the import; the brief carries the project's forms
531
+ npx @bettercms-ai/convert --forms --brief brief.json --root . --receipt forms-receipt.json
532
+ git diff
533
+ ```
534
+
535
+ ```
536
+ modify src/pages/contact.astro
537
+
538
+ 1 forms wired (0 already wired, 0 pending)
539
+ todo publish form Contact in the Forms tab — a draft form rejects every submission with 403.
540
+ ```
541
+
542
+ It writes six things onto a matched `<form>` and nothing else: `action="<submitUrl>"`,
543
+ `method="post"`, `data-bcms-form="<id>"` (plus `data-bcms-form-success` when the CMS form has a
544
+ success message), `data-bcms-form-field="<key>"` on each matched control, a
545
+ `<p class="bcms-form-msg" hidden>` before `</form>` when there is none, and the submit script once
546
+ per file — the same bytes the hosted renderer ships, pinned by a test. No helper, no snapshot, no
547
+ import: a form posts to an absolute URL at runtime, so there is nothing to read at build time.
548
+
549
+ The match is the FIELD KEYS: a `<form>` is a CMS form's only when the names its controls post are a
550
+ superset of that form's field keys and exactly one `<form>` answers. Everything else is pending with
551
+ a reason — `FORM_NOT_IN_SOURCE`, `FORM_AMBIGUOUS`, `FIELD_UNMATCHED`, `ACTION_IS_EXPRESSION`,
552
+ `DIALECT_UNSUPPORTED`, `PARSE_ERROR` — because a form wired to the wrong id sends the customer's
553
+ leads into another form's inbox, silently, forever. A `<form>` that already carries
554
+ `data-bcms-form` is `alreadyWired` and is left alone, so a second run is a no-op.
555
+
556
+ `--receipt <file>` writes this run's own receipt (`forms-receipt.json` — a separate file from the
557
+ P2 run's, since the two runs describe different work), and `--strict` exits 2 on any pending form.
558
+ `--forms` and `--componentize` are separate runs; doing both in one command is refused.
559
+
560
+ **Publish the drafts afterwards.** The lane creates forms as drafts and the public submit route
561
+ 403s a draft, so a wired draft form is an inviting form on a live site that rejects every
562
+ submission. The receipt carries one `notes` line per draft naming the form to publish in the Forms
563
+ tab; that is the step no codemod can do for you.
564
+
471
565
  ## Not here yet
472
566
 
473
- An array of PRIMITIVES — `const items = ["a", "b", "c"]`, or of tuples — whose elements the brief
474
- gave a separate NON-indexed path each (`li-field`, `li-rtl`, …) rather than one repeater group.
475
- There is no group to key and no constant to rewrite, so those stay `IN_EXPRESSION`; what would fix
476
- them is the derive lane minting the group, not a third shape here. Svelte and vue data literals are
477
- not converted either — both lanes are written against the two dialects that keep their module and
478
- their markup in one file with an AST this package already holds.
567
+ GROWABLE lists. An array of primitives or tuples is bound (above) but renders the number of rows
568
+ the template shipped with, because the read maps over a fixed path list rather than over the CMS
569
+ array — and when the brief gave its elements unrelated scalar paths (`li-field`, `li-rtl`, …)
570
+ rather than one group's rows, it is not bound at all: the only declaration that could carry a
571
+ different path per row is an expression, and one nothing downstream can verify is worse than the
572
+ honest `IN_EXPRESSION`. Both are the same fix on the derive side — minting the group — not a
573
+ further shape here. Svelte and vue data literals are not converted
574
+ either; all three lanes are written against the two dialects that keep their module and their
575
+ markup in one file with an AST this package already holds.
479
576
 
480
577
  SHARED CHROME the brief still calls page copy. A header rendered by one component that nine routes
481
578
  each declare a `nav[0].label` for is nine targets on one element, and there is no honest binding: a