@bettercms-ai/convert 0.1.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/README.md +123 -0
- package/dist/cli.js +5450 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +839 -0
- package/dist/index.js +5370 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# @bettercms-ai/convert
|
|
2
|
+
|
|
3
|
+
Turn an imported site's text-matched bindings into **declared** ones — deterministically, and with
|
|
4
|
+
a receipt of everything it could not do.
|
|
5
|
+
|
|
6
|
+
BetterCMS can already make an imported site editable by matching text: a release reads the built
|
|
7
|
+
HTML, finds the element whose text equals a field's value, and binds it. That works until someone
|
|
8
|
+
edits the copy, at which point the match is gone and the field quietly stops reflecting. The fix is
|
|
9
|
+
for the template itself to say which element holds which field — `data-bcms-field` — and to read the
|
|
10
|
+
value from the CMS with the current copy as its in-code fallback. This package does that edit.
|
|
11
|
+
|
|
12
|
+
## Use it
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
# In your site repo, with the brief from get_conversion_brief
|
|
16
|
+
npx @bettercms-ai/convert --brief brief.json --root .
|
|
17
|
+
git diff
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
modify src/pages/index.astro
|
|
22
|
+
add src/bcms-content.ts
|
|
23
|
+
add bcms-content/home.json
|
|
24
|
+
|
|
25
|
+
10 of 11 paths bound (0 already declared, 1 pending)
|
|
26
|
+
pending /about p-founded-in-2019-in — DIALECT_UNSUPPORTED (src/data/facts.js)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Exit codes: `0` done — the pending list is printed, not hidden; `1` something was refused;
|
|
30
|
+
`2` only with `--strict`, when any path is left pending. That last one is for CI.
|
|
31
|
+
|
|
32
|
+
| flag | |
|
|
33
|
+
|---|---|
|
|
34
|
+
| `--brief <file>` | the brief, as `get_conversion_brief` returns it. Required. |
|
|
35
|
+
| `--root <dir>` | the repository to convert. Nothing outside it is ever read or written. |
|
|
36
|
+
| `--dry-run` | print what would change; write nothing. |
|
|
37
|
+
| `--receipt <file>` | write the receipt as JSON. |
|
|
38
|
+
| `--strict` | exit 2 when any path is pending. |
|
|
39
|
+
| `--overwrite-helper` | replace a `bcms-content` helper whose bytes this tool did not write — including one of ours that has since been edited. |
|
|
40
|
+
| `--verbose` | stack traces, and every file considered. |
|
|
41
|
+
|
|
42
|
+
## What it actually does
|
|
43
|
+
|
|
44
|
+
**Parser offsets in, one `magic-string` splice out.** The tree is never re-serialised, so your
|
|
45
|
+
formatting, comments and directives survive and the diff is one a human can read. `parse5` reads
|
|
46
|
+
html, `@astrojs/compiler` reads astro, `@babel/parser` reads jsx/tsx, `svelte/compiler` reads
|
|
47
|
+
svelte and `@vue/compiler-sfc` reads vue — five dialects, one neutral tree, one matcher.
|
|
48
|
+
|
|
49
|
+
**Values are read at BUILD time, page-scoped.** A converted page statically imports
|
|
50
|
+
`bcms-content/<page>.json` and calls a ~30-line helper this package commits. Every bundler resolves
|
|
51
|
+
that at build time — no `fs`, no fetch, no key, and only that page's content can reach its bundle.
|
|
52
|
+
The committed stubs are `{}`, so a build with them renders every fallback and still succeeds; the
|
|
53
|
+
platform overwrites them before a real build. The consequence is worth stating plainly: outside the
|
|
54
|
+
visual editor, seeing an edit means a rebuild.
|
|
55
|
+
|
|
56
|
+
**Three tiers per file, and a file is exactly one of them.** The dialect's parser; failing that, a
|
|
57
|
+
tolerant pass that will only place a literal occurring exactly once between a `>` and a `<`; failing
|
|
58
|
+
that, an injected `llmFallback` whose output is accepted only if it re-parses and declares exactly
|
|
59
|
+
the paths that were asked for. The CLI injects none — the agent running it is tier 3.
|
|
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:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
paths.declared === paths.rewritten + paths.alreadyDeclared + paths.pending.length
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
A path counts as rewritten only when EVERY located occurrence of it was. A value rendered by both
|
|
71
|
+
the home page and a shared header is two places a reader sees it, and converting one of them reads
|
|
72
|
+
as a finished job.
|
|
73
|
+
|
|
74
|
+
## As a library
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { convertSources } from "@bettercms-ai/convert";
|
|
78
|
+
|
|
79
|
+
const { files, receipt } = await convertSources(brief, sources);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Pure: no filesystem, no network, no credential, no clock. `cli.ts` is the only module that touches a
|
|
83
|
+
disk. That is what lets the interesting half — which element holds which path, and why one could not
|
|
84
|
+
be found — be tested without a repository.
|
|
85
|
+
|
|
86
|
+
**A repeater becomes a loop, not three stamps.** Three identical siblings the derive lane called
|
|
87
|
+
`cards[i].*` are rewritten as one row inside the dialect's loop, reading
|
|
88
|
+
`bcmsRows(page, "cards", [ …every row's copy… ])` — so a card added in the dashboard renders, and a
|
|
89
|
+
build against the committed stub still renders the three the repository has. The bindings there are
|
|
90
|
+
template literals (`` data-bcms-field={`cards[${i}].title`} ``), the receipt lists each one, and the
|
|
91
|
+
proposer re-derives them from its own scan rather than believing the receipt. A group that cannot
|
|
92
|
+
become a loop — non-consecutive siblings, one row shaped differently, the html dialect — is still
|
|
93
|
+
declared and read per index and reported `REPEATER_FIXED_LENGTH`.
|
|
94
|
+
|
|
95
|
+
**A prop-drilled literal is converted in two hops, in one proposal.** `<Hero title="…" />` becomes
|
|
96
|
+
`title={bcms(…)}` plus `bcmsBindings={{ title: "hero.title" }}`, and the `<h1>{title}</h1>` inside
|
|
97
|
+
`Hero` — resolved through the file's own imports, relative or through the project's tsconfig
|
|
98
|
+
`paths` — gets `data-bcms-field={bcmsBindings?.title}`, the prop added to its destructuring, and
|
|
99
|
+
`bcmsBindings?: Record<string, string>` added to its `Props` type when it has one. A chain through
|
|
100
|
+
intermediate components is followed for four components — including a `{...props}` spread, which is
|
|
101
|
+
how most of them forward — with the object handed over explicitly at each call site; deeper is
|
|
102
|
+
`PROP_DRILLED_DEEP`. Which component is analysed comes from the IMPORT, so a module exporting a
|
|
103
|
+
`Hero` and a `HeroSkeleton` gets the declaration on the one the page renders. If the component cannot be resolved or nothing in it renders the prop, hop
|
|
104
|
+
1 still reads from the CMS, the bindings prop is DROPPED, and the path is `PROP_TARGET_NOT_FOUND`.
|
|
105
|
+
|
|
106
|
+
**The shared chrome is one field, not one per route.** A path the brief marks `scope: "layout"` is
|
|
107
|
+
read with `bcmsLayout("<address>", "…")` and declared on `data-bcms-layout-field` — the chrome
|
|
108
|
+
lane's own attribute, so the block lane's `[data-bcms-field]` query cannot reach it — and it is
|
|
109
|
+
counted once however many pages list it.
|
|
110
|
+
|
|
111
|
+
**A dynamic route reads its own entry.** `[slug].astro` appends `Astro.params.slug`, SvelteKit
|
|
112
|
+
`$page.params.slug` (importing the store), Nuxt `useRoute().params.slug`. A Next `[slug]/page.tsx`
|
|
113
|
+
gets a TYPED `{ params }: { params: Promise<{ slug: string }> }` on its page component, is made
|
|
114
|
+
`async` if it is not already, and reads `(await params).slug` — because `params` is a promise, and
|
|
115
|
+
reading `.slug` off one is `undefined` on every request. A `"use client"` page cannot be made async
|
|
116
|
+
and a signature this cannot edit is not guessed at: both are `DYNAMIC_PARAMS_UNAVAILABLE` and are
|
|
117
|
+
left alone. A repeater on a dynamic route passes the same parameter to `bcmsRows`.
|
|
118
|
+
|
|
119
|
+
## Not here yet
|
|
120
|
+
|
|
121
|
+
`promoteSharedChrome`, and prop drilling through a svelte or vue component (both parse, and both
|
|
122
|
+
bind their own copy; only `findPropTargets` is jsx/astro-only, so a drilled prop there is
|
|
123
|
+
`PROP_TARGET_NOT_FOUND` rather than a wrong edit).
|