@bettercms-ai/convert 0.11.0 → 0.12.1
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 +52 -0
- package/README.md +54 -0
- package/dist/cli.js +201 -22
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +89 -2
- package/dist/index.js +202 -20
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,57 @@
|
|
|
1
1
|
# @bettercms-ai/convert
|
|
2
2
|
|
|
3
|
+
## 0.12.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 6d826cf: The trunk-zero rule is per-country data, not a regex
|
|
8
|
+
|
|
9
|
+
`bcmsPhone` stripped a leading zero from every national number with a bare
|
|
10
|
+
`.replace(/^0/, "")`. That is only correct for countries that actually use a trunk
|
|
11
|
+
prefix. Where the leading digit is part of the number, the submitted value was
|
|
12
|
+
silently corrupted — one digit short, and wrong in a way nobody notices until a
|
|
13
|
+
customer cannot be called back.
|
|
14
|
+
|
|
15
|
+
The country list now carries a `trunk` flag derived from libphonenumber's own example
|
|
16
|
+
numbers, rendered as `data-trunk` on each option, and the zero is dropped only when
|
|
17
|
+
that country says it is a trunk prefix.
|
|
18
|
+
|
|
19
|
+
The inline script exists once per vanilla lane — the Astro component, the converter and
|
|
20
|
+
the server renderer — because each ships as inline script text on someone else's page
|
|
21
|
+
and cannot import at runtime. All three carry the identical change, which
|
|
22
|
+
`src/__tests__/content/form-script-pin.test.ts` pins byte for byte.
|
|
23
|
+
|
|
24
|
+
## 0.12.0
|
|
25
|
+
|
|
26
|
+
### Minor Changes
|
|
27
|
+
|
|
28
|
+
- a243594: Every pending path leaves with an exact fix, and a locator resolves each path sharing a literal.
|
|
29
|
+
|
|
30
|
+
`paths.pending[]` rows now carry `fix: { file, line, col?, snippet, action, why? }` — one action
|
|
31
|
+
out of a closed set (`wrap-span`, `declare-attr`, `bind-expression`, `declare-richtext`,
|
|
32
|
+
`bind-data`, `manual`), so "rerun until nothing is pending" is a loop an agent can finish instead
|
|
33
|
+
of a list of category names. A literal one element renders on several routes is named as shared
|
|
34
|
+
chrome rather than sent for an attribute that could only ever be one route's.
|
|
35
|
+
|
|
36
|
+
Positional resolution is now per PATH rather than per literal: one path with no locator used to
|
|
37
|
+
refuse the whole literal, so a page whose rows carried locators converted none of them.
|
|
38
|
+
|
|
39
|
+
Two paths whose locators cannot be told apart now place NEITHER. Resolution counts the targets
|
|
40
|
+
wanting each element before it assigns any, so it no longer depends on brief order: `plan` runs
|
|
41
|
+
per file, and claiming greedily let the first of two indistinguishable paths win in every file —
|
|
42
|
+
writing one path's `data-bcms-field` onto the other's element and reporting it bound.
|
|
43
|
+
|
|
44
|
+
An action that edits an element (`wrap-span`, `declare-attr`, `declare-richtext`) is never issued
|
|
45
|
+
at a position inside a frontmatter array or a `<script>`; there is no element there, so the fix is
|
|
46
|
+
`bind-data` on the row the copy actually lives in. A fix for a dynamic route names the template
|
|
47
|
+
that generates it instead of an empty file. Files that talk ABOUT the site — `*.test.*`,
|
|
48
|
+
`*.spec.*`, `__tests__/`, `__mocks__/`, root `*.config.*` — are no longer read, so a test
|
|
49
|
+
asserting on the hero line stops holding an already-declared path pending.
|
|
50
|
+
|
|
51
|
+
Fix positions are measured against the bytes the run WRITES, not the ones it read — the same run
|
|
52
|
+
inserts a helper import above most pending lines, so a receipt minted from the originals named a
|
|
53
|
+
line one or more short of the element on 616 of 948 Shatter rows.
|
|
54
|
+
|
|
3
55
|
## 0.11.0
|
|
4
56
|
|
|
5
57
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -184,6 +184,60 @@ the receipt's arithmetic is checked, not hoped for:
|
|
|
184
184
|
paths.declared === paths.rewritten + paths.alreadyDeclared + paths.pending.length
|
|
185
185
|
```
|
|
186
186
|
|
|
187
|
+
### What pending means — every row carries the edit, not the diagnosis
|
|
188
|
+
|
|
189
|
+
A reason is a diagnosis and nobody can act on one. So every `paths.pending[]` row also carries a
|
|
190
|
+
`fix`: the file, the 1-based line and column, the bytes that are there now, and ONE action out of
|
|
191
|
+
a closed set. That is what makes "run it again until nothing is pending" a loop an agent can
|
|
192
|
+
actually finish — and what `get_next_steps` prints, one line per path.
|
|
193
|
+
|
|
194
|
+
```jsonc
|
|
195
|
+
{ "route": "/about", "scope": "page", "path": "p-founded-in-2019-in", "kind": "text",
|
|
196
|
+
"reason": "DIALECT_UNSUPPORTED",
|
|
197
|
+
"fix": { "file": "src/data/facts.js", "line": 3, "col": 25,
|
|
198
|
+
"snippet": "export const founded = \"Founded in 2019 in Bristol.\";",
|
|
199
|
+
"action": "bind-data",
|
|
200
|
+
"why": "src/data/facts.js holds this copy as data — bind the element that renders it." } }
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
| action | what to type |
|
|
204
|
+
|---|---|
|
|
205
|
+
| `wrap-span` | the value shares its element with something else, or is rendered in a position no attribute reaches — give it a `<span data-bcms-field="…">` of its own. |
|
|
206
|
+
| `declare-attr` | the element is there and nothing said which path it is — add `data-bcms-field` to it at `file:line`. |
|
|
207
|
+
| `declare-richtext` | the container is fed markup (`set:html`, `dangerouslySetInnerHTML`) — bind it with the richtext helper. |
|
|
208
|
+
| `bind-expression` | the value is computed in the markup (a ternary, a template literal) — read the path there instead. |
|
|
209
|
+
| `bind-data` | the copy lives in a data module, a frontmatter array or a content-collection entry — bind THAT field, and the element that renders it. |
|
|
210
|
+
| `manual` | none of the above is true; `why` says what a human has to decide. |
|
|
211
|
+
|
|
212
|
+
Reason → action, and the two places the answer is not the obvious one:
|
|
213
|
+
|
|
214
|
+
| reason | action |
|
|
215
|
+
|---|---|
|
|
216
|
+
| `AMBIGUOUS_LITERAL`, one route | `declare-attr` — `why` names every candidate file. |
|
|
217
|
+
| `AMBIGUOUS_LITERAL`, **several routes** | `manual` — one element cannot carry N routes' bindings. This is SHARED CHROME the derive lane did not promote: a Layout field is one value for the whole site, and an attribute here would bind one route and silently freeze the rest. |
|
|
218
|
+
| `IN_EXPRESSION` in the markup | `bind-expression`, or `declare-richtext` for a richtext value. |
|
|
219
|
+
| `IN_EXPRESSION` **in the frontmatter or a `<script>`** | `bind-data` — the copy is a row in the file's own data, and the array is what gets bound, not the `{l.label}` that reads it. |
|
|
220
|
+
| `DIALECT_UNSUPPORTED` on `.md`/`.mdx` | `bind-data` — a content-collection body is the entry's `document` field. |
|
|
221
|
+
| `NOT_IN_SOURCE` / `NO_ORIGINAL` | `bind-data` when the copy is in a data module, else `manual`: the built page formats it and the source holds the input. |
|
|
222
|
+
| `PROP_TARGET_NOT_FOUND`, `PROP_DRILLED_DEEP`, `SUBSTRING_ONLY` | `wrap-span`. |
|
|
223
|
+
| `REPEATER_AMBIGUOUS`, `REPEATER_FIXED_LENGTH` | `declare-attr` on the row's own element. |
|
|
224
|
+
| `IMAGE_ASSET_UNRESOLVED`, `IN_DATA_FILE` | `bind-data`. |
|
|
225
|
+
| `SUBTREE_NOT_HTML` | `declare-richtext`. |
|
|
226
|
+
| `KIND_MISMATCH`, `IN_SCRIPT_OR_COMMENT`, `DYNAMIC_PARAMS_UNAVAILABLE`, `BRIEF_META_UNPLACED`, `PARSE_ERROR`, `TIER3_UNVERIFIABLE`, `CANVAS_BRIDGE_MANUAL`, `SSR_DRAFT_ROUTE` | `manual`, with the reason quoted in `why` — read the row's `message` for what blocked it. |
|
|
227
|
+
|
|
228
|
+
**A file that talks ABOUT the site is not a source file.** A path is rewritten only when every
|
|
229
|
+
located occurrence was, so a test asserting on the hero line, or a build config quoting the font
|
|
230
|
+
stack, used to hold an already-declared path pending. `*.test.*`, `*.spec.*`, `__tests__/`,
|
|
231
|
+
`__mocks__/` and root-level `*.config.*` are never opened, alongside `node_modules/` and `dist/`.
|
|
232
|
+
A data module under `src/` — `src/site.config.ts` — is not a build config and stays bindable.
|
|
233
|
+
|
|
234
|
+
**`--xray` and the receipt exclude the same things.** The xray measures a BUILT site's visible
|
|
235
|
+
copy and the receipt measures the brief's paths in SOURCE, so they never have the same
|
|
236
|
+
denominator — but neither counts a decorative glyph, a `<script>` or a `<style>` as copy
|
|
237
|
+
(`isDecorativeGlyph` is the one reader), and the two lane rows `CANVAS_BRIDGE_MANUAL` and
|
|
238
|
+
`SSR_DRAFT_ROUTE` ride `paths.pending` without entering the arithmetic above. A path the receipt
|
|
239
|
+
calls pending is copy the xray will still count as unaddressed; a path it calls rewritten is not.
|
|
240
|
+
|
|
187
241
|
A path counts as rewritten only when EVERY located occurrence of it was. A value rendered by both
|
|
188
242
|
the home page and a shared header is two places a reader sees it, and converting one of them reads
|
|
189
243
|
as a finished job.
|
package/dist/cli.js
CHANGED
|
@@ -5312,6 +5312,138 @@ function assertReceipt(receipt) {
|
|
|
5312
5312
|
return receipt;
|
|
5313
5313
|
}
|
|
5314
5314
|
|
|
5315
|
+
// src/fix.ts
|
|
5316
|
+
var MAX_SNIPPET = 160;
|
|
5317
|
+
var MAX_WORDS = 12;
|
|
5318
|
+
var escapeRegExp2 = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5319
|
+
function findLiteral(content, literal) {
|
|
5320
|
+
const words = literal.split(/\s+/).filter(Boolean).slice(0, MAX_WORDS);
|
|
5321
|
+
if (words.length === 0) return null;
|
|
5322
|
+
const pattern = new RegExp(words.map(escapeRegExp2).join("(?:\\s|<[^>]*>)+"), "u");
|
|
5323
|
+
const match = pattern.exec(content);
|
|
5324
|
+
return match ? { index: match.index, length: match[0].length } : null;
|
|
5325
|
+
}
|
|
5326
|
+
function placeOf(content, index) {
|
|
5327
|
+
const before = content.slice(0, index);
|
|
5328
|
+
const line = before.split("\n").length;
|
|
5329
|
+
const start = before.lastIndexOf("\n") + 1;
|
|
5330
|
+
const end = content.indexOf("\n", index);
|
|
5331
|
+
return {
|
|
5332
|
+
line,
|
|
5333
|
+
col: index - start + 1,
|
|
5334
|
+
snippet: content.slice(start, end < 0 ? content.length : end).trim().slice(0, MAX_SNIPPET)
|
|
5335
|
+
};
|
|
5336
|
+
}
|
|
5337
|
+
function fileHead(content) {
|
|
5338
|
+
return { line: 1, snippet: (content ?? "").split("\n", 1)[0].trim().slice(0, MAX_SNIPPET) };
|
|
5339
|
+
}
|
|
5340
|
+
var isMarkdown = (file) => /\.mdx?$/i.test(file);
|
|
5341
|
+
var isDataModule = (file) => /\.(json|ya?ml|[mc]?[jt]s)$/i.test(file) && !/\.d\.ts$/i.test(file);
|
|
5342
|
+
function inDataRegion(content, index) {
|
|
5343
|
+
if (content.startsWith("---")) {
|
|
5344
|
+
const close = content.indexOf("\n---", 3);
|
|
5345
|
+
if (close >= 0 && index < close) return true;
|
|
5346
|
+
}
|
|
5347
|
+
for (const match of content.matchAll(/<script\b[^>]*>([\s\S]*?)<\/script>/gi)) {
|
|
5348
|
+
const start = match.index + match[0].indexOf(">") + 1;
|
|
5349
|
+
if (index >= start && index < start + match[1].length) return true;
|
|
5350
|
+
}
|
|
5351
|
+
return false;
|
|
5352
|
+
}
|
|
5353
|
+
var ELEMENT_ACTIONS = /* @__PURE__ */ new Set([
|
|
5354
|
+
"wrap-span",
|
|
5355
|
+
"declare-attr",
|
|
5356
|
+
"declare-richtext"
|
|
5357
|
+
]);
|
|
5358
|
+
function actionFor(reason, input, file, inData) {
|
|
5359
|
+
const chosen = reasonAction(reason, input, file, inData);
|
|
5360
|
+
if (inData && ELEMENT_ACTIONS.has(chosen.action)) {
|
|
5361
|
+
return { action: "bind-data", why: "The copy is a row in this file's own data \u2014 bind that field." };
|
|
5362
|
+
}
|
|
5363
|
+
return chosen;
|
|
5364
|
+
}
|
|
5365
|
+
function reasonAction(reason, input, file, inData) {
|
|
5366
|
+
const richtext = input.kind === "richtext" || input.inline === true;
|
|
5367
|
+
switch (reason) {
|
|
5368
|
+
case "AMBIGUOUS_LITERAL":
|
|
5369
|
+
if (input.routes > 1) {
|
|
5370
|
+
return {
|
|
5371
|
+
action: "manual",
|
|
5372
|
+
why: `${file} renders this value on ${input.routes} routes, so it is shared chrome rather than page copy: promote it to the project Layout (a Layout field is one value for the whole site) and rerun, or give each route its own element.`
|
|
5373
|
+
};
|
|
5374
|
+
}
|
|
5375
|
+
return {
|
|
5376
|
+
action: "declare-attr",
|
|
5377
|
+
why: input.candidates.length > 1 ? `This copy appears in ${input.candidates.join(", ")}; declare the one this route renders.` : "Two paths want this element and nothing says which \u2014 declare the right one by hand."
|
|
5378
|
+
};
|
|
5379
|
+
case "REPEATER_AMBIGUOUS":
|
|
5380
|
+
case "REPEATER_FIXED_LENGTH":
|
|
5381
|
+
return {
|
|
5382
|
+
action: "declare-attr",
|
|
5383
|
+
why: "Declare this row's leaf on the element itself, or rewrite the loop to read the CMS array."
|
|
5384
|
+
};
|
|
5385
|
+
case "IN_EXPRESSION":
|
|
5386
|
+
if (inData) {
|
|
5387
|
+
return { action: "bind-data", why: "The copy is a row in this file's own data \u2014 bind that field." };
|
|
5388
|
+
}
|
|
5389
|
+
return richtext ? { action: "declare-richtext", why: "The container is fed markup \u2014 bind it with the richtext helper." } : { action: "bind-expression", why: "Replace the computed value with the helper's read for this path." };
|
|
5390
|
+
case "DIALECT_UNSUPPORTED":
|
|
5391
|
+
if (isMarkdown(file)) {
|
|
5392
|
+
return {
|
|
5393
|
+
action: "bind-data",
|
|
5394
|
+
why: "This is a content-collection body: bind the element that renders it and make the entry the source."
|
|
5395
|
+
};
|
|
5396
|
+
}
|
|
5397
|
+
return isDataModule(file) ? { action: "bind-data", why: `${file} holds this copy as data \u2014 bind the element that renders it.` } : { action: "manual", why: `No parser here reads ${file}, so the declaration has to be written by hand.` };
|
|
5398
|
+
case "NOT_IN_SOURCE":
|
|
5399
|
+
case "NO_ORIGINAL":
|
|
5400
|
+
return isDataModule(file) || inData ? { action: "bind-data", why: "The template computes this from data \u2014 bind the data field instead of the text." } : {
|
|
5401
|
+
action: "manual",
|
|
5402
|
+
why: "The built page renders this and no source file holds it verbatim; find what formats it."
|
|
5403
|
+
};
|
|
5404
|
+
case "IN_DATA_FILE":
|
|
5405
|
+
case "IMAGE_ASSET_UNRESOLVED":
|
|
5406
|
+
return { action: "bind-data", why: "Bind the data field this value comes from." };
|
|
5407
|
+
case "PROP_TARGET_NOT_FOUND":
|
|
5408
|
+
case "PROP_DRILLED_DEEP":
|
|
5409
|
+
return {
|
|
5410
|
+
action: "wrap-span",
|
|
5411
|
+
why: "The component renders this in a position no attribute reaches \u2014 wrap it in a span of its own."
|
|
5412
|
+
};
|
|
5413
|
+
case "SUBSTRING_ONLY":
|
|
5414
|
+
return { action: "wrap-span", why: "Only part of the element's text is this value \u2014 give it its own span." };
|
|
5415
|
+
case "SUBTREE_NOT_HTML":
|
|
5416
|
+
return { action: "declare-richtext", why: "The value is the element's whole subtree \u2014 bind it as richtext." };
|
|
5417
|
+
default:
|
|
5418
|
+
return { action: "manual", why: `${reason} \u2014 see the receipt's message for what blocked this path.` };
|
|
5419
|
+
}
|
|
5420
|
+
}
|
|
5421
|
+
function fixFor(row, input) {
|
|
5422
|
+
const file = row.file ?? input.candidates[0] ?? "";
|
|
5423
|
+
const content = input.contents.get(file);
|
|
5424
|
+
const at = content ? findLiteral(content, input.literal) : null;
|
|
5425
|
+
const place = at ? placeOf(content, at.index) : { ...fileHead(content), col: void 0 };
|
|
5426
|
+
const { action, why } = actionFor(row.reason, input, file, at !== null && inDataRegion(content, at.index));
|
|
5427
|
+
return {
|
|
5428
|
+
file,
|
|
5429
|
+
line: row.line ?? place.line,
|
|
5430
|
+
...place.col !== void 0 ? { col: place.col } : {},
|
|
5431
|
+
snippet: place.snippet,
|
|
5432
|
+
action,
|
|
5433
|
+
...why ? { why } : {}
|
|
5434
|
+
};
|
|
5435
|
+
}
|
|
5436
|
+
function routeCounts(pending) {
|
|
5437
|
+
const routes = /* @__PURE__ */ new Map();
|
|
5438
|
+
for (const row of pending) {
|
|
5439
|
+
const id = `${row.scope}|${row.path}`;
|
|
5440
|
+
const held = routes.get(id) ?? /* @__PURE__ */ new Set();
|
|
5441
|
+
held.add(row.route);
|
|
5442
|
+
routes.set(id, held);
|
|
5443
|
+
}
|
|
5444
|
+
return new Map([...routes].map(([id, set]) => [id, set.size]));
|
|
5445
|
+
}
|
|
5446
|
+
|
|
5315
5447
|
// src/sites/html.ts
|
|
5316
5448
|
import { parse as parseHtml } from "parse5";
|
|
5317
5449
|
var RAW_TAGS = /* @__PURE__ */ new Set(["script", "style", "noscript", "template"]);
|
|
@@ -6189,6 +6321,7 @@ import MagicString4 from "magic-string";
|
|
|
6189
6321
|
var WIRABLE = ["html", "astro", "jsx"];
|
|
6190
6322
|
var CONTROLS = /* @__PURE__ */ new Set(["input", "select", "textarea"]);
|
|
6191
6323
|
var FORM_SCRIPT = `<script>
|
|
6324
|
+
function bcmsPhone(cc,raw,trunk){var d=String(raw||'').trim();if(!d)return '';if(d.charAt(0)==='+'){var i=d.replace(/D/g,'');return i?'+'+i:'';}if(!cc)return d;var n=d.replace(/D/g,'');if(trunk&&n.charAt(0)==='0')n=n.slice(1);return n?'+'+cc+n:'';}
|
|
6192
6325
|
document.querySelectorAll('form[data-bcms-form]').forEach(function(f){
|
|
6193
6326
|
f.addEventListener('submit',function(e){
|
|
6194
6327
|
e.preventDefault();
|
|
@@ -6204,6 +6337,15 @@ document.querySelectorAll('form[data-bcms-form]').forEach(function(f){
|
|
|
6204
6337
|
if(Object.prototype.hasOwnProperty.call(data,k))data[k]=(Array.isArray(data[k])?data[k]:[data[k]]).concat(v);
|
|
6205
6338
|
else data[k]=v;
|
|
6206
6339
|
});
|
|
6340
|
+
// Country-picker phone fields post ONE key. The __country select is scaffolding: it is
|
|
6341
|
+
// composed into the field's own value here, AFTER collection, so a showIf that points at a
|
|
6342
|
+
// phone key still compares the raw digits. Then it is removed from the body.
|
|
6343
|
+
f.querySelectorAll('select[name$="__country"]').forEach(function(sel){
|
|
6344
|
+
var k=sel.name.slice(0,-9);
|
|
6345
|
+
var o=sel.options[sel.selectedIndex];
|
|
6346
|
+
data[k]=bcmsPhone(o?(o.getAttribute('data-cc')||''):'',data[k],o?o.getAttribute('data-trunk')==='1':false);
|
|
6347
|
+
delete data[k+'__country'];
|
|
6348
|
+
});
|
|
6207
6349
|
var body={data:data};
|
|
6208
6350
|
var tok=f.querySelector('[name="cf-turnstile-response"]');
|
|
6209
6351
|
if(tok&&tok.value)body['cf-turnstile-response']=tok.value;
|
|
@@ -6428,7 +6570,9 @@ var SKIP_DIRS = [
|
|
|
6428
6570
|
".vercel/",
|
|
6429
6571
|
".cache/",
|
|
6430
6572
|
"coverage/",
|
|
6431
|
-
"vendor/"
|
|
6573
|
+
"vendor/",
|
|
6574
|
+
"__tests__/",
|
|
6575
|
+
"__mocks__/"
|
|
6432
6576
|
];
|
|
6433
6577
|
var SKIP_FILES = ["package-lock.json", "bun.lock", "bun.lockb", "yarn.lock", "pnpm-lock.yaml"];
|
|
6434
6578
|
var SOURCE_EXTENSIONS = [
|
|
@@ -6466,11 +6610,13 @@ var TEMPLATE_DIRS = /* @__PURE__ */ new Set([
|
|
|
6466
6610
|
"_includes",
|
|
6467
6611
|
"_layouts"
|
|
6468
6612
|
]);
|
|
6613
|
+
var isAboutTheSite = (lower) => /\.(test|spec)\.[^./]+$/.test(lower) || /^[^/]*\.config\.[^./]+$/.test(lower);
|
|
6469
6614
|
function isSourceCandidate(path) {
|
|
6470
6615
|
const lower = path.toLowerCase();
|
|
6471
6616
|
if (SKIP_DIRS.some((dir) => lower === dir.slice(0, -1) || lower.startsWith(dir) || lower.includes(`/${dir}`)))
|
|
6472
6617
|
return false;
|
|
6473
6618
|
if (SKIP_FILES.includes(lower.split("/").pop() ?? "")) return false;
|
|
6619
|
+
if (isAboutTheSite(lower)) return false;
|
|
6474
6620
|
if (/(^|\/)[jt]sconfig\.json$/.test(lower)) return true;
|
|
6475
6621
|
if (!SOURCE_EXTENSIONS.some((ext) => lower.endsWith(ext))) return false;
|
|
6476
6622
|
if (!PROSE_EXTENSIONS.some((ext) => lower.endsWith(ext))) return true;
|
|
@@ -8411,7 +8557,7 @@ var ConvertError = class extends Error {
|
|
|
8411
8557
|
code;
|
|
8412
8558
|
};
|
|
8413
8559
|
var keyOf = (route, scope, path) => scope === "layout" ? `layout::${path}` : `${route}::page::${path}`;
|
|
8414
|
-
var
|
|
8560
|
+
var escapeRegExp3 = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
8415
8561
|
var identifierFor2 = (slug) => `bcms${slug.replace(/[^A-Za-z0-9]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^./, (c) => c.toUpperCase())}`;
|
|
8416
8562
|
var UNPLACED = /* @__PURE__ */ new Set(["NOT_IN_SOURCE", "IN_EXPRESSION"]);
|
|
8417
8563
|
async function drillProp(ctx, fromFile, component, prop, kind, hop) {
|
|
@@ -8758,6 +8904,33 @@ async function convertSources(briefIn, sources, options) {
|
|
|
8758
8904
|
else outFiles.push({ path, operation: "modify", content });
|
|
8759
8905
|
}
|
|
8760
8906
|
pending.push(...canvas.pending);
|
|
8907
|
+
for (const file of outFiles) if (file.operation === "modify") latest.set(file.path, file.content);
|
|
8908
|
+
const pageFilesOrTemplate = (route, files) => {
|
|
8909
|
+
const all = [...files];
|
|
8910
|
+
const exact = pageFilesFor(route, all);
|
|
8911
|
+
if (exact.length > 0) return exact;
|
|
8912
|
+
const wanted = route.split("/").filter(Boolean);
|
|
8913
|
+
return all.filter((file) => {
|
|
8914
|
+
if (!isDynamicRoute(file)) return false;
|
|
8915
|
+
const pattern = routeOfFile(file)?.split("/").filter(Boolean);
|
|
8916
|
+
if (!pattern) return false;
|
|
8917
|
+
const rest = pattern.some((segment) => segment.startsWith("[..."));
|
|
8918
|
+
if (rest ? wanted.length < pattern.length : wanted.length !== pattern.length) return false;
|
|
8919
|
+
return pattern.every((segment, i) => segment.startsWith("[") || segment === wanted[i]);
|
|
8920
|
+
}).sort();
|
|
8921
|
+
};
|
|
8922
|
+
const routes = routeCounts(pending);
|
|
8923
|
+
for (const row of pending) {
|
|
8924
|
+
const target = targets.get(keyOf(row.route, row.scope, row.path));
|
|
8925
|
+
row.fix = fixFor(row, {
|
|
8926
|
+
literal: target?.literal ?? "",
|
|
8927
|
+
kind: row.kind,
|
|
8928
|
+
...target?.inline ? { inline: true } : {},
|
|
8929
|
+
candidates: locatedByKey.get(keyOf(row.route, row.scope, row.path))?.files ?? pageFilesOrTemplate(row.route, latest.keys()),
|
|
8930
|
+
contents: latest,
|
|
8931
|
+
routes: routes.get(`${row.scope}|${row.path}`) ?? 1
|
|
8932
|
+
});
|
|
8933
|
+
}
|
|
8761
8934
|
const receipt = assertReceipt({
|
|
8762
8935
|
briefDigest: digest,
|
|
8763
8936
|
paths: {
|
|
@@ -8970,13 +9143,13 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
8970
9143
|
for (const target of sharing) outcomes.set(target.key, missing(literal));
|
|
8971
9144
|
continue;
|
|
8972
9145
|
}
|
|
8973
|
-
const
|
|
8974
|
-
if (!assigned) {
|
|
8975
|
-
for (const target of sharing) outcomes.set(target.key, "AMBIGUOUS_LITERAL");
|
|
8976
|
-
continue;
|
|
8977
|
-
}
|
|
9146
|
+
const resolved = sharing.length === 1 ? { assigned: /* @__PURE__ */ new Map([[sharing[0].key, forLiteral]]), ambiguous: /* @__PURE__ */ new Set() } : resolvePositionally(sharing, forLiteral);
|
|
8978
9147
|
for (const target of sharing) {
|
|
8979
|
-
|
|
9148
|
+
if (resolved.ambiguous.has(target.key)) {
|
|
9149
|
+
outcomes.set(target.key, "AMBIGUOUS_LITERAL");
|
|
9150
|
+
continue;
|
|
9151
|
+
}
|
|
9152
|
+
const mine = resolved.assigned.get(target.key) ?? [];
|
|
8980
9153
|
const rewritable = mine.filter((s) => s.where !== "script" && s.where !== "comment" && !s.partial);
|
|
8981
9154
|
if (rewritable.length === 0) {
|
|
8982
9155
|
outcomes.set(
|
|
@@ -9321,19 +9494,25 @@ function repeatGroups(targets) {
|
|
|
9321
9494
|
}
|
|
9322
9495
|
function resolvePositionally(targets, sites) {
|
|
9323
9496
|
const assigned = /* @__PURE__ */ new Map();
|
|
9324
|
-
const
|
|
9497
|
+
const ambiguous = /* @__PURE__ */ new Set();
|
|
9498
|
+
const picked = /* @__PURE__ */ new Map();
|
|
9499
|
+
const wanters = /* @__PURE__ */ new Map();
|
|
9325
9500
|
for (const target of targets) {
|
|
9326
9501
|
const locator = target.locator;
|
|
9327
|
-
|
|
9328
|
-
const
|
|
9329
|
-
|
|
9330
|
-
|
|
9331
|
-
|
|
9332
|
-
|
|
9333
|
-
|
|
9334
|
-
|
|
9502
|
+
const candidates = locator ? sites.filter((s) => s.siblingIndex === locator.siblingIndex && isSubsequence(locator.ancestors, s.ancestors)) : [];
|
|
9503
|
+
const site = locator?.repeat ? candidates[locator.repeat.index] : candidates.length === 1 ? candidates[0] : void 0;
|
|
9504
|
+
if (!site) {
|
|
9505
|
+
ambiguous.add(target.key);
|
|
9506
|
+
continue;
|
|
9507
|
+
}
|
|
9508
|
+
picked.set(target.key, site);
|
|
9509
|
+
wanters.set(site, (wanters.get(site) ?? 0) + 1);
|
|
9510
|
+
}
|
|
9511
|
+
for (const [key, site] of picked) {
|
|
9512
|
+
if ((wanters.get(site) ?? 0) > 1) ambiguous.add(key);
|
|
9513
|
+
else assigned.set(key, [site]);
|
|
9335
9514
|
}
|
|
9336
|
-
return assigned;
|
|
9515
|
+
return { assigned, ambiguous };
|
|
9337
9516
|
}
|
|
9338
9517
|
function isSubsequence(wanted, chain) {
|
|
9339
9518
|
let at = 0;
|
|
@@ -9356,7 +9535,7 @@ async function tierThree(file, content, dialect, targets, llmFallback) {
|
|
|
9356
9535
|
if (elements.length !== targets.length) return { ok: false, reason: "unverifiable" };
|
|
9357
9536
|
const locals = helperLocals(reparsed, dialect, file);
|
|
9358
9537
|
const reads = DIALECT_RULES[dialect].text !== null;
|
|
9359
|
-
const boundary = (name) => `(?<![\\w$])${
|
|
9538
|
+
const boundary = (name) => `(?<![\\w$])${escapeRegExp3(name)}(?![\\w$])`;
|
|
9360
9539
|
const claimed = /* @__PURE__ */ new Set();
|
|
9361
9540
|
for (const element of elements) {
|
|
9362
9541
|
const candidates = targets.filter(
|
|
@@ -9364,7 +9543,7 @@ async function tierThree(file, content, dialect, targets, llmFallback) {
|
|
|
9364
9543
|
);
|
|
9365
9544
|
const match = candidates.find((target) => {
|
|
9366
9545
|
if (!reads) return true;
|
|
9367
|
-
const quoted =
|
|
9546
|
+
const quoted = escapeRegExp3(JSON.stringify(target.path));
|
|
9368
9547
|
if (target.scope === "layout") {
|
|
9369
9548
|
return locals.bcmsLayout.some(
|
|
9370
9549
|
(name) => new RegExp(`${boundary(name)}\\s*\\(\\s*${quoted}`).test(element.own)
|
|
@@ -9426,7 +9605,7 @@ function emitStubs2(dialects, slugs, contents, out) {
|
|
|
9426
9605
|
// package.json
|
|
9427
9606
|
var package_default = {
|
|
9428
9607
|
name: "@bettercms-ai/convert",
|
|
9429
|
-
version: "0.
|
|
9608
|
+
version: "0.12.1",
|
|
9430
9609
|
type: "module",
|
|
9431
9610
|
description: "Deterministic codemod that turns an imported site's text-matched bindings into declared ones \u2014 parser offsets in, one magic-string pass out, plus a receipt of everything it could not do.",
|
|
9432
9611
|
bin: {
|
|
@@ -9466,7 +9645,7 @@ var package_default = {
|
|
|
9466
9645
|
},
|
|
9467
9646
|
devDependencies: {
|
|
9468
9647
|
"@types/node": "^20",
|
|
9469
|
-
esbuild: "^0.
|
|
9648
|
+
esbuild: "^0.28.1",
|
|
9470
9649
|
tsup: "^8.5.1",
|
|
9471
9650
|
typescript: "^5",
|
|
9472
9651
|
vitest: "^4.1.4"
|