@bettercms-ai/convert 0.8.0 → 0.10.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/dist/index.d.ts CHANGED
@@ -37,6 +37,18 @@ interface BriefPath {
37
37
  kind: string;
38
38
  /** The declared field type, which `kind` deliberately loses (richtext vs document). */
39
39
  type: string;
40
+ /**
41
+ * The richtext value IS the element's whole inline subtree — the field's `config.inline`.
42
+ *
43
+ * A heading with a styled run (`<h1>Notes from a <span class="serif">web studio</span></h1>`)
44
+ * derives as ONE richtext field whose value is that innerHTML, not as the text node beside the
45
+ * span. Without this the codemod cannot tell it from the icon+text shape, whose value really is
46
+ * one text node — and it wraps the text node in a `<span>`, leaving the styled run rendered
47
+ * twice. Optional on purpose: a brief written before the distinction existed carries none, and
48
+ * the old wrap-the-text-node reading is still the right answer for it.
49
+ * @see derive-schema.ts textType, which mints `config: { inline: true }` for exactly this shape.
50
+ */
51
+ inline?: true;
40
52
  label: string;
41
53
  /** The copy the REPO renders today. Null when the import captured no default. */
42
54
  original: string | null;
@@ -56,6 +68,11 @@ interface BriefPage {
56
68
  /** Only what the codemod reads. A backend `ConversionBrief` satisfies it. */
57
69
  interface Brief {
58
70
  pages: BriefPage[];
71
+ /**
72
+ * The project's forms, for the `--forms` lane. OPTIONAL: a brief issued before that lane
73
+ * existed carries none, and a run over one simply has no form to wire. @see BriefForm
74
+ */
75
+ forms?: BriefForm[];
59
76
  }
60
77
  /**
61
78
  * The name of one brief — `(route, path, kind, original)` for every page, hashed.
@@ -72,6 +89,29 @@ interface Brief {
72
89
  * coverage meter read zero forever. `brief-digest.test.ts` pins both against one literal hash.
73
90
  */
74
91
  declare function briefDigest(brief: Brief): string;
92
+ /**
93
+ * One CMS form, as the brief lists it — the half the codemod cannot derive from the repository.
94
+ *
95
+ * The `<form>` in the source and the row in the Forms tab are two halves of one thing, and only
96
+ * the platform knows the id that joins them. `submitUrl` is the absolute endpoint a browser posts
97
+ * to (`packages/sdk/src/forms.ts`), so a wired form needs no helper, no import and no snapshot:
98
+ * it posts to a URL. `status` is carried because a DRAFT form 403s that endpoint — wiring one is
99
+ * correct and incomplete, and the receipt says so rather than leaving a live form that silently
100
+ * rejects every visitor.
101
+ */
102
+ interface BriefForm {
103
+ id: string;
104
+ name: string;
105
+ status: "draft" | "published";
106
+ /** `/api/v1/forms/public/<id>/submissions`, absolute or origin-relative as the brief states it. */
107
+ submitUrl: string;
108
+ successMessage?: string;
109
+ /** The control this form expects, by `name`. `type` is informative; the key is the match. */
110
+ fields: {
111
+ key: string;
112
+ type: string;
113
+ }[];
114
+ }
75
115
 
76
116
  /**
77
117
  * Where in the source does each of the brief's paths actually appear?
@@ -114,8 +154,13 @@ interface UnlocatedPath {
114
154
  * correctly formatted file, which is every file.
115
155
  */
116
156
  declare const flat: (value: string) => string;
117
- /** The same text with tags removed — what a reader sees, not what the file says. */
118
- declare const stripTags: (value: string) => string;
157
+ /**
158
+ * The same text with tags removed — what a reader sees, not what the file says.
159
+ *
160
+ * `br` is what a line break becomes. A SPACE is what a reader sees; the empty string is what
161
+ * `textContent` sees, and the matcher works on the latter. @see locate, which searches both.
162
+ */
163
+ declare const stripTags: (value: string, br?: string) => string;
119
164
  interface BriefLike {
120
165
  pages: {
121
166
  route: string;
@@ -164,7 +209,9 @@ declare function locate(brief: BriefLike, sources: SourceFile[], options?: Locat
164
209
  * sentence appears in three files" and "this field is bound" are different questions — and a path
165
210
  * is only `rewritten` when EVERY located occurrence of it was.
166
211
  */
167
- type PendingReason = "NO_ORIGINAL" | "NOT_IN_SOURCE" | "DIALECT_UNSUPPORTED" | "IN_EXPRESSION" | "IN_SCRIPT_OR_COMMENT" | "IN_DATA_FILE" | "AMBIGUOUS_LITERAL" | "KIND_MISMATCH" | "SUBSTRING_ONLY" | "PROP_TARGET_NOT_FOUND" | "PROP_DRILLED_DEEP" | "REPEATER_FIXED_LENGTH"
212
+ type PendingReason = "NO_ORIGINAL" | "NOT_IN_SOURCE" | "DIALECT_UNSUPPORTED" | "IN_EXPRESSION" | "IN_SCRIPT_OR_COMMENT" | "IN_DATA_FILE" | "AMBIGUOUS_LITERAL" | "KIND_MISMATCH" | "SUBSTRING_ONLY"
213
+ /** An INLINE richtext value whose element holds a component, a `<slot>` or an expression. */
214
+ | "SUBTREE_NOT_HTML" | "PROP_TARGET_NOT_FOUND" | "PROP_DRILLED_DEEP" | "REPEATER_FIXED_LENGTH"
168
215
  /** One array declaration, two loops over it: the row this leaf belongs to has two homes. */
169
216
  | "REPEATER_AMBIGUOUS"
170
217
  /** An image whose built url maps to no repository asset, or to more than one. */
@@ -255,7 +302,59 @@ interface ConversionReceipt {
255
302
  lane: "bridge" | "pending";
256
303
  files?: string[];
257
304
  };
305
+ /**
306
+ * What `--forms` wired, and what it could not. OPTIONAL, and ADDITIVE ON PURPOSE: the server's
307
+ * `receiptInvariantHolds` reads `paths` only, so a receipt carrying this is accepted verbatim by
308
+ * a server that has never heard of the lane. @see FormsReceipt
309
+ */
310
+ forms?: FormsReceipt;
258
311
  }
312
+ /**
313
+ * Why one CMS form could not be wired to a `<form>` in the repository.
314
+ *
315
+ * `FORM_NOT_IN_SOURCE` no `<form>` in any file this run read holds any of its fields.
316
+ * `FORM_AMBIGUOUS` two `<form>`s hold its fields, or two CMS forms want the same one.
317
+ * `FIELD_UNMATCHED` a `<form>` is plainly the one, but a field's key names no control in it.
318
+ * `ACTION_IS_EXPRESSION` the `<form>`'s `action` is code; replacing it would rewrite a program.
319
+ * `DIALECT_UNSUPPORTED` the `<form>` is in a svelte/vue file, which this lane does not write.
320
+ * `PARSE_ERROR` the file holding it did not parse.
321
+ */
322
+ type FormPendingReason = "FORM_NOT_IN_SOURCE" | "FORM_AMBIGUOUS" | "FIELD_UNMATCHED" | "ACTION_IS_EXPRESSION" | "DIALECT_UNSUPPORTED" | "PARSE_ERROR";
323
+ interface PendingForm {
324
+ id: string;
325
+ name: string;
326
+ reason: FormPendingReason;
327
+ /** The file the reason is about, when the reason is about one. */
328
+ file?: string;
329
+ /** What exactly could not be done. Informative; the reason is the contract. */
330
+ message?: string;
331
+ }
332
+ interface FormsReceipt {
333
+ /** A `<form>` this run pointed at the CMS: action, method, id, field markers and the script. */
334
+ wired: number;
335
+ /**
336
+ * Already ours, from a previous run — the `<form>` carries `data-bcms-form`.
337
+ *
338
+ * Its own bucket for the same reason componentize keeps one: a second run over a wired tree has
339
+ * to report `wired: 0, pending: []` AND still account for every form of the brief.
340
+ */
341
+ alreadyWired: number;
342
+ pending: PendingForm[];
343
+ /** Prose a human has to act on — a draft form still 403s every submission. */
344
+ notes?: string[];
345
+ }
346
+ /** Raised when the receipt does not account for every form. Never caught inside this package. */
347
+ declare class FormInvariantError extends Error {
348
+ readonly code = "FORM_INVARIANT";
349
+ constructor(message: string);
350
+ }
351
+ /**
352
+ * The invariant, checked where the receipt is built — the same shape as `assertSections`.
353
+ *
354
+ * A form in none of the buckets is a form the codemod forgot, and a forgotten form is a live site
355
+ * whose submit button does nothing at all.
356
+ */
357
+ declare function assertForms(total: number, forms: FormsReceipt): FormsReceipt;
259
358
  /**
260
359
  * One binding whose `data-bcms-field` is an expression, and where its value is written literally.
261
360
  *
@@ -435,6 +534,12 @@ interface Site {
435
534
  literal: string;
436
535
  /** The literal is only PART of what this element renders; rewriting it would eat the rest. */
437
536
  partial: boolean;
537
+ /**
538
+ * The subtree holds markup only the FRAMEWORK renders — a component, a `<slot>`, an expression.
539
+ *
540
+ * Absent on every site whose children are plain html, which is the common case. @see ownsSubtree
541
+ */
542
+ nonHtml?: true;
438
543
  /** Tag chain from the document root down to the element's parent, outermost first. */
439
544
  ancestors: string[];
440
545
  /** Index among the element's ELEMENT siblings. */
@@ -549,6 +654,8 @@ interface Rewrite {
549
654
  path: string;
550
655
  /** The brief's kind: "text" | "richtext" | "image". */
551
656
  kind: string;
657
+ /** The richtext value is the element's whole inline subtree. @see BriefPath.inline */
658
+ inline?: true;
552
659
  /** The copy the repo renders today, kept as the in-code fallback. */
553
660
  fallback: string;
554
661
  /** The identifier this page's snapshot is imported under. */
@@ -749,6 +856,32 @@ declare function helperSource(dialect: Dialect): string;
749
856
  */
750
857
  declare function isKnownHelper(content: string, dialect: Dialect): number | null;
751
858
 
859
+ /**
860
+ * The submit runtime, LIFTED VERBATIM from `src/lib/sites/render-page.ts` `FORM_SCRIPT`.
861
+ *
862
+ * 🔴 THE SAME BYTES, AND A TEST THAT SAYS SO. The hosted renderer and this codemod ship the same
863
+ * form contract to two different kinds of site, and the `cf-turnstile-response` hoist in the
864
+ * middle is the part that cannot drift: the token must travel as a TOP-LEVEL key and never inside
865
+ * `data`, or the ingest route reads `token === undefined` and — once any Turnstile secret resolves
866
+ * — answers 403 to every single submission. A copy that fell behind would turn enabling Turnstile
867
+ * into a kill switch for the customer's live form, on their own repository, where nothing here can
868
+ * see it. `src/__tests__/content/form-script-pin.test.ts` reads THIS FILE as text and asserts the
869
+ * renderer's constant occurs in it byte for byte, which is the only thing that keeps the copy a
870
+ * copy. Change it there first, then paste it here.
871
+ */
872
+ declare const FORM_SCRIPT = "<script>\ndocument.querySelectorAll('form[data-bcms-form]').forEach(function(f){\n f.addEventListener('submit',function(e){\n e.preventDefault();\n var msg=f.querySelector('.bcms-form-msg');\n var data={};\n // A checkboxes group posts one entry per checked box under the SAME name; the old\n // one-at-a-time assignment kept only the last, so \"pick many\" delivered one answer.\n // One checked box still arrives as a scalar \u2014 FormData cannot tell a group from a\n // lone field \u2014 which is exactly why lib/forms/validate.ts's chosenValues() accepts\n // both an array and a comma string.\n new FormData(f).forEach(function(v,k){\n if(k==='cf-turnstile-response')return;\n if(Object.prototype.hasOwnProperty.call(data,k))data[k]=(Array.isArray(data[k])?data[k]:[data[k]]).concat(v);\n else data[k]=v;\n });\n var body={data:data};\n var tok=f.querySelector('[name=\"cf-turnstile-response\"]');\n if(tok&&tok.value)body['cf-turnstile-response']=tok.value;\n fetch(f.action,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)})\n .then(function(r){return r.ok;})\n .then(function(ok){if(msg){msg.hidden=false;msg.textContent=ok?(f.getAttribute('data-bcms-form-success')||'Thanks! Your submission was received.'):'Sorry, something went wrong.';}if(ok)f.reset();})\n .catch(function(){if(msg){msg.hidden=false;msg.textContent='Sorry, something went wrong.';}});\n });\n});\n</script>";
873
+ /**
874
+ * Wire every form the brief lists, and say what could not be.
875
+ *
876
+ * PURE, like the rest of this package: files in as values, files out as values. The receipt is a
877
+ * `ConversionReceipt` with an empty path arithmetic and the lane's own account under `forms`, so
878
+ * `submit_conversion_receipt` takes it unchanged — the server's invariant reads `paths` only.
879
+ */
880
+ declare function wireForms(briefIn: Brief, sources: SourceFile[]): Promise<{
881
+ files: PlanFile[];
882
+ receipt: ConversionReceipt;
883
+ }>;
884
+
752
885
  /**
753
886
  * Copy that lives in a DATA LITERAL beside the markup, rendered through `.map`.
754
887
  *
@@ -1026,6 +1159,41 @@ declare const SOURCE_EXTENSIONS: string[];
1026
1159
  /** Is this repository-relative path a file worth opening? */
1027
1160
  declare function isSourceCandidate(path: string): boolean;
1028
1161
 
1162
+ /**
1163
+ * Text that is NOT copy, and is therefore outside both halves of the ratio.
1164
+ *
1165
+ * 🔴 IN THE DENOMINATOR TOO. Counting a `✓`, a `01`, the platform badge or a skip link as
1166
+ * unaddressable text would put a floor under every route's percentage that no amount of binding
1167
+ * could lift — and the target is a number someone has to be able to reach. They are not copy an
1168
+ * author edits, so they are not copy this measures.
1169
+ *
1170
+ * - a decorative glyph: two characters or fewer, a single non-letter symbol, or an ordinal
1171
+ * (`01`, `2.`, `3 /`) that numbers a list rather than saying anything;
1172
+ * - the platform badge every free site renders;
1173
+ * - a skip link, which exists for keyboards and is never edited.
1174
+ */
1175
+ declare const isDecorativeGlyph: (text: string) => boolean;
1176
+ interface XrayBucket {
1177
+ /** The element that renders it. */
1178
+ tag: string;
1179
+ /** Its landmark chain, outermost first — `main>ul`. */
1180
+ context: string;
1181
+ chars: number;
1182
+ nodes: number;
1183
+ /** Up to two examples, ≤ 50 characters each. */
1184
+ samples: string[];
1185
+ }
1186
+ interface Xray {
1187
+ /** Characters of visible copy, decorative glyphs excluded. */
1188
+ visible: number;
1189
+ /** Of those, the characters no binding owns. */
1190
+ unaddressable: number;
1191
+ /** The biggest buckets, largest first — at most 12. */
1192
+ buckets: XrayBucket[];
1193
+ }
1194
+ /** What a reader sees on this page, and how much of it no field owns. */
1195
+ declare function measureUnaddressable(html: string): Xray;
1196
+
1029
1197
  /**
1030
1198
  * The project's own `compilerOptions.paths`, when a tsconfig is among the files.
1031
1199
  *
@@ -1538,4 +1706,4 @@ declare function convertSources(briefIn: Brief, sources: SourceFile[], options?:
1538
1706
  receipt: ConversionReceipt;
1539
1707
  }>;
1540
1708
 
1541
- export { ASTRO_LANE_PENDING, type AstNode, type AttrBinding, type Brief, type BriefPage, type BriefPath, COMPONENT_DIR, CONTENT_DIR, type CanvasResult, type CanvasSource, type Carried, type ComponentizeOptions, type ComponentizePlan, type ComponentizePlanComponent, type ComponentizePlanField, type ComponentizePlanPage, type ComponentizePlanSection, type ComponentizeReceipt, type ConversionReceipt, ConvertError, type ConvertErrorCode, type ConvertOptions, DIALECT_RULES, type DataMember, type DataResult, type DeclaredPath, type Dialect, type DynamicBinding, type FileDeclaration, type FindSitesResult, HELPER_PATH, HELPER_VERSION, type ImageResult, type ImageTarget, type ImportedFrom, type LlmFallback, type LocatedPath, type Node, PARSE_FILE, type ParsedFile, type ParserError, type PathLocator, type PendingPath, type PendingReason, type PendingSection, type PlanFile, REGISTRY_MARKER, type ReceiptFile, ReceiptInvariantError, type ReceiptNote, type Rewrite, SECTIONS_LIB, SECTION_MARKER, SKIP_DIRS, SKIP_FILES, SOURCE_EXTENSIONS, SSR_DRAFT_RECIPE, STUB_CONTENT, SectionInvariantError, type SectionPendingReason, type SectionsReceipt, type Site, type SiteWhere, type SourceFile, type Splice, type TargetIdentity, type UnlocatedPath, type ValidationCode, ValidationError, aliasesFrom, assertReceipt, assertSections, assetCandidates, assetImport, attrsFor, bindDataLiterals, bindImages, briefDigest, canvasBridge, carryFor, componentizeSources, convertSources, convertedHere, coverageOf, declaringElements, dialectOf, exportedFunction, extendedConfigs, findSites, findSitesTolerant, flat, helperSource, importsIn, isDynamicRoute, isKnownHelper, isSourceCandidate, locate, overlapping, pageFilesFor, parseFile, propsAttribute, readBrief, readComponentizePlan, readDeclarations, readExpr, readPlan, relativeImport, resolveSpecifier, rewriteFile, routeOfFile, stripTags, unreadableConfigs, walkAst };
1709
+ export { ASTRO_LANE_PENDING, type AstNode, type AttrBinding, type Brief, type BriefForm, type BriefPage, type BriefPath, COMPONENT_DIR, CONTENT_DIR, type CanvasResult, type CanvasSource, type Carried, type ComponentizeOptions, type ComponentizePlan, type ComponentizePlanComponent, type ComponentizePlanField, type ComponentizePlanPage, type ComponentizePlanSection, type ComponentizeReceipt, type ConversionReceipt, ConvertError, type ConvertErrorCode, type ConvertOptions, DIALECT_RULES, type DataMember, type DataResult, type DeclaredPath, type Dialect, type DynamicBinding, FORM_SCRIPT, type FileDeclaration, type FindSitesResult, FormInvariantError, type FormPendingReason, type FormsReceipt, HELPER_PATH, HELPER_VERSION, type ImageResult, type ImageTarget, type ImportedFrom, type LlmFallback, type LocatedPath, type Node, PARSE_FILE, type ParsedFile, type ParserError, type PathLocator, type PendingForm, type PendingPath, type PendingReason, type PendingSection, type PlanFile, REGISTRY_MARKER, type ReceiptFile, ReceiptInvariantError, type ReceiptNote, type Rewrite, SECTIONS_LIB, SECTION_MARKER, SKIP_DIRS, SKIP_FILES, SOURCE_EXTENSIONS, SSR_DRAFT_RECIPE, STUB_CONTENT, SectionInvariantError, type SectionPendingReason, type SectionsReceipt, type Site, type SiteWhere, type SourceFile, type Splice, type TargetIdentity, type UnlocatedPath, type ValidationCode, ValidationError, type Xray, type XrayBucket, aliasesFrom, assertForms, assertReceipt, assertSections, assetCandidates, assetImport, attrsFor, bindDataLiterals, bindImages, briefDigest, canvasBridge, carryFor, componentizeSources, convertSources, convertedHere, coverageOf, declaringElements, dialectOf, exportedFunction, extendedConfigs, findSites, findSitesTolerant, flat, helperSource, importsIn, isDecorativeGlyph, isDynamicRoute, isKnownHelper, isSourceCandidate, locate, measureUnaddressable, overlapping, pageFilesFor, parseFile, propsAttribute, readBrief, readComponentizePlan, readDeclarations, readExpr, readPlan, relativeImport, resolveSpecifier, rewriteFile, routeOfFile, stripTags, unreadableConfigs, walkAst, wireForms };