@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 +90 -0
- package/README.md +145 -0
- package/dist/cli.js +1418 -30
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +294 -4
- package/dist/index.js +1348 -18
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
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.
|