@bettercms-ai/convert 0.11.0 → 0.12.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 +31 -0
- package/README.md +54 -0
- package/dist/cli.js +190 -21
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +88 -1
- package/dist/index.js +192 -20
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -197,6 +197,83 @@ declare function locate(brief: BriefLike, sources: SourceFile[], options?: Locat
|
|
|
197
197
|
unlocated: UnlocatedPath[];
|
|
198
198
|
};
|
|
199
199
|
|
|
200
|
+
/**
|
|
201
|
+
* What to DO about a path this run could not bind — an instruction, never a category name.
|
|
202
|
+
*
|
|
203
|
+
* 🔴 A REASON IS A DIAGNOSIS AND AN AGENT CANNOT ACT ON ONE. `IN_EXPRESSION` tells a coding agent
|
|
204
|
+
* that something, somewhere, is an expression; it does not say which file, which line, or what to
|
|
205
|
+
* type. So every pending row leaves with a `fix`: one file, one line, the bytes that are there
|
|
206
|
+
* now, and the one edit that makes the path bindable on the next run. The closed `action` set is
|
|
207
|
+
* the contract the server validates (`src/lib/validation/schemas/conversion-receipt.ts`) and the
|
|
208
|
+
* loop `get_next_steps` reads — an unknown action is a 400, so it is minted in exactly one place.
|
|
209
|
+
*/
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* The edits an agent can be asked for, and nothing else.
|
|
213
|
+
*
|
|
214
|
+
* `wrap-span` the value has no element of its own — wrap it in `<span data-bcms-field>`.
|
|
215
|
+
* `declare-attr` the element is there — add `data-bcms-field` to it at file:line.
|
|
216
|
+
* `declare-richtext` the container renders markup — bind it with the richtext helper.
|
|
217
|
+
* `bind-expression` the value is computed — replace the expression with the helper's read.
|
|
218
|
+
* `bind-data` the copy lives in a data file or a content entry — bind THAT field.
|
|
219
|
+
* `manual` none of the above is true, and `why` says what a human has to decide.
|
|
220
|
+
*/
|
|
221
|
+
type FixAction = "wrap-span" | "declare-attr" | "bind-expression" | "declare-richtext" | "bind-data" | "manual";
|
|
222
|
+
/** One mechanical instruction, at one place in one file. @see FixAction */
|
|
223
|
+
interface PendingFix {
|
|
224
|
+
file: string;
|
|
225
|
+
/** 1-based. */
|
|
226
|
+
line: number;
|
|
227
|
+
/** 1-based, when the literal was found on the line. */
|
|
228
|
+
col?: number;
|
|
229
|
+
/** The bytes at `line`, trimmed — so the agent can see it is looking at the right place. */
|
|
230
|
+
snippet: string;
|
|
231
|
+
action: FixAction;
|
|
232
|
+
/** One sentence, present whenever the action alone does not say enough. */
|
|
233
|
+
why?: string;
|
|
234
|
+
}
|
|
235
|
+
/** Everything about one pending path a fix is derived from. Pure input; no filesystem. */
|
|
236
|
+
interface FixInput {
|
|
237
|
+
/** The copy the repository renders, flattened. Empty when the brief captured none. */
|
|
238
|
+
literal: string;
|
|
239
|
+
kind: string;
|
|
240
|
+
/** The richtext value IS the element's whole inline subtree. @see brief.ts */
|
|
241
|
+
inline?: true;
|
|
242
|
+
/** Files this path's copy was found in, best first. May be empty. */
|
|
243
|
+
candidates: string[];
|
|
244
|
+
/** Source, by path — only the files named in `candidates` need be present. */
|
|
245
|
+
contents: ReadonlyMap<string, string>;
|
|
246
|
+
/** How many ROUTES share this `(scope, path)` in the pending list. @see sharedChrome */
|
|
247
|
+
routes: number;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Where `literal` sits in `content` — tolerant of the two things every template does to copy.
|
|
251
|
+
*
|
|
252
|
+
* A template wraps its copy across lines and indents it, and it puts inline markup in the middle
|
|
253
|
+
* of it (`Read <strong>this</strong> now`). Both are invisible to a reader and fatal to
|
|
254
|
+
* `indexOf`, so the words are matched with whitespace OR a tag between them. Bounded at
|
|
255
|
+
* `MAX_WORDS`, because a paragraph's first dozen words identify it as well as all ninety do.
|
|
256
|
+
*/
|
|
257
|
+
declare function findLiteral(content: string, literal: string): {
|
|
258
|
+
index: number;
|
|
259
|
+
length: number;
|
|
260
|
+
} | null;
|
|
261
|
+
/**
|
|
262
|
+
* The fix for one pending row. Always answers: a row without one is a row an agent cannot act on.
|
|
263
|
+
*
|
|
264
|
+
* The FILE is the row's own when it has one (the lane that refused named it), and otherwise the
|
|
265
|
+
* first candidate `locate` found — and when there is neither, the fix still names the route's page
|
|
266
|
+
* file with line 1, because "go and look here" beats "go and look".
|
|
267
|
+
*/
|
|
268
|
+
declare function fixFor(row: PendingPath, input: FixInput): PendingFix;
|
|
269
|
+
/**
|
|
270
|
+
* How many ROUTES each `(scope, path)` is pending on — the shared-chrome tell. @see actionFor
|
|
271
|
+
*
|
|
272
|
+
* Counted over the whole pending list rather than per row: one nav label pending on twelve routes
|
|
273
|
+
* is one element, and the fix for every one of those rows is the same sentence about promotion.
|
|
274
|
+
*/
|
|
275
|
+
declare function routeCounts(pending: readonly PendingPath[]): Map<string, number>;
|
|
276
|
+
|
|
200
277
|
/**
|
|
201
278
|
* What the conversion did, and — the half that matters — what it did not.
|
|
202
279
|
*
|
|
@@ -209,6 +286,7 @@ declare function locate(brief: BriefLike, sources: SourceFile[], options?: Locat
|
|
|
209
286
|
* sentence appears in three files" and "this field is bound" are different questions — and a path
|
|
210
287
|
* is only `rewritten` when EVERY located occurrence of it was.
|
|
211
288
|
*/
|
|
289
|
+
|
|
212
290
|
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
291
|
/** An INLINE richtext value whose element holds a component, a `<slot>` or an expression. */
|
|
214
292
|
| "SUBTREE_NOT_HTML" | "PROP_TARGET_NOT_FOUND" | "PROP_DRILLED_DEEP" | "REPEATER_FIXED_LENGTH"
|
|
@@ -231,6 +309,15 @@ interface PendingPath {
|
|
|
231
309
|
line?: number;
|
|
232
310
|
/** What to do about it, in prose — a recipe, not a diagnosis. @see message */
|
|
233
311
|
hint?: string;
|
|
312
|
+
/**
|
|
313
|
+
* The EDIT that makes this path bindable on the next run — one file, one line, one action.
|
|
314
|
+
*
|
|
315
|
+
* 🔴 THE REASON IS A DIAGNOSIS AND NOBODY CAN ACT ON ONE. Every row carries this, because a
|
|
316
|
+
* conversion ends when nothing is pending and the only way an agent gets there is by being told
|
|
317
|
+
* what to type and where. OPTIONAL in the type so a receipt written by an older version of this
|
|
318
|
+
* package still parses; this package emits one for every row. @see fix.ts
|
|
319
|
+
*/
|
|
320
|
+
fix?: PendingFix;
|
|
234
321
|
}
|
|
235
322
|
/**
|
|
236
323
|
* One path that was already DECLARED before this run — and how honestly.
|
|
@@ -1720,4 +1807,4 @@ declare function convertSources(briefIn: Brief, sources: SourceFile[], options?:
|
|
|
1720
1807
|
receipt: ConversionReceipt;
|
|
1721
1808
|
}>;
|
|
1722
1809
|
|
|
1723
|
-
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 };
|
|
1810
|
+
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, type FixAction, 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 PendingFix, 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, findLiteral, findSites, findSitesTolerant, fixFor, flat, helperSource, importsIn, isDecorativeGlyph, isDynamicRoute, isKnownHelper, isSourceCandidate, locate, measureUnaddressable, overlapping, pageFilesFor, parseFile, propsAttribute, readBrief, readComponentizePlan, readDeclarations, readExpr, readPlan, relativeImport, resolveSpecifier, rewriteFile, routeCounts, routeOfFile, stripTags, unreadableConfigs, walkAst, wireForms };
|
package/dist/index.js
CHANGED
|
@@ -5318,6 +5318,138 @@ function coverageOf(receipt) {
|
|
|
5318
5318
|
};
|
|
5319
5319
|
}
|
|
5320
5320
|
|
|
5321
|
+
// src/fix.ts
|
|
5322
|
+
var MAX_SNIPPET = 160;
|
|
5323
|
+
var MAX_WORDS = 12;
|
|
5324
|
+
var escapeRegExp2 = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5325
|
+
function findLiteral(content, literal) {
|
|
5326
|
+
const words = literal.split(/\s+/).filter(Boolean).slice(0, MAX_WORDS);
|
|
5327
|
+
if (words.length === 0) return null;
|
|
5328
|
+
const pattern = new RegExp(words.map(escapeRegExp2).join("(?:\\s|<[^>]*>)+"), "u");
|
|
5329
|
+
const match = pattern.exec(content);
|
|
5330
|
+
return match ? { index: match.index, length: match[0].length } : null;
|
|
5331
|
+
}
|
|
5332
|
+
function placeOf(content, index) {
|
|
5333
|
+
const before = content.slice(0, index);
|
|
5334
|
+
const line = before.split("\n").length;
|
|
5335
|
+
const start = before.lastIndexOf("\n") + 1;
|
|
5336
|
+
const end = content.indexOf("\n", index);
|
|
5337
|
+
return {
|
|
5338
|
+
line,
|
|
5339
|
+
col: index - start + 1,
|
|
5340
|
+
snippet: content.slice(start, end < 0 ? content.length : end).trim().slice(0, MAX_SNIPPET)
|
|
5341
|
+
};
|
|
5342
|
+
}
|
|
5343
|
+
function fileHead(content) {
|
|
5344
|
+
return { line: 1, snippet: (content ?? "").split("\n", 1)[0].trim().slice(0, MAX_SNIPPET) };
|
|
5345
|
+
}
|
|
5346
|
+
var isMarkdown = (file) => /\.mdx?$/i.test(file);
|
|
5347
|
+
var isDataModule = (file) => /\.(json|ya?ml|[mc]?[jt]s)$/i.test(file) && !/\.d\.ts$/i.test(file);
|
|
5348
|
+
function inDataRegion(content, index) {
|
|
5349
|
+
if (content.startsWith("---")) {
|
|
5350
|
+
const close = content.indexOf("\n---", 3);
|
|
5351
|
+
if (close >= 0 && index < close) return true;
|
|
5352
|
+
}
|
|
5353
|
+
for (const match of content.matchAll(/<script\b[^>]*>([\s\S]*?)<\/script>/gi)) {
|
|
5354
|
+
const start = match.index + match[0].indexOf(">") + 1;
|
|
5355
|
+
if (index >= start && index < start + match[1].length) return true;
|
|
5356
|
+
}
|
|
5357
|
+
return false;
|
|
5358
|
+
}
|
|
5359
|
+
var ELEMENT_ACTIONS = /* @__PURE__ */ new Set([
|
|
5360
|
+
"wrap-span",
|
|
5361
|
+
"declare-attr",
|
|
5362
|
+
"declare-richtext"
|
|
5363
|
+
]);
|
|
5364
|
+
function actionFor(reason, input, file, inData) {
|
|
5365
|
+
const chosen = reasonAction(reason, input, file, inData);
|
|
5366
|
+
if (inData && ELEMENT_ACTIONS.has(chosen.action)) {
|
|
5367
|
+
return { action: "bind-data", why: "The copy is a row in this file's own data \u2014 bind that field." };
|
|
5368
|
+
}
|
|
5369
|
+
return chosen;
|
|
5370
|
+
}
|
|
5371
|
+
function reasonAction(reason, input, file, inData) {
|
|
5372
|
+
const richtext = input.kind === "richtext" || input.inline === true;
|
|
5373
|
+
switch (reason) {
|
|
5374
|
+
case "AMBIGUOUS_LITERAL":
|
|
5375
|
+
if (input.routes > 1) {
|
|
5376
|
+
return {
|
|
5377
|
+
action: "manual",
|
|
5378
|
+
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.`
|
|
5379
|
+
};
|
|
5380
|
+
}
|
|
5381
|
+
return {
|
|
5382
|
+
action: "declare-attr",
|
|
5383
|
+
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."
|
|
5384
|
+
};
|
|
5385
|
+
case "REPEATER_AMBIGUOUS":
|
|
5386
|
+
case "REPEATER_FIXED_LENGTH":
|
|
5387
|
+
return {
|
|
5388
|
+
action: "declare-attr",
|
|
5389
|
+
why: "Declare this row's leaf on the element itself, or rewrite the loop to read the CMS array."
|
|
5390
|
+
};
|
|
5391
|
+
case "IN_EXPRESSION":
|
|
5392
|
+
if (inData) {
|
|
5393
|
+
return { action: "bind-data", why: "The copy is a row in this file's own data \u2014 bind that field." };
|
|
5394
|
+
}
|
|
5395
|
+
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." };
|
|
5396
|
+
case "DIALECT_UNSUPPORTED":
|
|
5397
|
+
if (isMarkdown(file)) {
|
|
5398
|
+
return {
|
|
5399
|
+
action: "bind-data",
|
|
5400
|
+
why: "This is a content-collection body: bind the element that renders it and make the entry the source."
|
|
5401
|
+
};
|
|
5402
|
+
}
|
|
5403
|
+
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.` };
|
|
5404
|
+
case "NOT_IN_SOURCE":
|
|
5405
|
+
case "NO_ORIGINAL":
|
|
5406
|
+
return isDataModule(file) || inData ? { action: "bind-data", why: "The template computes this from data \u2014 bind the data field instead of the text." } : {
|
|
5407
|
+
action: "manual",
|
|
5408
|
+
why: "The built page renders this and no source file holds it verbatim; find what formats it."
|
|
5409
|
+
};
|
|
5410
|
+
case "IN_DATA_FILE":
|
|
5411
|
+
case "IMAGE_ASSET_UNRESOLVED":
|
|
5412
|
+
return { action: "bind-data", why: "Bind the data field this value comes from." };
|
|
5413
|
+
case "PROP_TARGET_NOT_FOUND":
|
|
5414
|
+
case "PROP_DRILLED_DEEP":
|
|
5415
|
+
return {
|
|
5416
|
+
action: "wrap-span",
|
|
5417
|
+
why: "The component renders this in a position no attribute reaches \u2014 wrap it in a span of its own."
|
|
5418
|
+
};
|
|
5419
|
+
case "SUBSTRING_ONLY":
|
|
5420
|
+
return { action: "wrap-span", why: "Only part of the element's text is this value \u2014 give it its own span." };
|
|
5421
|
+
case "SUBTREE_NOT_HTML":
|
|
5422
|
+
return { action: "declare-richtext", why: "The value is the element's whole subtree \u2014 bind it as richtext." };
|
|
5423
|
+
default:
|
|
5424
|
+
return { action: "manual", why: `${reason} \u2014 see the receipt's message for what blocked this path.` };
|
|
5425
|
+
}
|
|
5426
|
+
}
|
|
5427
|
+
function fixFor(row, input) {
|
|
5428
|
+
const file = row.file ?? input.candidates[0] ?? "";
|
|
5429
|
+
const content = input.contents.get(file);
|
|
5430
|
+
const at = content ? findLiteral(content, input.literal) : null;
|
|
5431
|
+
const place = at ? placeOf(content, at.index) : { ...fileHead(content), col: void 0 };
|
|
5432
|
+
const { action, why } = actionFor(row.reason, input, file, at !== null && inDataRegion(content, at.index));
|
|
5433
|
+
return {
|
|
5434
|
+
file,
|
|
5435
|
+
line: row.line ?? place.line,
|
|
5436
|
+
...place.col !== void 0 ? { col: place.col } : {},
|
|
5437
|
+
snippet: place.snippet,
|
|
5438
|
+
action,
|
|
5439
|
+
...why ? { why } : {}
|
|
5440
|
+
};
|
|
5441
|
+
}
|
|
5442
|
+
function routeCounts(pending) {
|
|
5443
|
+
const routes = /* @__PURE__ */ new Map();
|
|
5444
|
+
for (const row of pending) {
|
|
5445
|
+
const id = `${row.scope}|${row.path}`;
|
|
5446
|
+
const held = routes.get(id) ?? /* @__PURE__ */ new Set();
|
|
5447
|
+
held.add(row.route);
|
|
5448
|
+
routes.set(id, held);
|
|
5449
|
+
}
|
|
5450
|
+
return new Map([...routes].map(([id, set]) => [id, set.size]));
|
|
5451
|
+
}
|
|
5452
|
+
|
|
5321
5453
|
// src/sites/html.ts
|
|
5322
5454
|
import { parse as parseHtml } from "parse5";
|
|
5323
5455
|
var RAW_TAGS = /* @__PURE__ */ new Set(["script", "style", "noscript", "template"]);
|
|
@@ -6438,7 +6570,9 @@ var SKIP_DIRS = [
|
|
|
6438
6570
|
".vercel/",
|
|
6439
6571
|
".cache/",
|
|
6440
6572
|
"coverage/",
|
|
6441
|
-
"vendor/"
|
|
6573
|
+
"vendor/",
|
|
6574
|
+
"__tests__/",
|
|
6575
|
+
"__mocks__/"
|
|
6442
6576
|
];
|
|
6443
6577
|
var SKIP_FILES = ["package-lock.json", "bun.lock", "bun.lockb", "yarn.lock", "pnpm-lock.yaml"];
|
|
6444
6578
|
var SOURCE_EXTENSIONS = [
|
|
@@ -6476,11 +6610,13 @@ var TEMPLATE_DIRS = /* @__PURE__ */ new Set([
|
|
|
6476
6610
|
"_includes",
|
|
6477
6611
|
"_layouts"
|
|
6478
6612
|
]);
|
|
6613
|
+
var isAboutTheSite = (lower) => /\.(test|spec)\.[^./]+$/.test(lower) || /^[^/]*\.config\.[^./]+$/.test(lower);
|
|
6479
6614
|
function isSourceCandidate(path) {
|
|
6480
6615
|
const lower = path.toLowerCase();
|
|
6481
6616
|
if (SKIP_DIRS.some((dir) => lower === dir.slice(0, -1) || lower.startsWith(dir) || lower.includes(`/${dir}`)))
|
|
6482
6617
|
return false;
|
|
6483
6618
|
if (SKIP_FILES.includes(lower.split("/").pop() ?? "")) return false;
|
|
6619
|
+
if (isAboutTheSite(lower)) return false;
|
|
6484
6620
|
if (/(^|\/)[jt]sconfig\.json$/.test(lower)) return true;
|
|
6485
6621
|
if (!SOURCE_EXTENSIONS.some((ext) => lower.endsWith(ext))) return false;
|
|
6486
6622
|
if (!PROSE_EXTENSIONS.some((ext) => lower.endsWith(ext))) return true;
|
|
@@ -8429,7 +8565,7 @@ var ConvertError = class extends Error {
|
|
|
8429
8565
|
code;
|
|
8430
8566
|
};
|
|
8431
8567
|
var keyOf = (route, scope, path) => scope === "layout" ? `layout::${path}` : `${route}::page::${path}`;
|
|
8432
|
-
var
|
|
8568
|
+
var escapeRegExp3 = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
8433
8569
|
var identifierFor2 = (slug) => `bcms${slug.replace(/[^A-Za-z0-9]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^./, (c) => c.toUpperCase())}`;
|
|
8434
8570
|
var UNPLACED = /* @__PURE__ */ new Set(["NOT_IN_SOURCE", "IN_EXPRESSION"]);
|
|
8435
8571
|
async function drillProp(ctx, fromFile, component, prop, kind, hop) {
|
|
@@ -8776,6 +8912,33 @@ async function convertSources(briefIn, sources, options) {
|
|
|
8776
8912
|
else outFiles.push({ path, operation: "modify", content });
|
|
8777
8913
|
}
|
|
8778
8914
|
pending.push(...canvas.pending);
|
|
8915
|
+
for (const file of outFiles) if (file.operation === "modify") latest.set(file.path, file.content);
|
|
8916
|
+
const pageFilesOrTemplate = (route, files) => {
|
|
8917
|
+
const all = [...files];
|
|
8918
|
+
const exact = pageFilesFor(route, all);
|
|
8919
|
+
if (exact.length > 0) return exact;
|
|
8920
|
+
const wanted = route.split("/").filter(Boolean);
|
|
8921
|
+
return all.filter((file) => {
|
|
8922
|
+
if (!isDynamicRoute(file)) return false;
|
|
8923
|
+
const pattern = routeOfFile(file)?.split("/").filter(Boolean);
|
|
8924
|
+
if (!pattern) return false;
|
|
8925
|
+
const rest = pattern.some((segment) => segment.startsWith("[..."));
|
|
8926
|
+
if (rest ? wanted.length < pattern.length : wanted.length !== pattern.length) return false;
|
|
8927
|
+
return pattern.every((segment, i) => segment.startsWith("[") || segment === wanted[i]);
|
|
8928
|
+
}).sort();
|
|
8929
|
+
};
|
|
8930
|
+
const routes = routeCounts(pending);
|
|
8931
|
+
for (const row of pending) {
|
|
8932
|
+
const target = targets.get(keyOf(row.route, row.scope, row.path));
|
|
8933
|
+
row.fix = fixFor(row, {
|
|
8934
|
+
literal: target?.literal ?? "",
|
|
8935
|
+
kind: row.kind,
|
|
8936
|
+
...target?.inline ? { inline: true } : {},
|
|
8937
|
+
candidates: locatedByKey.get(keyOf(row.route, row.scope, row.path))?.files ?? pageFilesOrTemplate(row.route, latest.keys()),
|
|
8938
|
+
contents: latest,
|
|
8939
|
+
routes: routes.get(`${row.scope}|${row.path}`) ?? 1
|
|
8940
|
+
});
|
|
8941
|
+
}
|
|
8779
8942
|
const receipt = assertReceipt({
|
|
8780
8943
|
briefDigest: digest,
|
|
8781
8944
|
paths: {
|
|
@@ -8988,13 +9151,13 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
8988
9151
|
for (const target of sharing) outcomes.set(target.key, missing(literal));
|
|
8989
9152
|
continue;
|
|
8990
9153
|
}
|
|
8991
|
-
const
|
|
8992
|
-
if (!assigned) {
|
|
8993
|
-
for (const target of sharing) outcomes.set(target.key, "AMBIGUOUS_LITERAL");
|
|
8994
|
-
continue;
|
|
8995
|
-
}
|
|
9154
|
+
const resolved = sharing.length === 1 ? { assigned: /* @__PURE__ */ new Map([[sharing[0].key, forLiteral]]), ambiguous: /* @__PURE__ */ new Set() } : resolvePositionally(sharing, forLiteral);
|
|
8996
9155
|
for (const target of sharing) {
|
|
8997
|
-
|
|
9156
|
+
if (resolved.ambiguous.has(target.key)) {
|
|
9157
|
+
outcomes.set(target.key, "AMBIGUOUS_LITERAL");
|
|
9158
|
+
continue;
|
|
9159
|
+
}
|
|
9160
|
+
const mine = resolved.assigned.get(target.key) ?? [];
|
|
8998
9161
|
const rewritable = mine.filter((s) => s.where !== "script" && s.where !== "comment" && !s.partial);
|
|
8999
9162
|
if (rewritable.length === 0) {
|
|
9000
9163
|
outcomes.set(
|
|
@@ -9339,19 +9502,25 @@ function repeatGroups(targets) {
|
|
|
9339
9502
|
}
|
|
9340
9503
|
function resolvePositionally(targets, sites) {
|
|
9341
9504
|
const assigned = /* @__PURE__ */ new Map();
|
|
9342
|
-
const
|
|
9505
|
+
const ambiguous = /* @__PURE__ */ new Set();
|
|
9506
|
+
const picked = /* @__PURE__ */ new Map();
|
|
9507
|
+
const wanters = /* @__PURE__ */ new Map();
|
|
9343
9508
|
for (const target of targets) {
|
|
9344
9509
|
const locator = target.locator;
|
|
9345
|
-
|
|
9346
|
-
const
|
|
9347
|
-
|
|
9348
|
-
|
|
9349
|
-
|
|
9350
|
-
|
|
9351
|
-
|
|
9352
|
-
|
|
9510
|
+
const candidates = locator ? sites.filter((s) => s.siblingIndex === locator.siblingIndex && isSubsequence(locator.ancestors, s.ancestors)) : [];
|
|
9511
|
+
const site = locator?.repeat ? candidates[locator.repeat.index] : candidates.length === 1 ? candidates[0] : void 0;
|
|
9512
|
+
if (!site) {
|
|
9513
|
+
ambiguous.add(target.key);
|
|
9514
|
+
continue;
|
|
9515
|
+
}
|
|
9516
|
+
picked.set(target.key, site);
|
|
9517
|
+
wanters.set(site, (wanters.get(site) ?? 0) + 1);
|
|
9518
|
+
}
|
|
9519
|
+
for (const [key, site] of picked) {
|
|
9520
|
+
if ((wanters.get(site) ?? 0) > 1) ambiguous.add(key);
|
|
9521
|
+
else assigned.set(key, [site]);
|
|
9353
9522
|
}
|
|
9354
|
-
return assigned;
|
|
9523
|
+
return { assigned, ambiguous };
|
|
9355
9524
|
}
|
|
9356
9525
|
function isSubsequence(wanted, chain) {
|
|
9357
9526
|
let at = 0;
|
|
@@ -9374,7 +9543,7 @@ async function tierThree(file, content, dialect, targets, llmFallback) {
|
|
|
9374
9543
|
if (elements.length !== targets.length) return { ok: false, reason: "unverifiable" };
|
|
9375
9544
|
const locals = helperLocals(reparsed, dialect, file);
|
|
9376
9545
|
const reads = DIALECT_RULES[dialect].text !== null;
|
|
9377
|
-
const boundary = (name) => `(?<![\\w$])${
|
|
9546
|
+
const boundary = (name) => `(?<![\\w$])${escapeRegExp3(name)}(?![\\w$])`;
|
|
9378
9547
|
const claimed = /* @__PURE__ */ new Set();
|
|
9379
9548
|
for (const element of elements) {
|
|
9380
9549
|
const candidates = targets.filter(
|
|
@@ -9382,7 +9551,7 @@ async function tierThree(file, content, dialect, targets, llmFallback) {
|
|
|
9382
9551
|
);
|
|
9383
9552
|
const match = candidates.find((target) => {
|
|
9384
9553
|
if (!reads) return true;
|
|
9385
|
-
const quoted =
|
|
9554
|
+
const quoted = escapeRegExp3(JSON.stringify(target.path));
|
|
9386
9555
|
if (target.scope === "layout") {
|
|
9387
9556
|
return locals.bcmsLayout.some(
|
|
9388
9557
|
(name) => new RegExp(`${boundary(name)}\\s*\\(\\s*${quoted}`).test(element.own)
|
|
@@ -9482,8 +9651,10 @@ export {
|
|
|
9482
9651
|
dialectOf,
|
|
9483
9652
|
exportedFunction,
|
|
9484
9653
|
extendedConfigs,
|
|
9654
|
+
findLiteral,
|
|
9485
9655
|
findSites,
|
|
9486
9656
|
findSitesTolerant,
|
|
9657
|
+
fixFor,
|
|
9487
9658
|
flat,
|
|
9488
9659
|
helperSource,
|
|
9489
9660
|
importsIn,
|
|
@@ -9505,6 +9676,7 @@ export {
|
|
|
9505
9676
|
relativeImport,
|
|
9506
9677
|
resolveSpecifier,
|
|
9507
9678
|
rewriteFile,
|
|
9679
|
+
routeCounts,
|
|
9508
9680
|
routeOfFile,
|
|
9509
9681
|
stripTags,
|
|
9510
9682
|
unreadableConfigs,
|