@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/dist/index.d.ts CHANGED
@@ -56,6 +56,11 @@ interface BriefPage {
56
56
  /** Only what the codemod reads. A backend `ConversionBrief` satisfies it. */
57
57
  interface Brief {
58
58
  pages: BriefPage[];
59
+ /**
60
+ * The project's forms, for the `--forms` lane. OPTIONAL: a brief issued before that lane
61
+ * existed carries none, and a run over one simply has no form to wire. @see BriefForm
62
+ */
63
+ forms?: BriefForm[];
59
64
  }
60
65
  /**
61
66
  * The name of one brief — `(route, path, kind, original)` for every page, hashed.
@@ -72,6 +77,29 @@ interface Brief {
72
77
  * coverage meter read zero forever. `brief-digest.test.ts` pins both against one literal hash.
73
78
  */
74
79
  declare function briefDigest(brief: Brief): string;
80
+ /**
81
+ * One CMS form, as the brief lists it — the half the codemod cannot derive from the repository.
82
+ *
83
+ * The `<form>` in the source and the row in the Forms tab are two halves of one thing, and only
84
+ * the platform knows the id that joins them. `submitUrl` is the absolute endpoint a browser posts
85
+ * to (`packages/sdk/src/forms.ts`), so a wired form needs no helper, no import and no snapshot:
86
+ * it posts to a URL. `status` is carried because a DRAFT form 403s that endpoint — wiring one is
87
+ * correct and incomplete, and the receipt says so rather than leaving a live form that silently
88
+ * rejects every visitor.
89
+ */
90
+ interface BriefForm {
91
+ id: string;
92
+ name: string;
93
+ status: "draft" | "published";
94
+ /** `/api/v1/forms/public/<id>/submissions`, absolute or origin-relative as the brief states it. */
95
+ submitUrl: string;
96
+ successMessage?: string;
97
+ /** The control this form expects, by `name`. `type` is informative; the key is the match. */
98
+ fields: {
99
+ key: string;
100
+ type: string;
101
+ }[];
102
+ }
75
103
 
76
104
  /**
77
105
  * Where in the source does each of the brief's paths actually appear?
@@ -255,7 +283,59 @@ interface ConversionReceipt {
255
283
  lane: "bridge" | "pending";
256
284
  files?: string[];
257
285
  };
286
+ /**
287
+ * What `--forms` wired, and what it could not. OPTIONAL, and ADDITIVE ON PURPOSE: the server's
288
+ * `receiptInvariantHolds` reads `paths` only, so a receipt carrying this is accepted verbatim by
289
+ * a server that has never heard of the lane. @see FormsReceipt
290
+ */
291
+ forms?: FormsReceipt;
292
+ }
293
+ /**
294
+ * Why one CMS form could not be wired to a `<form>` in the repository.
295
+ *
296
+ * `FORM_NOT_IN_SOURCE` no `<form>` in any file this run read holds any of its fields.
297
+ * `FORM_AMBIGUOUS` two `<form>`s hold its fields, or two CMS forms want the same one.
298
+ * `FIELD_UNMATCHED` a `<form>` is plainly the one, but a field's key names no control in it.
299
+ * `ACTION_IS_EXPRESSION` the `<form>`'s `action` is code; replacing it would rewrite a program.
300
+ * `DIALECT_UNSUPPORTED` the `<form>` is in a svelte/vue file, which this lane does not write.
301
+ * `PARSE_ERROR` the file holding it did not parse.
302
+ */
303
+ type FormPendingReason = "FORM_NOT_IN_SOURCE" | "FORM_AMBIGUOUS" | "FIELD_UNMATCHED" | "ACTION_IS_EXPRESSION" | "DIALECT_UNSUPPORTED" | "PARSE_ERROR";
304
+ interface PendingForm {
305
+ id: string;
306
+ name: string;
307
+ reason: FormPendingReason;
308
+ /** The file the reason is about, when the reason is about one. */
309
+ file?: string;
310
+ /** What exactly could not be done. Informative; the reason is the contract. */
311
+ message?: string;
312
+ }
313
+ interface FormsReceipt {
314
+ /** A `<form>` this run pointed at the CMS: action, method, id, field markers and the script. */
315
+ wired: number;
316
+ /**
317
+ * Already ours, from a previous run — the `<form>` carries `data-bcms-form`.
318
+ *
319
+ * Its own bucket for the same reason componentize keeps one: a second run over a wired tree has
320
+ * to report `wired: 0, pending: []` AND still account for every form of the brief.
321
+ */
322
+ alreadyWired: number;
323
+ pending: PendingForm[];
324
+ /** Prose a human has to act on — a draft form still 403s every submission. */
325
+ notes?: string[];
258
326
  }
327
+ /** Raised when the receipt does not account for every form. Never caught inside this package. */
328
+ declare class FormInvariantError extends Error {
329
+ readonly code = "FORM_INVARIANT";
330
+ constructor(message: string);
331
+ }
332
+ /**
333
+ * The invariant, checked where the receipt is built — the same shape as `assertSections`.
334
+ *
335
+ * A form in none of the buckets is a form the codemod forgot, and a forgotten form is a live site
336
+ * whose submit button does nothing at all.
337
+ */
338
+ declare function assertForms(total: number, forms: FormsReceipt): FormsReceipt;
259
339
  /**
260
340
  * One binding whose `data-bcms-field` is an expression, and where its value is written literally.
261
341
  *
@@ -283,9 +363,15 @@ interface DynamicBinding {
283
363
  * later reason may name the wrong cause. `TSCONFIG_UNREADABLE` is the first — a project config this
284
364
  * package could not parse declares no `paths` aliases as far as it is concerned, so every aliased
285
365
  * import resolves to nothing and the component behind it reads as one the page does not import.
366
+ *
367
+ * `PRIMITIVE_LIST` is the second, and it is about a CONVERSION rather than a silence: an array of
368
+ * primitives or tuples renders exactly the paths the codemod wrote into it, so every row's text is
369
+ * editable and the NUMBER of rows is still the template's — a row added in the CMS does not appear
370
+ * and a deleted one falls back to the template's own copy. The paths are `rewritten` and true; what
371
+ * the note adds is that ceiling. @see bindLists
286
372
  */
287
373
  interface ReceiptNote {
288
- code: "TSCONFIG_UNREADABLE";
374
+ code: "TSCONFIG_UNREADABLE" | "TSCONFIG_EXTENDS_ONLY" | "PRIMITIVE_LIST";
289
375
  file: string;
290
376
  message: string;
291
377
  }
@@ -716,7 +802,7 @@ declare function attrsFor(path: string, kind: string, props?: AttrBinding[], sco
716
802
  declare function propsAttribute(props: AttrBinding[]): string;
717
803
 
718
804
  /** Bumped whenever the emitted source changes. `isKnownHelper` upgrades anything older. */
719
- declare const HELPER_VERSION = 5;
805
+ declare const HELPER_VERSION = 6;
720
806
  /** Where each dialect's helper lives. Null = this dialect reads nothing at build time. */
721
807
  declare const HELPER_PATH: Record<Dialect, string | null>;
722
808
  /** The directory the snapshots live in, at the repository root. */
@@ -743,6 +829,32 @@ declare function helperSource(dialect: Dialect): string;
743
829
  */
744
830
  declare function isKnownHelper(content: string, dialect: Dialect): number | null;
745
831
 
832
+ /**
833
+ * The submit runtime, LIFTED VERBATIM from `src/lib/sites/render-page.ts` `FORM_SCRIPT`.
834
+ *
835
+ * 🔴 THE SAME BYTES, AND A TEST THAT SAYS SO. The hosted renderer and this codemod ship the same
836
+ * form contract to two different kinds of site, and the `cf-turnstile-response` hoist in the
837
+ * middle is the part that cannot drift: the token must travel as a TOP-LEVEL key and never inside
838
+ * `data`, or the ingest route reads `token === undefined` and — once any Turnstile secret resolves
839
+ * — answers 403 to every single submission. A copy that fell behind would turn enabling Turnstile
840
+ * into a kill switch for the customer's live form, on their own repository, where nothing here can
841
+ * see it. `src/__tests__/content/form-script-pin.test.ts` reads THIS FILE as text and asserts the
842
+ * renderer's constant occurs in it byte for byte, which is the only thing that keeps the copy a
843
+ * copy. Change it there first, then paste it here.
844
+ */
845
+ 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>";
846
+ /**
847
+ * Wire every form the brief lists, and say what could not be.
848
+ *
849
+ * PURE, like the rest of this package: files in as values, files out as values. The receipt is a
850
+ * `ConversionReceipt` with an empty path arithmetic and the lane's own account under `forms`, so
851
+ * `submit_conversion_receipt` takes it unchanged — the server's invariant reads `paths` only.
852
+ */
853
+ declare function wireForms(briefIn: Brief, sources: SourceFile[]): Promise<{
854
+ files: PlanFile[];
855
+ receipt: ConversionReceipt;
856
+ }>;
857
+
746
858
  /**
747
859
  * Copy that lives in a DATA LITERAL beside the markup, rendered through `.map`.
748
860
  *
@@ -794,18 +906,41 @@ interface DataSplice {
794
906
  end: number;
795
907
  text: string;
796
908
  }
909
+ /** What one drill did, or why it could not. @see drillProp, which is the implementation. */
910
+ type DrillOutcome = {
911
+ ok: true;
912
+ edits: {
913
+ file: string;
914
+ splices: DataSplice[];
915
+ }[];
916
+ } | {
917
+ ok: false;
918
+ reason: PendingReason;
919
+ };
797
920
  interface DataContext {
798
921
  content: string;
799
922
  parsed: ParsedFile;
800
923
  dialect: Dialect;
801
924
  /** The name `bcmsRowsAs` is imported under here. @see scope.ts */
802
925
  rowsAs: string;
926
+ /** The names `bcmsList` and `bcmsTuples` are imported under here. @see bindLists */
927
+ list: string;
928
+ tuples: string;
803
929
  /** The name `bcms` is imported under here — the scalar lane's read. @see scope.ts */
804
930
  read: string;
805
931
  /** The identifier a page's snapshot is imported under in this file. @see scope.ts */
806
932
  snapshotName: (slug: string) => string;
807
933
  /** How this file reads its route parameter, on a dynamic route. Null off one. */
808
934
  slug: string | null;
935
+ /**
936
+ * Follow one prop from a call site in THIS file into the component that renders it.
937
+ *
938
+ * Handed in rather than imported: the drill needs every source file, the alias table and the set
939
+ * of files this conversion already rewrites, and none of that is one file's business. Absent on
940
+ * a caller that cannot drill (the tests of this lane alone), which makes a row leaf passed as a
941
+ * prop the refusal it was before. @see index.ts
942
+ */
943
+ drill?: (component: string, prop: string, kind: string) => Promise<DrillOutcome>;
809
944
  }
810
945
  interface DataResult {
811
946
  splices: DataSplice[];
@@ -813,6 +948,16 @@ interface DataResult {
813
948
  usesRead: boolean;
814
949
  /** Did anything here write a `bcmsRowsAs(…)` read? */
815
950
  usesRows: boolean;
951
+ /** Did anything here write a `bcmsList(…)` / `bcmsTuples(…)` read? @see bindLists */
952
+ usesList: boolean;
953
+ usesTuples: boolean;
954
+ /**
955
+ * One sentence per list this lane bound, for the receipt's `PRIMITIVE_LIST` notes.
956
+ *
957
+ * The FILE is the caller's to add: this lane is handed one file's bytes and does not know its
958
+ * name, exactly as `findSites` does not. @see ReceiptNote
959
+ */
960
+ notes: string[];
816
961
  /** The page slugs whose snapshot this lane now reads, so the file imports them. */
817
962
  slugs: string[];
818
963
  /** Target keys this plan bound. */
@@ -827,6 +972,24 @@ interface DataResult {
827
972
  key: string;
828
973
  reason: PendingReason;
829
974
  }[];
975
+ /** Edits to the COMPONENT files this lane drilled a row leaf into. @see DataContext.drill */
976
+ componentEdits: {
977
+ file: string;
978
+ splices: DataSplice[];
979
+ }[];
980
+ /**
981
+ * One drilled row leaf: the component file that now declares it, and the call site's own entry.
982
+ *
983
+ * The receipt records both halves of a prop conversion — the element is in another file and the
984
+ * path it reads is at the call site — and a reader cannot verify either half from the other
985
+ * alone. @see DynamicBinding
986
+ */
987
+ props: {
988
+ file: string;
989
+ prop: string;
990
+ path: string;
991
+ literal: string;
992
+ }[];
830
993
  }
831
994
  /**
832
995
  * Bind every repeater group whose copy lives in a data literal in this same file.
@@ -835,7 +998,7 @@ interface DataResult {
835
998
  * account for are returned in `refused` with the reason, and are never partially written — a group
836
999
  * half bound is a row whose second field silently stops reflecting.
837
1000
  */
838
- declare function bindDataLiterals(ctx: DataContext, members: DataMember[]): DataResult;
1001
+ declare function bindDataLiterals(ctx: DataContext, members: DataMember[]): Promise<DataResult>;
839
1002
 
840
1003
  /**
841
1004
  * An image whose `original` is a BUILT url, and the import the template actually renders.
@@ -986,6 +1149,17 @@ declare function aliasesFrom(sources: SourceFile[]): Map<string, string[]>;
986
1149
  * The note says which file to look at. @see ConversionReceipt.notes
987
1150
  */
988
1151
  declare function unreadableConfigs(sources: SourceFile[]): string[];
1152
+ /**
1153
+ * Project configs whose aliases are NOT in them — they `extends` another file.
1154
+ *
1155
+ * 🔴 AN ABSENT ALIAS TABLE LOOKS EXACTLY LIKE A PROJECT THAT HAS NONE, and the two have opposite
1156
+ * consequences: the second converts relative imports and is complete, the first refuses every
1157
+ * component reached through `@components/*` with the file sitting right there. `extends` chains
1158
+ * are not followed — the base may be a published package (`@tsconfig/strict`) rather than a file
1159
+ * in the tree — so the honest answer is to say so in the receipt rather than to resolve nothing
1160
+ * silently. @see aliasesFrom
1161
+ */
1162
+ declare function extendedConfigs(sources: SourceFile[]): string[];
989
1163
  /** The repository path a specifier names, or null when it is not among the files we were given. */
990
1164
  declare function resolveSpecifier(fromFile: string, specifier: string, files: Set<string>, aliases: Map<string, string[]>): string | null;
991
1165
  /** What one local name was imported from, and WHICH export of it. */
@@ -1470,4 +1644,4 @@ declare function convertSources(briefIn: Brief, sources: SourceFile[], options?:
1470
1644
  receipt: ConversionReceipt;
1471
1645
  }>;
1472
1646
 
1473
- 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, 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 };
1647
+ 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, 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, isDynamicRoute, isKnownHelper, isSourceCandidate, locate, overlapping, pageFilesFor, parseFile, propsAttribute, readBrief, readComponentizePlan, readDeclarations, readExpr, readPlan, relativeImport, resolveSpecifier, rewriteFile, routeOfFile, stripTags, unreadableConfigs, walkAst, wireForms };