@bettercms-ai/convert 0.8.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/CHANGELOG.md +33 -0
- package/README.md +42 -0
- package/dist/cli.js +366 -9
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +107 -1
- package/dist/index.js +279 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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;
|
|
258
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[];
|
|
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
|
*
|
|
@@ -749,6 +829,32 @@ declare function helperSource(dialect: Dialect): string;
|
|
|
749
829
|
*/
|
|
750
830
|
declare function isKnownHelper(content: string, dialect: Dialect): number | null;
|
|
751
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
|
+
|
|
752
858
|
/**
|
|
753
859
|
* Copy that lives in a DATA LITERAL beside the markup, rendered through `.map`.
|
|
754
860
|
*
|
|
@@ -1538,4 +1644,4 @@ declare function convertSources(briefIn: Brief, sources: SourceFile[], options?:
|
|
|
1538
1644
|
receipt: ConversionReceipt;
|
|
1539
1645
|
}>;
|
|
1540
1646
|
|
|
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 };
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import
|
|
2
|
+
import MagicString6 from "magic-string";
|
|
3
3
|
|
|
4
4
|
// src/brief.ts
|
|
5
5
|
import { createHash } from "crypto";
|
|
@@ -2561,7 +2561,16 @@ var IMAGE_SOURCES = {
|
|
|
2561
2561
|
};
|
|
2562
2562
|
var isImageSource = (site) => site.where === "attr" && (IMAGE_SOURCES[site.tag.toLowerCase()] ?? []).includes(site.attr ?? "");
|
|
2563
2563
|
var norm = (value) => decodeEntities(value.normalize("NFC")).replace(/\s+/g, " ").trim();
|
|
2564
|
-
var BCMS_ATTRS = [
|
|
2564
|
+
var BCMS_ATTRS = [
|
|
2565
|
+
"data-bcms-field",
|
|
2566
|
+
"data-bcms-kind",
|
|
2567
|
+
"data-bcms-props",
|
|
2568
|
+
"data-bcms-layout-field",
|
|
2569
|
+
// The forms lane's two. Listed here so `collectSites` never matches a brief literal against a
|
|
2570
|
+
// marker this package itself wrote — a form id or a field key is not the page's copy.
|
|
2571
|
+
"data-bcms-form",
|
|
2572
|
+
"data-bcms-form-field"
|
|
2573
|
+
];
|
|
2565
2574
|
var attrInsertAfter = (attrs, start, tag) => attrs.length > 0 ? Math.max(...attrs.map((a) => a.range.end)) : start + 1 + tag.length;
|
|
2566
2575
|
function openTagEnd(content, start) {
|
|
2567
2576
|
let quote = null;
|
|
@@ -5160,6 +5169,22 @@ function importLines(imports) {
|
|
|
5160
5169
|
|
|
5161
5170
|
// src/receipt.ts
|
|
5162
5171
|
var CANVAS_REASONS = /* @__PURE__ */ new Set(["CANVAS_BRIDGE_MANUAL", "SSR_DRAFT_ROUTE"]);
|
|
5172
|
+
var FormInvariantError = class extends Error {
|
|
5173
|
+
code = "FORM_INVARIANT";
|
|
5174
|
+
constructor(message) {
|
|
5175
|
+
super(message);
|
|
5176
|
+
this.name = "FormInvariantError";
|
|
5177
|
+
}
|
|
5178
|
+
};
|
|
5179
|
+
function assertForms(total, forms) {
|
|
5180
|
+
const sum = forms.wired + forms.alreadyWired + forms.pending.length;
|
|
5181
|
+
if (sum !== total) {
|
|
5182
|
+
throw new FormInvariantError(
|
|
5183
|
+
`The receipt does not account for every form: ${total} listed but ${forms.wired} wired + ${forms.alreadyWired} already wired + ${forms.pending.length} pending = ${sum}.`
|
|
5184
|
+
);
|
|
5185
|
+
}
|
|
5186
|
+
return forms;
|
|
5187
|
+
}
|
|
5163
5188
|
var ReceiptInvariantError = class extends Error {
|
|
5164
5189
|
code = "RECEIPT_INVARIANT";
|
|
5165
5190
|
constructor(message) {
|
|
@@ -6011,6 +6036,17 @@ function readBrief(value) {
|
|
|
6011
6036
|
}
|
|
6012
6037
|
}
|
|
6013
6038
|
}
|
|
6039
|
+
for (const [at, entry] of check.array(brief.forms ?? [], `the brief's "forms"`).entries()) {
|
|
6040
|
+
const where = `brief.forms[${at}]`;
|
|
6041
|
+
const form = check.record(entry, where);
|
|
6042
|
+
check.segment(form.id, `${where}.id`);
|
|
6043
|
+
check.string(form.name, `${where}.name`);
|
|
6044
|
+
check.string(form.submitUrl, `${where}.submitUrl`);
|
|
6045
|
+
check.optionalString(form.successMessage, `${where}.successMessage`);
|
|
6046
|
+
for (const [n, held] of check.array(form.fields, `${where}.fields`).entries()) {
|
|
6047
|
+
check.segment(check.record(held, `${where}.fields[${n}]`).key, `${where}.fields[${n}].key`);
|
|
6048
|
+
}
|
|
6049
|
+
}
|
|
6014
6050
|
return brief;
|
|
6015
6051
|
}
|
|
6016
6052
|
function readPlan(value) {
|
|
@@ -6057,6 +6093,236 @@ function readPlan(value) {
|
|
|
6057
6093
|
return plan2;
|
|
6058
6094
|
}
|
|
6059
6095
|
|
|
6096
|
+
// src/forms.ts
|
|
6097
|
+
import MagicString4 from "magic-string";
|
|
6098
|
+
var WIRABLE = ["html", "astro", "jsx"];
|
|
6099
|
+
var CONTROLS = /* @__PURE__ */ new Set(["input", "select", "textarea"]);
|
|
6100
|
+
var FORM_SCRIPT = `<script>
|
|
6101
|
+
document.querySelectorAll('form[data-bcms-form]').forEach(function(f){
|
|
6102
|
+
f.addEventListener('submit',function(e){
|
|
6103
|
+
e.preventDefault();
|
|
6104
|
+
var msg=f.querySelector('.bcms-form-msg');
|
|
6105
|
+
var data={};
|
|
6106
|
+
// A checkboxes group posts one entry per checked box under the SAME name; the old
|
|
6107
|
+
// one-at-a-time assignment kept only the last, so "pick many" delivered one answer.
|
|
6108
|
+
// One checked box still arrives as a scalar \u2014 FormData cannot tell a group from a
|
|
6109
|
+
// lone field \u2014 which is exactly why lib/forms/validate.ts's chosenValues() accepts
|
|
6110
|
+
// both an array and a comma string.
|
|
6111
|
+
new FormData(f).forEach(function(v,k){
|
|
6112
|
+
if(k==='cf-turnstile-response')return;
|
|
6113
|
+
if(Object.prototype.hasOwnProperty.call(data,k))data[k]=(Array.isArray(data[k])?data[k]:[data[k]]).concat(v);
|
|
6114
|
+
else data[k]=v;
|
|
6115
|
+
});
|
|
6116
|
+
var body={data:data};
|
|
6117
|
+
var tok=f.querySelector('[name="cf-turnstile-response"]');
|
|
6118
|
+
if(tok&&tok.value)body['cf-turnstile-response']=tok.value;
|
|
6119
|
+
fetch(f.action,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)})
|
|
6120
|
+
.then(function(r){return r.ok;})
|
|
6121
|
+
.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();})
|
|
6122
|
+
.catch(function(){if(msg){msg.hidden=false;msg.textContent='Sorry, something went wrong.';}});
|
|
6123
|
+
});
|
|
6124
|
+
});
|
|
6125
|
+
</script>`;
|
|
6126
|
+
var SCRIPT_BODY = FORM_SCRIPT.slice("<script>".length, -"</script>".length);
|
|
6127
|
+
function scriptFor(dialect) {
|
|
6128
|
+
if (dialect === "jsx") return `<script dangerouslySetInnerHTML={{ __html: \`${SCRIPT_BODY}\` }} />`;
|
|
6129
|
+
if (dialect === "astro") return `<script is:inline>${SCRIPT_BODY}</script>`;
|
|
6130
|
+
return FORM_SCRIPT;
|
|
6131
|
+
}
|
|
6132
|
+
var messageFor = (dialect) => dialect === "jsx" ? `<p className="bcms-form-msg" hidden></p>` : `<p class="bcms-form-msg" hidden></p>`;
|
|
6133
|
+
var escapeAttr = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/"/g, """);
|
|
6134
|
+
var attrOf = (el, name) => el.attrs.find((a) => a.name.toLowerCase() === name);
|
|
6135
|
+
function* elementsIn(nodes) {
|
|
6136
|
+
for (const node of nodes) {
|
|
6137
|
+
if (node.type === "expr") {
|
|
6138
|
+
yield* elementsIn(node.children);
|
|
6139
|
+
continue;
|
|
6140
|
+
}
|
|
6141
|
+
if (node.type !== "element") continue;
|
|
6142
|
+
yield node;
|
|
6143
|
+
yield* elementsIn(node.children);
|
|
6144
|
+
}
|
|
6145
|
+
}
|
|
6146
|
+
var childElements = (node) => node.children.flatMap(
|
|
6147
|
+
(child) => child.type === "element" ? [child] : child.type === "expr" ? childElements(child) : []
|
|
6148
|
+
);
|
|
6149
|
+
function candidateOf(file, dialect, form) {
|
|
6150
|
+
const controls = /* @__PURE__ */ new Map();
|
|
6151
|
+
const wrapper = /* @__PURE__ */ new Map();
|
|
6152
|
+
const all = [];
|
|
6153
|
+
const walk = (parent) => {
|
|
6154
|
+
const own = [];
|
|
6155
|
+
for (const child of childElements(parent)) {
|
|
6156
|
+
if (CONTROLS.has(child.tag.toLowerCase())) {
|
|
6157
|
+
own.push(child);
|
|
6158
|
+
all.push(child);
|
|
6159
|
+
}
|
|
6160
|
+
walk(child);
|
|
6161
|
+
}
|
|
6162
|
+
if (own.length === 1 && parent !== form) wrapper.set(own[0], parent);
|
|
6163
|
+
};
|
|
6164
|
+
walk(form);
|
|
6165
|
+
for (const control of all) {
|
|
6166
|
+
const name = attrOf(control, "name")?.value;
|
|
6167
|
+
if (name && !controls.has(name)) controls.set(name, control);
|
|
6168
|
+
}
|
|
6169
|
+
for (const control of all) {
|
|
6170
|
+
const id = attrOf(control, "id")?.value;
|
|
6171
|
+
if (id && !controls.has(id)) controls.set(id, control);
|
|
6172
|
+
}
|
|
6173
|
+
return { file, dialect, el: form, controls, wrapper };
|
|
6174
|
+
}
|
|
6175
|
+
var hasMessage = (form) => [...elementsIn([form])].some((el) => (attrOf(el, "class") ?? attrOf(el, "classname"))?.value.includes("bcms-form-msg") ?? false);
|
|
6176
|
+
function wire(source, candidate, form) {
|
|
6177
|
+
const { el, dialect } = candidate;
|
|
6178
|
+
const inserts = [];
|
|
6179
|
+
const set = (name, value) => {
|
|
6180
|
+
const attr = attrOf(el, name);
|
|
6181
|
+
if (!attr) inserts.push(`${name}="${escapeAttr(value)}"`);
|
|
6182
|
+
else if (attr.valueRange) source.overwrite(attr.valueRange.start, attr.valueRange.end, escapeAttr(value));
|
|
6183
|
+
else source.overwrite(attr.range.start, attr.range.end, `${name}="${escapeAttr(value)}"`);
|
|
6184
|
+
};
|
|
6185
|
+
set("action", form.submitUrl);
|
|
6186
|
+
set("method", "post");
|
|
6187
|
+
inserts.push(`data-bcms-form="${escapeAttr(form.id)}"`);
|
|
6188
|
+
if (form.successMessage) inserts.push(`data-bcms-form-success="${escapeAttr(form.successMessage)}"`);
|
|
6189
|
+
source.appendLeft(el.attrInsertAt, ` ${inserts.join(" ")}`);
|
|
6190
|
+
for (const field of form.fields) {
|
|
6191
|
+
const control = candidate.controls.get(field.key);
|
|
6192
|
+
const host = candidate.wrapper.get(control) ?? control;
|
|
6193
|
+
source.appendLeft(host.attrInsertAt, ` data-bcms-form-field="${escapeAttr(field.key)}"`);
|
|
6194
|
+
const name = attrOf(control, "name");
|
|
6195
|
+
if (!name) source.appendLeft(control.attrInsertAt, ` name="${escapeAttr(field.key)}"`);
|
|
6196
|
+
else if (!name.expression && name.value !== field.key && name.valueRange) {
|
|
6197
|
+
source.overwrite(name.valueRange.start, name.valueRange.end, escapeAttr(field.key));
|
|
6198
|
+
}
|
|
6199
|
+
}
|
|
6200
|
+
if (el.inner && !hasMessage(el)) {
|
|
6201
|
+
const tail = /\s*$/.exec(source.original.slice(el.inner.start, el.inner.end))[0];
|
|
6202
|
+
source.appendLeft(el.inner.end - tail.length, `${tail || "\n"}${messageFor(dialect)}`);
|
|
6203
|
+
}
|
|
6204
|
+
}
|
|
6205
|
+
async function wireForms(briefIn, sources) {
|
|
6206
|
+
const brief = readBrief(briefIn);
|
|
6207
|
+
const forms = brief.forms ?? [];
|
|
6208
|
+
const candidates = [];
|
|
6209
|
+
const unreadable = [];
|
|
6210
|
+
for (const source of sources) {
|
|
6211
|
+
if (!/<form[\s/>]/i.test(source.content)) continue;
|
|
6212
|
+
const dialect = dialectOf(source.path, source.content);
|
|
6213
|
+
if (!dialect || !WIRABLE.includes(dialect)) {
|
|
6214
|
+
unreadable.push({
|
|
6215
|
+
file: source.path,
|
|
6216
|
+
reason: "DIALECT_UNSUPPORTED",
|
|
6217
|
+
message: `${source.path} is ${dialect ?? "not a template"}.`
|
|
6218
|
+
});
|
|
6219
|
+
continue;
|
|
6220
|
+
}
|
|
6221
|
+
const parsed = await parseFile4(dialect, source.content);
|
|
6222
|
+
if (parsed.error) {
|
|
6223
|
+
unreadable.push({
|
|
6224
|
+
file: source.path,
|
|
6225
|
+
reason: parsed.error.code === "DIALECT_UNSUPPORTED" ? "DIALECT_UNSUPPORTED" : "PARSE_ERROR",
|
|
6226
|
+
message: parsed.error.message
|
|
6227
|
+
});
|
|
6228
|
+
continue;
|
|
6229
|
+
}
|
|
6230
|
+
for (const el of elementsIn(parsed.roots)) {
|
|
6231
|
+
if (el.tag.toLowerCase() === "form") candidates.push(candidateOf(source.path, dialect, el));
|
|
6232
|
+
}
|
|
6233
|
+
}
|
|
6234
|
+
const holds = (c, f) => f.fields.filter((field) => c.controls.has(field.key)).length;
|
|
6235
|
+
const covers2 = (c, f) => f.fields.length > 0 && holds(c, f) === f.fields.length;
|
|
6236
|
+
const options = new Map(forms.map((f) => [f.id, candidates.filter((c) => covers2(c, f))]));
|
|
6237
|
+
const wanted = /* @__PURE__ */ new Map();
|
|
6238
|
+
for (const list of options.values()) for (const c of list) wanted.set(c, (wanted.get(c) ?? 0) + 1);
|
|
6239
|
+
const matched = [];
|
|
6240
|
+
const pending = [];
|
|
6241
|
+
const onSite = [];
|
|
6242
|
+
let alreadyWired = 0;
|
|
6243
|
+
const pend2 = (f, reason, file, message) => void pending.push({ id: f.id, name: f.name, reason, ...file ? { file } : {}, ...message ? { message } : {} });
|
|
6244
|
+
for (const form of forms) {
|
|
6245
|
+
const list = options.get(form.id) ?? [];
|
|
6246
|
+
if (list.length > 1) {
|
|
6247
|
+
pend2(form, "FORM_AMBIGUOUS", void 0, `${list.length} forms post every field of "${form.name}": ${list.map((c) => c.file).join(", ")}.`);
|
|
6248
|
+
continue;
|
|
6249
|
+
}
|
|
6250
|
+
if (list.length === 1 && (wanted.get(list[0]) ?? 0) > 1) {
|
|
6251
|
+
pend2(form, "FORM_AMBIGUOUS", list[0].file, "more than one CMS form's fields are posted by this <form>.");
|
|
6252
|
+
continue;
|
|
6253
|
+
}
|
|
6254
|
+
if (list.length === 0) {
|
|
6255
|
+
const [best] = candidates.map((c) => ({ c, n: holds(c, form) })).filter((x) => x.n > 0).sort((a, b) => b.n - a.n);
|
|
6256
|
+
if (best) {
|
|
6257
|
+
const missing = form.fields.filter((f) => !best.c.controls.has(f.key)).map((f) => f.key);
|
|
6258
|
+
pend2(form, "FIELD_UNMATCHED", best.c.file, `no control posts ${missing.join(", ")} in the <form> that posts its other fields.`);
|
|
6259
|
+
} else if (unreadable[0]) {
|
|
6260
|
+
pend2(form, unreadable[0].reason, unreadable[0].file, unreadable[0].message);
|
|
6261
|
+
} else {
|
|
6262
|
+
pend2(form, "FORM_NOT_IN_SOURCE", void 0, `no <form> posts ${form.fields.map((f) => f.key).join(", ") || "any field of this form"}.`);
|
|
6263
|
+
}
|
|
6264
|
+
continue;
|
|
6265
|
+
}
|
|
6266
|
+
const candidate = list[0];
|
|
6267
|
+
if (attrOf(candidate.el, "action")?.expression) {
|
|
6268
|
+
pend2(form, "ACTION_IS_EXPRESSION", candidate.file, "its action is an expression, so this run will not replace it.");
|
|
6269
|
+
continue;
|
|
6270
|
+
}
|
|
6271
|
+
const misnamed = form.fields.find((field) => {
|
|
6272
|
+
const attr = attrOf(candidate.controls.get(field.key), "name");
|
|
6273
|
+
return !!attr && attr.value !== field.key && (attr.expression || !attr.valueRange);
|
|
6274
|
+
});
|
|
6275
|
+
if (misnamed) {
|
|
6276
|
+
pend2(form, "FIELD_UNMATCHED", candidate.file, `the control for ${misnamed.key} posts under an expression, so its name cannot be set to the CMS key.`);
|
|
6277
|
+
continue;
|
|
6278
|
+
}
|
|
6279
|
+
if (attrOf(candidate.el, "data-bcms-form")) {
|
|
6280
|
+
alreadyWired++;
|
|
6281
|
+
onSite.push(form);
|
|
6282
|
+
continue;
|
|
6283
|
+
}
|
|
6284
|
+
matched.push({ form, candidate });
|
|
6285
|
+
onSite.push(form);
|
|
6286
|
+
}
|
|
6287
|
+
const contents = new Map(sources.map((s) => [s.path, s.content]));
|
|
6288
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
6289
|
+
for (const m of matched) byFile.set(m.candidate.file, [...byFile.get(m.candidate.file) ?? [], m]);
|
|
6290
|
+
const files = [];
|
|
6291
|
+
for (const [path, list] of [...byFile].sort(([a], [b]) => a.localeCompare(b))) {
|
|
6292
|
+
const content = contents.get(path);
|
|
6293
|
+
const source = new MagicString4(content);
|
|
6294
|
+
for (const { form, candidate } of list) wire(source, candidate, form);
|
|
6295
|
+
if (!content.includes("form[data-bcms-form]")) {
|
|
6296
|
+
const last = list.reduce((a, b) => b.candidate.el.end > a.candidate.el.end ? b : a);
|
|
6297
|
+
const indent = /[^\S\n]*$/.exec(content.slice(0, last.candidate.el.start))[0];
|
|
6298
|
+
source.appendRight(last.candidate.el.end, `
|
|
6299
|
+
${indent}${scriptFor(last.candidate.dialect)}`);
|
|
6300
|
+
}
|
|
6301
|
+
files.push({ path, operation: "modify", content: source.toString() });
|
|
6302
|
+
}
|
|
6303
|
+
const notes = onSite.filter((f) => f.status === "draft").map((f) => `publish form ${f.name} in the Forms tab \u2014 a draft form rejects every submission with 403.`);
|
|
6304
|
+
return {
|
|
6305
|
+
files,
|
|
6306
|
+
receipt: {
|
|
6307
|
+
briefDigest: briefDigest(brief),
|
|
6308
|
+
// This lane binds no brief PATH, and says so rather than leaving the arithmetic to a reader:
|
|
6309
|
+
// a receipt claiming zero of zero paths is exactly what a forms-only run did.
|
|
6310
|
+
paths: { declared: 0, rewritten: 0, alreadyDeclared: 0, pending: [] },
|
|
6311
|
+
occurrences: { located: 0, rewritten: 0 },
|
|
6312
|
+
files: [],
|
|
6313
|
+
helpers: [],
|
|
6314
|
+
helperUpgraded: false,
|
|
6315
|
+
dynamicBindings: [],
|
|
6316
|
+
forms: assertForms(forms.length, {
|
|
6317
|
+
wired: matched.length,
|
|
6318
|
+
alreadyWired,
|
|
6319
|
+
pending,
|
|
6320
|
+
...notes.length > 0 ? { notes } : {}
|
|
6321
|
+
})
|
|
6322
|
+
}
|
|
6323
|
+
};
|
|
6324
|
+
}
|
|
6325
|
+
|
|
6060
6326
|
// src/scan.ts
|
|
6061
6327
|
var SKIP_DIRS = [
|
|
6062
6328
|
"node_modules/",
|
|
@@ -6425,7 +6691,7 @@ function moduleScope(parsed, content) {
|
|
|
6425
6691
|
}
|
|
6426
6692
|
|
|
6427
6693
|
// src/componentize.ts
|
|
6428
|
-
import
|
|
6694
|
+
import MagicString5 from "magic-string";
|
|
6429
6695
|
var SectionInvariantError = class extends Error {
|
|
6430
6696
|
code = "SECTION_INVARIANT";
|
|
6431
6697
|
constructor(message) {
|
|
@@ -7351,7 +7617,7 @@ export default function ${resolved.name}({ blockId, bind, overrides, page }: Bcm
|
|
|
7351
7617
|
` };
|
|
7352
7618
|
}
|
|
7353
7619
|
function sectionMarkup(content, root, fieldName) {
|
|
7354
|
-
const slice = new
|
|
7620
|
+
const slice = new MagicString5(content.slice(root.start, root.end));
|
|
7355
7621
|
const at = (offset) => offset - root.start;
|
|
7356
7622
|
const stamps = root.attrs.filter((attr) => attr.name === "data-bcms-block");
|
|
7357
7623
|
if (stamps.length === 0) slice.appendLeft(at(root.attrInsertAt), " data-bcms-block={blockId}");
|
|
@@ -7550,7 +7816,7 @@ async function migrateSections(plan2, contents, written, fileSet, aliases) {
|
|
|
7550
7816
|
);
|
|
7551
7817
|
continue;
|
|
7552
7818
|
}
|
|
7553
|
-
const source = new
|
|
7819
|
+
const source = new MagicString5(content);
|
|
7554
7820
|
const { bound, duplicates } = wrapFieldAttrs(source, content, elements, (at) => at, "bcmsField");
|
|
7555
7821
|
if (duplicates.length > 0) {
|
|
7556
7822
|
refused.set(refusalKey(dialect, slug), `${entry.path} has ${duplicates.join(", ")} carrying more than one data-bcms-field, so there is no single address to rewrite.`);
|
|
@@ -7827,7 +8093,7 @@ export default function Sections({ page }: { page: string }) {
|
|
|
7827
8093
|
function splicePageRun(file, content, dialect, parsed, placeable, pageSlug) {
|
|
7828
8094
|
const names = allocateNames(topLevelBindings(dialect, parsed, content), [{ key: "Sections", name: "Sections" }]);
|
|
7829
8095
|
const local = names.get("Sections");
|
|
7830
|
-
const source = new
|
|
8096
|
+
const source = new MagicString5(content);
|
|
7831
8097
|
const first = placeable[0].root;
|
|
7832
8098
|
const last = placeable[placeable.length - 1].root;
|
|
7833
8099
|
source.overwrite(first.start, last.end, `<${local} page="${pageSlug}" />`);
|
|
@@ -7847,7 +8113,7 @@ function splicePageInline(file, content, dialect, parsed, placeable, pageSlug, w
|
|
|
7847
8113
|
{ key: "pageSnapshot", name: "pageSnapshot" }
|
|
7848
8114
|
]);
|
|
7849
8115
|
const snapshot = names.get("pageSnapshot");
|
|
7850
|
-
const source = new
|
|
8116
|
+
const source = new MagicString5(content);
|
|
7851
8117
|
for (const resolved of placeable) {
|
|
7852
8118
|
const component = written.get(resolved.slug);
|
|
7853
8119
|
const tag = component.member ? `${names.get(resolved.slug)}.${component.member}` : names.get(resolved.slug);
|
|
@@ -8234,7 +8500,7 @@ async function convertSources(briefIn, sources, options) {
|
|
|
8234
8500
|
} else rewritten++;
|
|
8235
8501
|
}
|
|
8236
8502
|
for (const [file, splices] of [...componentEdits].sort(([a], [b]) => a.localeCompare(b))) {
|
|
8237
|
-
const source = new
|
|
8503
|
+
const source = new MagicString6(contents.get(file));
|
|
8238
8504
|
const seen = /* @__PURE__ */ new Set();
|
|
8239
8505
|
for (const splice of splices) {
|
|
8240
8506
|
const id = `${splice.start}:${splice.end}:${splice.text}`;
|
|
@@ -8942,6 +9208,8 @@ export {
|
|
|
8942
9208
|
CONTENT_DIR,
|
|
8943
9209
|
ConvertError,
|
|
8944
9210
|
DIALECT_RULES,
|
|
9211
|
+
FORM_SCRIPT,
|
|
9212
|
+
FormInvariantError,
|
|
8945
9213
|
HELPER_PATH,
|
|
8946
9214
|
HELPER_VERSION,
|
|
8947
9215
|
PARSE_FILE,
|
|
@@ -8957,6 +9225,7 @@ export {
|
|
|
8957
9225
|
SectionInvariantError,
|
|
8958
9226
|
ValidationError,
|
|
8959
9227
|
aliasesFrom,
|
|
9228
|
+
assertForms,
|
|
8960
9229
|
assertReceipt,
|
|
8961
9230
|
assertSections,
|
|
8962
9231
|
assetCandidates,
|
|
@@ -8999,6 +9268,7 @@ export {
|
|
|
8999
9268
|
routeOfFile,
|
|
9000
9269
|
stripTags,
|
|
9001
9270
|
unreadableConfigs,
|
|
9002
|
-
walkAst
|
|
9271
|
+
walkAst,
|
|
9272
|
+
wireForms
|
|
9003
9273
|
};
|
|
9004
9274
|
//# sourceMappingURL=index.js.map
|