@bettercms-ai/convert 0.1.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/README.md +123 -0
- package/dist/cli.js +5450 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +839 -0
- package/dist/index.js +5370 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,839 @@
|
|
|
1
|
+
import { BindingKind } from '@bettercms-ai/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Where the BUILT element that carried this value sat, so a duplicate literal can be resolved
|
|
5
|
+
* by position rather than by guessing.
|
|
6
|
+
*
|
|
7
|
+
* Best-effort by construction: built-DOM positions do not survive component expansion, wrappers
|
|
8
|
+
* or conditional rendering, so `ancestors` is compared as a SUBSEQUENCE of the source element's
|
|
9
|
+
* tag chain and any mismatch falls to `AMBIGUOUS_LITERAL` rather than to a coin flip.
|
|
10
|
+
*/
|
|
11
|
+
interface PathLocator {
|
|
12
|
+
/** Tag names from the nearest landmark down to the element's parent, outermost first. */
|
|
13
|
+
ancestors: string[];
|
|
14
|
+
/** The element's index among its ELEMENT siblings. */
|
|
15
|
+
siblingIndex: number;
|
|
16
|
+
/** Set when the element is one member of a derived repeater group. */
|
|
17
|
+
repeat?: {
|
|
18
|
+
groupKey: string;
|
|
19
|
+
index: number;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
interface BriefPath {
|
|
23
|
+
/** The `data-bcms-field` value, in the editor's grammar (`hero.title`, `facts[2].label`). */
|
|
24
|
+
path: string;
|
|
25
|
+
/**
|
|
26
|
+
* PAGE or LAYOUT, and the difference is an attribute, a helper and an identity.
|
|
27
|
+
*
|
|
28
|
+
* A value the derive lane found identical on every route is the shared chrome — a header, a
|
|
29
|
+
* footer — and it is ONE field, not one per page. It is read with `bcmsLayout` and declared on
|
|
30
|
+
* `data-bcms-layout-field`, the separate attribute the chrome lane already carries
|
|
31
|
+
* (`src/lib/sites/render-page.ts`), so the block lane's own `[data-bcms-field]` query cannot
|
|
32
|
+
* reach it. Absent means `"page"`: a brief written before the distinction existed still means
|
|
33
|
+
* what it said.
|
|
34
|
+
*/
|
|
35
|
+
scope?: "page" | "layout";
|
|
36
|
+
/** `data-bcms-kind` — "text" | "richtext" | "image", as `kindOf` spells it. */
|
|
37
|
+
kind: string;
|
|
38
|
+
/** The declared field type, which `kind` deliberately loses (richtext vs document). */
|
|
39
|
+
type: string;
|
|
40
|
+
label: string;
|
|
41
|
+
/** The copy the REPO renders today. Null when the import captured no default. */
|
|
42
|
+
original: string | null;
|
|
43
|
+
/** What the CMS holds now. */
|
|
44
|
+
current: string;
|
|
45
|
+
truncated?: true;
|
|
46
|
+
locator?: PathLocator;
|
|
47
|
+
}
|
|
48
|
+
interface BriefPage {
|
|
49
|
+
slug: string;
|
|
50
|
+
/** The URL the built site serves this page at. */
|
|
51
|
+
route: string;
|
|
52
|
+
title?: string;
|
|
53
|
+
paths: BriefPath[];
|
|
54
|
+
pathsOmitted?: number;
|
|
55
|
+
}
|
|
56
|
+
/** Only what the codemod reads. A backend `ConversionBrief` satisfies it. */
|
|
57
|
+
interface Brief {
|
|
58
|
+
pages: BriefPage[];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The name of one brief — `(route, path, kind, original)` for every page, hashed.
|
|
62
|
+
*
|
|
63
|
+
* Identity, not integrity: the receipt carries it so a coverage meter can say "this conversion
|
|
64
|
+
* was run against THAT list of paths" and refuse to compare against a re-derived one. Labels and
|
|
65
|
+
* `current` values are excluded because an edit in the dashboard changes them without changing
|
|
66
|
+
* what there is to convert. So is SCOPE: a field that moves from page to layout is the same field
|
|
67
|
+
* with the same copy, and the pin carries each path's scope separately where it is actually read.
|
|
68
|
+
*
|
|
69
|
+
* 🔴 BYTE-FOR-BYTE `src/lib/content/brief-digest.ts`. The codemod runs on the customer's machine
|
|
70
|
+
* and the server computes the same name for the same brief; the pin is keyed by it and the
|
|
71
|
+
* receipt carries it, so a disagreement makes every receipt stop matching its pin and the
|
|
72
|
+
* coverage meter read zero forever. `brief-digest.test.ts` pins both against one literal hash.
|
|
73
|
+
*/
|
|
74
|
+
declare function briefDigest(brief: Brief): string;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Where in the source does each of the brief's paths actually appear?
|
|
78
|
+
*
|
|
79
|
+
* Moved here verbatim from `src/lib/conversion/propose.ts`, which now re-exports it: the proposer
|
|
80
|
+
* checks "did the conversion bind every path in every file that renders it", and the codemod
|
|
81
|
+
* decides which files to open. A second implementation of "where does this string appear" would
|
|
82
|
+
* drift from the first silently, and the failure it produces is a proposal refused for not binding
|
|
83
|
+
* a path in a file the other half never asked it to touch.
|
|
84
|
+
*/
|
|
85
|
+
/** One file as it was read. Same shape the proposer's `SourceFile` has. */
|
|
86
|
+
interface SourceFile {
|
|
87
|
+
path: string;
|
|
88
|
+
content: string;
|
|
89
|
+
}
|
|
90
|
+
/** A brief path whose `original` was found verbatim in the source. */
|
|
91
|
+
interface LocatedPath {
|
|
92
|
+
route: string;
|
|
93
|
+
/** Page or shared chrome — half of the `(route, scope, path)` identity. @see brief.ts */
|
|
94
|
+
scope?: "page" | "layout";
|
|
95
|
+
path: string;
|
|
96
|
+
kind: string;
|
|
97
|
+
/** The whitespace-normalised copy that was matched. */
|
|
98
|
+
original: string;
|
|
99
|
+
/** Every source file that renders it. A value rendered twice must be bound twice. */
|
|
100
|
+
files: string[];
|
|
101
|
+
}
|
|
102
|
+
/** A brief path the source does not appear to render, with the reason it could not be placed. */
|
|
103
|
+
interface UnlocatedPath {
|
|
104
|
+
route: string;
|
|
105
|
+
scope?: "page" | "layout";
|
|
106
|
+
path: string;
|
|
107
|
+
reason: "no-original" | "not-in-source";
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Collapse whitespace for comparison.
|
|
111
|
+
*
|
|
112
|
+
* The same normalisation the annotator and the live-editor matcher use. A template wraps its copy
|
|
113
|
+
* across lines and indents it; the CMS holds one line. Comparing raw bytes would find nothing on a
|
|
114
|
+
* correctly formatted file, which is every file.
|
|
115
|
+
*/
|
|
116
|
+
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;
|
|
119
|
+
interface BriefLike {
|
|
120
|
+
pages: {
|
|
121
|
+
route: string;
|
|
122
|
+
paths: {
|
|
123
|
+
path: string;
|
|
124
|
+
kind: string;
|
|
125
|
+
original: string | null;
|
|
126
|
+
scope?: "page" | "layout";
|
|
127
|
+
}[];
|
|
128
|
+
}[];
|
|
129
|
+
}
|
|
130
|
+
interface LocateOptions {
|
|
131
|
+
/**
|
|
132
|
+
* Also match a path whose original is the element's TEXT rather than its markup.
|
|
133
|
+
*
|
|
134
|
+
* A richtext field's brief `original` is the first block's text — `Read this now` — while the
|
|
135
|
+
* template says `Read <strong>this</strong> now`. A literal search finds nothing, which is the
|
|
136
|
+
* right answer for the proposer (it must not demand a binding for a string it cannot point at)
|
|
137
|
+
* and the wrong one for the codemod, which can see the element. OFF by default so the moved
|
|
138
|
+
* implementation keeps its exact previous behaviour for `propose.ts`.
|
|
139
|
+
*/
|
|
140
|
+
stripTags?: boolean;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Split the brief's paths into the ones the source renders and the ones it does not.
|
|
144
|
+
*
|
|
145
|
+
* Exported because the generator needs the SAME answer: the transformer is told which originals
|
|
146
|
+
* live in which file, and a second implementation of "where does this string appear" would drift
|
|
147
|
+
* from this one silently — the model would then be asked to bind a path in a file this module has
|
|
148
|
+
* already decided does not render it.
|
|
149
|
+
*/
|
|
150
|
+
declare function locate(brief: BriefLike, sources: SourceFile[], options?: LocateOptions): {
|
|
151
|
+
located: LocatedPath[];
|
|
152
|
+
unlocated: UnlocatedPath[];
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* What the conversion did, and — the half that matters — what it did not.
|
|
157
|
+
*
|
|
158
|
+
* A codemod that silently skips a path presents as a finished conversion and fails months later,
|
|
159
|
+
* when an editor changes a value and the page keeps rendering the old one. So every path in the
|
|
160
|
+
* brief lands in exactly one of four buckets, and the arithmetic saying so is asserted rather than
|
|
161
|
+
* believed: `declared === rewritten + alreadyDeclared + pending.length`.
|
|
162
|
+
*
|
|
163
|
+
* PATH IDENTITY IS `(route, scope, path)`. Occurrences are counted separately, because "this
|
|
164
|
+
* sentence appears in three files" and "this field is bound" are different questions — and a path
|
|
165
|
+
* is only `rewritten` when EVERY located occurrence of it was.
|
|
166
|
+
*/
|
|
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" | "DYNAMIC_PARAMS_UNAVAILABLE" | "BRIEF_META_UNPLACED" | "PARSE_ERROR" | "TIER3_UNVERIFIABLE";
|
|
168
|
+
interface PendingPath {
|
|
169
|
+
route: string;
|
|
170
|
+
scope: "page" | "layout";
|
|
171
|
+
path: string;
|
|
172
|
+
kind: string;
|
|
173
|
+
/** The file the reason is about, when the reason is about one. */
|
|
174
|
+
file?: string;
|
|
175
|
+
reason: PendingReason;
|
|
176
|
+
message?: string;
|
|
177
|
+
}
|
|
178
|
+
/** One file the conversion touched, and the tier that produced it. A file is exactly one tier. */
|
|
179
|
+
interface ReceiptFile {
|
|
180
|
+
path: string;
|
|
181
|
+
bindings: string[];
|
|
182
|
+
tier: 1 | 2 | 3;
|
|
183
|
+
}
|
|
184
|
+
interface ConversionReceipt {
|
|
185
|
+
briefDigest: string;
|
|
186
|
+
paths: {
|
|
187
|
+
declared: number;
|
|
188
|
+
rewritten: number;
|
|
189
|
+
alreadyDeclared: number;
|
|
190
|
+
pending: PendingPath[];
|
|
191
|
+
};
|
|
192
|
+
occurrences: {
|
|
193
|
+
located: number;
|
|
194
|
+
rewritten: number;
|
|
195
|
+
};
|
|
196
|
+
files: ReceiptFile[];
|
|
197
|
+
/** Helper modules written or upgraded, by path. */
|
|
198
|
+
helpers: string[];
|
|
199
|
+
helperUpgraded: boolean;
|
|
200
|
+
/**
|
|
201
|
+
* Bindings declared as an EXPRESSION rather than as a literal path — informative ONLY.
|
|
202
|
+
*
|
|
203
|
+
* 🔴 THE PROPOSER NEVER TRUSTS THIS. It re-derives every entry from its own scan of the
|
|
204
|
+
* proposed files, because a receipt is a claim by the thing being checked. The list is here so
|
|
205
|
+
* a reviewer can see what to look for, and so the scan has somewhere to disagree with.
|
|
206
|
+
*/
|
|
207
|
+
dynamicBindings: DynamicBinding[];
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* One binding whose `data-bcms-field` is an expression, and where its value is written literally.
|
|
211
|
+
*
|
|
212
|
+
* Two shapes produce one: a repeater's `` {`cards[${i}].title`} ``, whose call site is the loop in
|
|
213
|
+
* the same file, and a drilled prop's `{bcmsBindings?.title}`, whose call sites are every place
|
|
214
|
+
* the component is used with a literal in `bcmsBindings`.
|
|
215
|
+
*/
|
|
216
|
+
interface DynamicBinding {
|
|
217
|
+
/** The file carrying the expression. */
|
|
218
|
+
file: string;
|
|
219
|
+
/** The prop the expression reads, or null for a repeater. */
|
|
220
|
+
prop: string | null;
|
|
221
|
+
/** The path it resolves to — `cards[*].title` for a repeater, a real path for a prop. */
|
|
222
|
+
path: string;
|
|
223
|
+
/** Where the literal that makes it resolvable is written. */
|
|
224
|
+
callSites: {
|
|
225
|
+
file: string;
|
|
226
|
+
literal: string;
|
|
227
|
+
}[];
|
|
228
|
+
}
|
|
229
|
+
/** Raised when the receipt does not add up. Never caught inside this package. */
|
|
230
|
+
declare class ReceiptInvariantError extends Error {
|
|
231
|
+
readonly code = "RECEIPT_INVARIANT";
|
|
232
|
+
constructor(message: string);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* The invariant, checked where the receipt is built rather than where it is read.
|
|
236
|
+
*
|
|
237
|
+
* A path that is in none of the four buckets is a path the conversion forgot, and forgetting is
|
|
238
|
+
* exactly the failure mode this whole lane exists to make impossible. Refusing here means a bug in
|
|
239
|
+
* this package surfaces as a loud error on the developer's own fixture run, not as a coverage
|
|
240
|
+
* meter that reads 96 % forever.
|
|
241
|
+
*/
|
|
242
|
+
declare function assertReceipt(receipt: ConversionReceipt): ConversionReceipt;
|
|
243
|
+
/** What a coverage meter reads: how much of the brief this conversion actually made editable. */
|
|
244
|
+
declare function coverageOf(receipt: ConversionReceipt): {
|
|
245
|
+
briefDigest: string;
|
|
246
|
+
declared: number;
|
|
247
|
+
bound: number;
|
|
248
|
+
pending: {
|
|
249
|
+
path: string;
|
|
250
|
+
reason: PendingReason;
|
|
251
|
+
}[];
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Which template language a file is written in — by EXTENSION, and nothing else.
|
|
256
|
+
*
|
|
257
|
+
* Sniffing content would let one badly named file decide which parser runs, and a parser that
|
|
258
|
+
* runs on the wrong grammar does not fail loudly: it returns a plausible tree with the wrong
|
|
259
|
+
* offsets, and the rewrite splices bytes into the middle of something. `content` is accepted
|
|
260
|
+
* because the callers all have it and one distinction will eventually need it (a `.js` file that
|
|
261
|
+
* is really JSX), and until then the answer must not depend on it.
|
|
262
|
+
*/
|
|
263
|
+
type Dialect = "html" | "astro" | "jsx" | "svelte" | "vue";
|
|
264
|
+
declare function dialectOf(path: string, _content?: string): Dialect | null;
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* The ONE matcher, and the neutral tree every dialect parser normalises into.
|
|
268
|
+
*
|
|
269
|
+
* Three parsers disagree about everything except the two facts this package needs: where an
|
|
270
|
+
* element's bytes are, and what text it renders. Normalising to that pair keeps "which element
|
|
271
|
+
* holds this sentence" written once — the alternative is the same subtle traversal in five files,
|
|
272
|
+
* where a rule fixed in one of them stays broken in the other four.
|
|
273
|
+
*/
|
|
274
|
+
interface Range {
|
|
275
|
+
start: number;
|
|
276
|
+
end: number;
|
|
277
|
+
}
|
|
278
|
+
interface AttrNode {
|
|
279
|
+
name: string;
|
|
280
|
+
/** The literal value, or "" for an expression / valueless attribute. */
|
|
281
|
+
value: string;
|
|
282
|
+
/** The value's own characters, quotes excluded. Null when there is nothing to replace. */
|
|
283
|
+
valueRange: Range | null;
|
|
284
|
+
/** `alt={x}` rather than `alt="x"` — never rewritten, never matched. */
|
|
285
|
+
expression: boolean;
|
|
286
|
+
/** The whole `name="value"` span, so an attribute can be replaced outright. */
|
|
287
|
+
range: Range;
|
|
288
|
+
}
|
|
289
|
+
type Node = {
|
|
290
|
+
type: "element";
|
|
291
|
+
tag: string;
|
|
292
|
+
/** The `<` of the open tag. */
|
|
293
|
+
start: number;
|
|
294
|
+
/** Where an attribute may be inserted: after the last attribute, before `/` and `>`. */
|
|
295
|
+
attrInsertAt: number;
|
|
296
|
+
/** Everything between the tags. Null for void and self-closing elements. */
|
|
297
|
+
inner: Range | null;
|
|
298
|
+
attrs: AttrNode[];
|
|
299
|
+
children: Node[];
|
|
300
|
+
/** `<script>` / `<style>`: the children are code, not copy. */
|
|
301
|
+
raw: boolean;
|
|
302
|
+
} | {
|
|
303
|
+
type: "text";
|
|
304
|
+
range: Range;
|
|
305
|
+
value: string;
|
|
306
|
+
} | {
|
|
307
|
+
type: "comment";
|
|
308
|
+
range: Range;
|
|
309
|
+
value: string;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* An expression the codemod does not read — but which may CONTAIN markup it has to see.
|
|
313
|
+
*
|
|
314
|
+
* `children` is populated with the elements nested inside (a `.map(…)` body, a ternary's
|
|
315
|
+
* branches) and is deliberately invisible to `collectSites`: the copy inside an expression is a
|
|
316
|
+
* program's, and matching it would bind a value to a branch that may not render. What does read
|
|
317
|
+
* it is the DECLARATION walk, because a loop this package itself wrote lives here, and a second
|
|
318
|
+
* run that could not see it would convert the same file twice.
|
|
319
|
+
*/
|
|
320
|
+
| {
|
|
321
|
+
type: "expr";
|
|
322
|
+
range: Range;
|
|
323
|
+
value: string;
|
|
324
|
+
children: Node[];
|
|
325
|
+
};
|
|
326
|
+
type SiteWhere = "text" | "attr" | "prop" | "expr" | "script" | "comment" | "data";
|
|
327
|
+
interface Site {
|
|
328
|
+
/** Filled in by the caller; `findSites` is given one file's bytes and does not know its name. */
|
|
329
|
+
file: string;
|
|
330
|
+
tag: string;
|
|
331
|
+
attrInsertAt: number;
|
|
332
|
+
/** The bytes this site's value occupies — a text node, an inner range, or an attribute value. */
|
|
333
|
+
range: Range;
|
|
334
|
+
/** The element's inner range, for the kinds that replace a whole subtree (richtext). */
|
|
335
|
+
inner: Range | null;
|
|
336
|
+
where: SiteWhere;
|
|
337
|
+
scope: "page";
|
|
338
|
+
attr?: string;
|
|
339
|
+
/** The whole `name="value"` span, so an attribute can be rewritten as an expression. */
|
|
340
|
+
attrRange?: Range;
|
|
341
|
+
existingAttrs: Record<string, string>;
|
|
342
|
+
/** The element renders something besides this literal — an icon, a nested element. */
|
|
343
|
+
mixed: boolean;
|
|
344
|
+
/** The one non-blank direct text child, when there is exactly one. The wrap target. */
|
|
345
|
+
textRange: Range | null;
|
|
346
|
+
/** The whitespace-flattened literal this site matched. */
|
|
347
|
+
literal: string;
|
|
348
|
+
/** The literal is only PART of what this element renders; rewriting it would eat the rest. */
|
|
349
|
+
partial: boolean;
|
|
350
|
+
/** Tag chain from the document root down to the element's parent, outermost first. */
|
|
351
|
+
ancestors: string[];
|
|
352
|
+
/** Index among the element's ELEMENT siblings. */
|
|
353
|
+
siblingIndex: number;
|
|
354
|
+
/**
|
|
355
|
+
* Reserved for `emitRepeater`: the sibling group this element belongs to. Set by nothing today —
|
|
356
|
+
* the group comes from the brief's locator, which is the derive lane's answer. @see loops.ts
|
|
357
|
+
*/
|
|
358
|
+
repeat?: {
|
|
359
|
+
groupId: string;
|
|
360
|
+
index: number;
|
|
361
|
+
siblingRanges: Range[];
|
|
362
|
+
};
|
|
363
|
+
/**
|
|
364
|
+
* The `bcmsBindings={{ … }}` already on this element, when there is one.
|
|
365
|
+
*
|
|
366
|
+
* A second prop drilled into the same component must MERGE into that object rather than write a
|
|
367
|
+
* second attribute of the same name, which JSX takes the last of and Astro the first.
|
|
368
|
+
*/
|
|
369
|
+
bindingsAttr?: Range;
|
|
370
|
+
}
|
|
371
|
+
/** A parser that refused. Never thrown across the package boundary — it selects the next tier. */
|
|
372
|
+
interface ParserError {
|
|
373
|
+
code: "PARSE_ERROR" | "DIALECT_UNSUPPORTED";
|
|
374
|
+
message: string;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Where the helper's `import` goes, answered by the parser that already read the file.
|
|
378
|
+
*
|
|
379
|
+
* The rule differs per dialect (frontmatter top, after the last import, `<script>` top) and every
|
|
380
|
+
* version of it needs a position only a parse knows. Returning it with the sites keeps one parse
|
|
381
|
+
* per file, which is also what keeps the offsets in one coordinate system.
|
|
382
|
+
*/
|
|
383
|
+
interface ImportPoint {
|
|
384
|
+
at: number;
|
|
385
|
+
/**
|
|
386
|
+
* The file has no block to put an import IN, so the caller opens one around the import lines.
|
|
387
|
+
*
|
|
388
|
+
* Astro wants a frontmatter fence, Svelte and Vue want a `<script>`. One field rather than one
|
|
389
|
+
* boolean per dialect: the parser knows what its own language opens with, and `rewrite.ts` only
|
|
390
|
+
* has to splice what it is given.
|
|
391
|
+
*/
|
|
392
|
+
wrap?: {
|
|
393
|
+
before: string;
|
|
394
|
+
after: string;
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
interface FindSitesResult {
|
|
398
|
+
sites: Site[];
|
|
399
|
+
error?: ParserError;
|
|
400
|
+
importAt?: ImportPoint;
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* ONE PARSE PER FILE, handed to everything that needs it.
|
|
404
|
+
*
|
|
405
|
+
* The matcher wants the neutral tree; prop drilling wants the module's own AST; the alias
|
|
406
|
+
* allocator wants its top-level bindings. Parsing three times would be three coordinate systems
|
|
407
|
+
* for one file, which is how a splice lands in the middle of a tag.
|
|
408
|
+
*/
|
|
409
|
+
interface ParsedFile {
|
|
410
|
+
roots: Node[];
|
|
411
|
+
error?: ParserError;
|
|
412
|
+
importAt?: ImportPoint;
|
|
413
|
+
/** The dialect's own AST, when it has one worth keeping (jsx: babel; astro: the frontmatter). */
|
|
414
|
+
ast?: unknown;
|
|
415
|
+
/** The file's TypeScript/JavaScript region, for the dialects that separate it from the markup. */
|
|
416
|
+
script?: {
|
|
417
|
+
code: string;
|
|
418
|
+
offset: number;
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Tier 2: what can still be done for a file no parser would read.
|
|
424
|
+
*
|
|
425
|
+
* A template with a syntax error, or a dialect this build cannot parse, still has its copy sitting
|
|
426
|
+
* between a `>` and a `<`. That is enough to place a binding — and ONLY when the literal occurs
|
|
427
|
+
* exactly once in the file, because without a tree there is nothing to tell two copies apart and
|
|
428
|
+
* a wrong guess splices bytes into the middle of someone's markup.
|
|
429
|
+
*
|
|
430
|
+
* Everything this tier cannot answer is left to tier 3 or reported as pending. It never widens
|
|
431
|
+
* its own rules to cover one more case: the whole reason it is allowed to run on an unparsable
|
|
432
|
+
* file is that its rule is narrow enough to be obviously safe.
|
|
433
|
+
*/
|
|
434
|
+
|
|
435
|
+
declare function findSitesTolerant(content: string, literals: string[]): Site[];
|
|
436
|
+
|
|
437
|
+
/** One entry point per dialect, chosen by a table rather than by a chain of ifs. */
|
|
438
|
+
|
|
439
|
+
type Parser = (content: string) => ParsedFile | Promise<ParsedFile>;
|
|
440
|
+
declare const PARSE_FILE: Record<Dialect, Parser>;
|
|
441
|
+
/**
|
|
442
|
+
* ONE parse of one file, in the dialect's own parser. A parser that refuses returns an error on
|
|
443
|
+
* the result; it never throws at the caller, because a refusal SELECTS the next tier.
|
|
444
|
+
*/
|
|
445
|
+
declare const parseFile: (dialect: Dialect, content: string) => Promise<ParsedFile>;
|
|
446
|
+
/** Tier 1 for one file: parse, then match. */
|
|
447
|
+
declare const findSites: (dialect: Dialect, content: string, literals: string[]) => Promise<FindSitesResult>;
|
|
448
|
+
|
|
449
|
+
/** One path, one place in one file, and what to write there. */
|
|
450
|
+
interface Rewrite {
|
|
451
|
+
site: Site;
|
|
452
|
+
/**
|
|
453
|
+
* The receipt's path identity — `(route, scope, path)` — carried, never re-derived.
|
|
454
|
+
*
|
|
455
|
+
* 🔴 A PATH IS NOT AN IDENTITY. Two routes rendered by ONE file legitimately carry the same
|
|
456
|
+
* path, and recovering "which target is this?" by path alone picks whichever one sorted first:
|
|
457
|
+
* the page reads `bcmsAbout` while the file imports only `bcmsHome`, and the site does not
|
|
458
|
+
* build. Every consumer below takes the answer from the rewrite it is already holding.
|
|
459
|
+
*/
|
|
460
|
+
key: string;
|
|
461
|
+
path: string;
|
|
462
|
+
/** The brief's kind: "text" | "richtext" | "image". */
|
|
463
|
+
kind: string;
|
|
464
|
+
/** The copy the repo renders today, kept as the in-code fallback. */
|
|
465
|
+
fallback: string;
|
|
466
|
+
/** The identifier this page's snapshot is imported under. */
|
|
467
|
+
snapshot: string;
|
|
468
|
+
/** The page slug that snapshot belongs to. The file's import set is derived from these. */
|
|
469
|
+
slug: string;
|
|
470
|
+
/** Page copy, or the shared chrome — which decides the attribute AND the helper. @see expr.ts */
|
|
471
|
+
scope: "page" | "layout";
|
|
472
|
+
dynamic: boolean;
|
|
473
|
+
/**
|
|
474
|
+
* A whole span replaced by text computed elsewhere — a repeater's loop.
|
|
475
|
+
*
|
|
476
|
+
* The loop's own leaves were rewritten against a COPY of the row, so its bytes arrive finished;
|
|
477
|
+
* splicing them here keeps every write to one file in one `magic-string` pass. @see loops.ts
|
|
478
|
+
*/
|
|
479
|
+
raw?: string;
|
|
480
|
+
/**
|
|
481
|
+
* The literal half of a drilled prop: `bcmsBindings={{ title: "hero.title" }}` at the call site.
|
|
482
|
+
*
|
|
483
|
+
* Absent when hop 2 missed — the value still comes from the CMS, and nothing claims a binding
|
|
484
|
+
* that does not exist. @see props.ts
|
|
485
|
+
*/
|
|
486
|
+
bindings?: {
|
|
487
|
+
prop: string;
|
|
488
|
+
path: string;
|
|
489
|
+
};
|
|
490
|
+
/**
|
|
491
|
+
* How the call site's existing `bcmsBindings` is joined, when it has one.
|
|
492
|
+
*
|
|
493
|
+
* `object` merges into `{{ … }}`; `spread` wraps whatever expression is there in a new object
|
|
494
|
+
* literal. A shape neither of those covers is refused by the caller before it reaches here.
|
|
495
|
+
*/
|
|
496
|
+
bindingsMerge?: {
|
|
497
|
+
kind: "object" | "spread";
|
|
498
|
+
range: Range;
|
|
499
|
+
inner: string;
|
|
500
|
+
insertAt: number;
|
|
501
|
+
/** What goes before the new members — `", "`, or `" "` when the object already ends in one. */
|
|
502
|
+
separator: string;
|
|
503
|
+
/** What goes after them, so an empty object closes as `{{ … }}` rather than `{{ …}}`. */
|
|
504
|
+
trailing: string;
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
interface FileImports {
|
|
508
|
+
/** `bcmsHome` → `../../bcms-content/home.json`. */
|
|
509
|
+
snapshots: {
|
|
510
|
+
identifier: string;
|
|
511
|
+
specifier: string;
|
|
512
|
+
}[];
|
|
513
|
+
/** The helper module, relative to this file. Null when the dialect needs none. */
|
|
514
|
+
helper: string | null;
|
|
515
|
+
/** The name `bcms` is imported under here — `bcms` unless this module already binds it. */
|
|
516
|
+
read: string;
|
|
517
|
+
/** Every named helper this file uses, with the name it was imported under. */
|
|
518
|
+
helpers: {
|
|
519
|
+
name: string;
|
|
520
|
+
alias: string;
|
|
521
|
+
}[];
|
|
522
|
+
/** The name `bcmsLayout` is imported under here. */
|
|
523
|
+
layout: string;
|
|
524
|
+
/** How THIS file reads its route parameter, when it is a dynamic route. @see routes.ts */
|
|
525
|
+
slug: string | null;
|
|
526
|
+
/** Import lines something other than the helper needs — a route parameter's store. */
|
|
527
|
+
extra?: string[];
|
|
528
|
+
at: ImportPoint | undefined;
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* The rewrites whose ranges collide, which is how one element ends up claimed by two paths.
|
|
532
|
+
*
|
|
533
|
+
* Reported rather than resolved: splicing both would corrupt the file and picking one would be a
|
|
534
|
+
* coin flip the receipt could not explain. The caller drops them as `AMBIGUOUS_LITERAL` and the
|
|
535
|
+
* rest of the file still converts.
|
|
536
|
+
*/
|
|
537
|
+
declare function overlapping(rewrites: Rewrite[]): Rewrite[];
|
|
538
|
+
/**
|
|
539
|
+
* Apply every rewrite to one file.
|
|
540
|
+
*
|
|
541
|
+
* Attribute insertions are grouped by element first: an `<img>` that binds both its `src` and its
|
|
542
|
+
* `alt` gets one `data-bcms-props`, not two attributes with the same name — which a browser
|
|
543
|
+
* silently keeps only the first of.
|
|
544
|
+
*/
|
|
545
|
+
declare function rewriteFile(content: string, dialect: Dialect, rewrites: Rewrite[], imports: FileImports,
|
|
546
|
+
/** Edits this file needs that no binding asked for — a page component's `{ params }`. */
|
|
547
|
+
extra?: {
|
|
548
|
+
start: number;
|
|
549
|
+
end: number;
|
|
550
|
+
text: string;
|
|
551
|
+
}[]): string;
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* The two strings the codemod writes: the READ (how a template asks the CMS for a value) and the
|
|
555
|
+
* DECLARATION (the attributes that say which element holds it).
|
|
556
|
+
*
|
|
557
|
+
* Both are literal text on purpose. `data-bcms-field={paths.hero.title}` would be a binding
|
|
558
|
+
* nothing can verify before a release — the proposer refuses it by name — so this module never
|
|
559
|
+
* emits an expression where a path belongs, and the fallback is the exact copy the repo renders
|
|
560
|
+
* today so a build with an empty snapshot renders the site it rendered before.
|
|
561
|
+
*/
|
|
562
|
+
|
|
563
|
+
/** How a dialect spells the things that differ. Data, so a sixth dialect is a row. */
|
|
564
|
+
interface DialectRule {
|
|
565
|
+
/** A value in TEXT position, or null when this dialect only ever writes attributes. */
|
|
566
|
+
text: ((expression: string) => string) | null;
|
|
567
|
+
/** The attribute that replaces an element's children with HTML. */
|
|
568
|
+
richtext: ((expression: string) => string) | null;
|
|
569
|
+
/** `alt={…}` — a value in ATTRIBUTE position. */
|
|
570
|
+
attr: ((name: string, expression: string) => string) | null;
|
|
571
|
+
/** How this dialect reads the route parameter on a dynamic route. */
|
|
572
|
+
slug: string | null;
|
|
573
|
+
/**
|
|
574
|
+
* How this dialect repeats one element over a list.
|
|
575
|
+
*
|
|
576
|
+
* TWO SHAPES, because the dialects genuinely have two: jsx, astro and svelte WRAP the row in a
|
|
577
|
+
* construct, vue puts `v-for` ON the row's open tag. One shape forced onto the other would mean
|
|
578
|
+
* emitting a `<template v-for>` wrapper nobody writes by hand, in a diff a human has to approve.
|
|
579
|
+
*/
|
|
580
|
+
loop: {
|
|
581
|
+
kind: "wrap";
|
|
582
|
+
open: (rows: string, row: string, index: string) => string;
|
|
583
|
+
close: string;
|
|
584
|
+
} | {
|
|
585
|
+
kind: "attr";
|
|
586
|
+
attr: (rows: string, row: string, index: string) => string;
|
|
587
|
+
} | null;
|
|
588
|
+
/** An extra attribute every row carries — React's list key. Null where the dialect needs none. */
|
|
589
|
+
loopKey: ((index: string) => string) | null;
|
|
590
|
+
/** The helper module, repository-relative, or null when the dialect needs none. */
|
|
591
|
+
helper: string | null;
|
|
592
|
+
/** How an import statement is spelled, and where it goes. */
|
|
593
|
+
importAt: "frontmatter" | "afterImports" | null;
|
|
594
|
+
}
|
|
595
|
+
declare const DIALECT_RULES: Record<Dialect, DialectRule>;
|
|
596
|
+
/**
|
|
597
|
+
* `bcms(<snapshot>, "<path>", <fallback>)` — the call a template makes.
|
|
598
|
+
*
|
|
599
|
+
* `page` is the IDENTIFIER the page's snapshot was imported under, not the snapshot itself: this
|
|
600
|
+
* module writes source, and the import is added once per file by `rewrite.ts`. Same for `fn`,
|
|
601
|
+
* the name the helper itself was imported under.
|
|
602
|
+
*/
|
|
603
|
+
declare function readExpr(dialect: Dialect, page: string, path: string, _kind: string, fallback: string, route: {
|
|
604
|
+
dynamic: boolean;
|
|
605
|
+
fn?: string;
|
|
606
|
+
slug?: string | null;
|
|
607
|
+
}): string;
|
|
608
|
+
/** Attribute bindings for one element: `{ alt: "hero.image.alt" }` → `data-bcms-props`. */
|
|
609
|
+
interface AttrBinding {
|
|
610
|
+
attr: string;
|
|
611
|
+
path: string;
|
|
612
|
+
kind: BindingKind;
|
|
613
|
+
/** Page copy, or the shared chrome. Decides the prefix the ADDRESS carries. @see propsAttribute */
|
|
614
|
+
scope?: "page" | "layout";
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* The declaration, as attribute text ready to splice after an open tag's last attribute.
|
|
618
|
+
*
|
|
619
|
+
* A kind is emitted whenever the brief states one — which, for a brief built by `flattenBindings`,
|
|
620
|
+
* is ALWAYS: `kindOf` spells plain text `"text"`, not `""`. The proposer then requires
|
|
621
|
+
* `data-bcms-kind` on that element, so omitting it because the platform's prose says "plain text
|
|
622
|
+
* needs no kind" is what would get the conversion refused.
|
|
623
|
+
*/
|
|
624
|
+
declare function attrsFor(path: string, kind: string, props?: AttrBinding[], scope?: "page" | "layout"): string;
|
|
625
|
+
/**
|
|
626
|
+
* `data-bcms-props` for a set of attribute bindings, or "" when there are none.
|
|
627
|
+
*
|
|
628
|
+
* 🔴 THE ADDRESS CARRIES ITS LANE HERE TOO. A value in an attribute — a promoted nav link's
|
|
629
|
+
* `href` — is declared on `data-bcms-props` rather than on a field attribute, and the raw path was
|
|
630
|
+
* written there with no `layout:` prefix. The release reader then classified a chrome address as a
|
|
631
|
+
* page one, found no page field by that name, and reported it unmatched on every release. One
|
|
632
|
+
* prefix, applied wherever an address is written.
|
|
633
|
+
*/
|
|
634
|
+
declare function propsAttribute(props: AttrBinding[]): string;
|
|
635
|
+
|
|
636
|
+
/** Bumped whenever the emitted source changes. `isKnownHelper` upgrades anything older. */
|
|
637
|
+
declare const HELPER_VERSION = 2;
|
|
638
|
+
/** Where each dialect's helper lives. Null = this dialect reads nothing at build time. */
|
|
639
|
+
declare const HELPER_PATH: Record<Dialect, string | null>;
|
|
640
|
+
/** The directory the snapshots live in, at the repository root. */
|
|
641
|
+
declare const CONTENT_DIR = "bcms-content";
|
|
642
|
+
/** The stub the codemod commits, and the exact content the proposer's NEW_FILE exemption allows. */
|
|
643
|
+
declare const STUB_CONTENT = "{}";
|
|
644
|
+
/** `src/pages/index.astro` + `bcms-content/home.json` → `../../bcms-content/home.json`. */
|
|
645
|
+
declare function relativeImport(fromFile: string, toFile: string): string;
|
|
646
|
+
/**
|
|
647
|
+
* The helper, for one dialect.
|
|
648
|
+
*
|
|
649
|
+
* `bcms` takes the SNAPSHOT as its first argument because the import is page-scoped: a helper that
|
|
650
|
+
* imported every page's content would put a 4,974-entry workspace into one client bundle.
|
|
651
|
+
* `bcmsLayout` is the exception and imports directly, because the layout is one shared object by
|
|
652
|
+
* definition — that is what makes it the layout. `bcmsRows` is `bcms` for a value that is a LIST:
|
|
653
|
+
* a repeater's loop reads it once and each row reads its own leaves back through `bcms(card, …)`.
|
|
654
|
+
*/
|
|
655
|
+
declare function helperSource(dialect: Dialect): string;
|
|
656
|
+
/**
|
|
657
|
+
* The version of a helper WE wrote, byte for byte, or null for anything else.
|
|
658
|
+
*
|
|
659
|
+
* Null covers both "somebody else's module lives here" and "our module, edited" — the caller
|
|
660
|
+
* treats them the same way, because it cannot tell them apart and must not overwrite either.
|
|
661
|
+
*/
|
|
662
|
+
declare function isKnownHelper(content: string, dialect: Dialect): number | null;
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Which files in a repository can possibly render a page's copy.
|
|
666
|
+
*
|
|
667
|
+
* ONE list, shared by the CLI (which walks a working tree) and by the server lane's `acquire.ts`
|
|
668
|
+
* (which walks a GitHub tree). They were two copies of the same three arrays; a file kind added
|
|
669
|
+
* to one and not the other means the two lanes convert different sites from the same repository,
|
|
670
|
+
* and nothing would say so.
|
|
671
|
+
*/
|
|
672
|
+
/** Directories that never hold a template a human wrote. */
|
|
673
|
+
declare const SKIP_DIRS: string[];
|
|
674
|
+
/** Generated or machine-owned files. A lockfile can contain anything and means nothing. */
|
|
675
|
+
declare const SKIP_FILES: string[];
|
|
676
|
+
/**
|
|
677
|
+
* Extensions that can render copy. Everything else — images, fonts, data — is skipped unread.
|
|
678
|
+
*
|
|
679
|
+
* Wider than the five dialects the codemod can rewrite, deliberately: a literal that lives only
|
|
680
|
+
* in an `.md` or a `.liquid` has to be REPORTED as `DIALECT_UNSUPPORTED`, and a file nobody
|
|
681
|
+
* opened cannot be reported at all.
|
|
682
|
+
*/
|
|
683
|
+
declare const SOURCE_EXTENSIONS: string[];
|
|
684
|
+
/** Is this repository-relative path a file worth opening? */
|
|
685
|
+
declare function isSourceCandidate(path: string): boolean;
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* The project's own `compilerOptions.paths`, when a tsconfig is among the files.
|
|
689
|
+
*
|
|
690
|
+
* 🔴 THE PROJECT'S, NEVER A DEFAULT. `@/` means `src/` in most Next projects and something else in
|
|
691
|
+
* plenty of others; assuming it would resolve `<Hero>` to a file that is not the one the build
|
|
692
|
+
* uses, and the edit would land in a component nobody renders. No tsconfig means relative
|
|
693
|
+
* specifiers only, which is a smaller conversion and a true one.
|
|
694
|
+
*/
|
|
695
|
+
declare function aliasesFrom(sources: SourceFile[]): Map<string, string[]>;
|
|
696
|
+
/** The repository path a specifier names, or null when it is not among the files we were given. */
|
|
697
|
+
declare function resolveSpecifier(fromFile: string, specifier: string, files: Set<string>, aliases: Map<string, string[]>): string | null;
|
|
698
|
+
/** What one local name was imported from, and WHICH export of it. */
|
|
699
|
+
interface ImportedFrom {
|
|
700
|
+
specifier: string;
|
|
701
|
+
/** The exported name, or null for a default import. */
|
|
702
|
+
imported: string | null;
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Local name → what it was imported from, for one file.
|
|
706
|
+
*
|
|
707
|
+
* From the parse: a regex over `import` lines cannot tell a real import from one inside a string
|
|
708
|
+
* or a comment, and this decides which file gets edited. The EXPORT is carried too, because a
|
|
709
|
+
* module legitimately exports several components and only one of them renders this prop.
|
|
710
|
+
*/
|
|
711
|
+
declare function importsIn(parsed: ParsedFile, _dialect: Dialect): Map<string, ImportedFrom>;
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* A literal that reaches the page as a PROP — `<Hero title="Ship your site today" />` — and the
|
|
715
|
+
* element inside the component that actually renders it.
|
|
716
|
+
*
|
|
717
|
+
* 🔴 THE ATTRIBUTE ON `<Hero>` IS NOT A BINDING. React and Astro pass unknown props nowhere, so a
|
|
718
|
+
* `data-bcms-field` written there reaches no DOM node: every release reports the path unmatched,
|
|
719
|
+
* and the value the editor changes never appears. The declaration has to go on the `<h1>` inside
|
|
720
|
+
* `Hero`, which is in a different file — so the conversion is TWO HOPS, and it is one proposal
|
|
721
|
+
* because half of it is worse than neither half.
|
|
722
|
+
*
|
|
723
|
+
* WHAT THE SECOND HOP WRITES, and why an expression is unavoidable here. The component renders
|
|
724
|
+
* every call site, and different call sites carry different paths, so the element cannot declare a
|
|
725
|
+
* literal one: it declares `data-bcms-field={bcmsBindings?.title}`, and the LITERAL lives at each
|
|
726
|
+
* call site in `bcmsBindings={{ title: "hero.title" }}`. That is the one shape this package emits
|
|
727
|
+
* that a reader cannot verify by looking at the element alone — which is exactly why
|
|
728
|
+
* `proposeConversion` re-derives it from its OWN scan of every call site and never from the
|
|
729
|
+
* receipt.
|
|
730
|
+
*
|
|
731
|
+
* A MISS IS A REFUSAL, NOT A GUESS. If the component cannot be resolved, or nothing in it renders
|
|
732
|
+
* the prop, hop 1 still makes the value come from the CMS (the page is no worse off) but the
|
|
733
|
+
* bindings prop is DROPPED and the path is reported `PROP_TARGET_NOT_FOUND` — a declaration whose
|
|
734
|
+
* other half does not exist is a binding that reads as done and is not.
|
|
735
|
+
*/
|
|
736
|
+
|
|
737
|
+
/** The narrowed AST shape. @see sites/jsx.ts for the same narrowing, for the same reason. */
|
|
738
|
+
interface AstNode {
|
|
739
|
+
type: string;
|
|
740
|
+
start?: number | null;
|
|
741
|
+
end?: number | null;
|
|
742
|
+
[key: string]: unknown;
|
|
743
|
+
}
|
|
744
|
+
/** Walk every node of an AST once. */
|
|
745
|
+
declare function walkAst(node: unknown, visit: (node: AstNode) => void, seen?: Set<unknown>): void;
|
|
746
|
+
/**
|
|
747
|
+
* The function behind ONE export of a module.
|
|
748
|
+
*
|
|
749
|
+
* 🔴 A MODULE IS NOT A COMPONENT. `Hero.tsx` legitimately exports `Hero`, a `HeroSkeleton` and a
|
|
750
|
+
* `useHero`, and scanning every function in it for a `title` prop answers with whichever one
|
|
751
|
+
* happened to have a parameter of that name — a declaration written into a component the call
|
|
752
|
+
* site does not render. The import said which export it wants; that is the one analysed.
|
|
753
|
+
*/
|
|
754
|
+
declare function exportedFunction(ast: unknown, imported: string | null): AstNode | null;
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Is THIS element already converted for THIS target — declaration and read, together?
|
|
758
|
+
*
|
|
759
|
+
* 🔴 A FILE-WIDE DECLARATION PLUS A FILE-WIDE READ IS NOT EVIDENCE OF EITHER. A page that renders
|
|
760
|
+
* two routes carries both, for different elements; a page converted for `/` and a hardcoded `<h2>`
|
|
761
|
+
* for `/about` satisfies "the file declares that path" and "the file reads that path" while the
|
|
762
|
+
* second sentence never reflects an edit again. And a `bcms(…)` sitting three elements away — or
|
|
763
|
+
* in an unused statement at the top of the module — answers the same way. So the question is
|
|
764
|
+
* asked of ONE element's own bytes, against identifiers this file actually imports.
|
|
765
|
+
*
|
|
766
|
+
* Used by both ends of the conversion: the per-file prefilter, which decides whether a target has
|
|
767
|
+
* already been done, and tier 3's verification, which decides whether a model's output may be
|
|
768
|
+
* believed. One implementation, because the two must not disagree about what "converted" means.
|
|
769
|
+
*/
|
|
770
|
+
|
|
771
|
+
/** The identity a declaration is compared against. */
|
|
772
|
+
interface TargetIdentity {
|
|
773
|
+
scope: "page" | "layout";
|
|
774
|
+
path: string;
|
|
775
|
+
/** The page slug whose snapshot the read must name. Unused for layout scope. */
|
|
776
|
+
slug: string;
|
|
777
|
+
}
|
|
778
|
+
/** One element that declares something, and the bytes a reader of THAT element would see. */
|
|
779
|
+
interface DeclaringElement {
|
|
780
|
+
scope: "page" | "layout";
|
|
781
|
+
path: string;
|
|
782
|
+
kind: string;
|
|
783
|
+
/** The open tag plus this element's OWN text and expressions — never a descendant's. */
|
|
784
|
+
own: string;
|
|
785
|
+
}
|
|
786
|
+
/** Every element in a parse that declares a binding, with its own bytes. */
|
|
787
|
+
declare function declaringElements(roots: Node[], content: string): DeclaringElement[];
|
|
788
|
+
/**
|
|
789
|
+
* Has this file already converted this target — on an element of its own, with a real read?
|
|
790
|
+
*
|
|
791
|
+
* Every half is required and every half is scoped: the DECLARATION has to be for this scope and
|
|
792
|
+
* this path, the READ has to be inside that same element, and the identifiers it names have to be
|
|
793
|
+
* ones this file imports. A repeater additionally needs the array read, which lives on the loop
|
|
794
|
+
* rather than on the row.
|
|
795
|
+
*/
|
|
796
|
+
declare function convertedHere(content: string, parsed: ParsedFile, dialect: Dialect, target: TargetIdentity, file: string): boolean;
|
|
797
|
+
|
|
798
|
+
interface FileDeclaration {
|
|
799
|
+
scope: "page" | "layout";
|
|
800
|
+
path: string;
|
|
801
|
+
}
|
|
802
|
+
/** Every path one file declares, with the lane it declares it on. */
|
|
803
|
+
declare function readDeclarations(file: string, content: string): Promise<FileDeclaration[]>;
|
|
804
|
+
|
|
805
|
+
/** One file operation, exactly as `src/lib/github/plan-digest.ts` spells it. */
|
|
806
|
+
interface PlanFile {
|
|
807
|
+
path: string;
|
|
808
|
+
operation: "modify" | "add";
|
|
809
|
+
content: string;
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Tier 3, INJECTED by the server lane and never by this package.
|
|
813
|
+
*
|
|
814
|
+
* The CLI passes none: the coding agent running it IS tier 3, and the playbook tells it to take
|
|
815
|
+
* the `PARSE_ERROR` list itself.
|
|
816
|
+
*/
|
|
817
|
+
type LlmFallback = (file: SourceFile, remaining: LocatedPath[]) => Promise<string | null>;
|
|
818
|
+
interface ConvertOptions {
|
|
819
|
+
llmFallback?: LlmFallback;
|
|
820
|
+
/** Replace a helper module this package did not write, instead of refusing. The CLI's flag. */
|
|
821
|
+
overwriteHelper?: boolean;
|
|
822
|
+
}
|
|
823
|
+
type ConvertErrorCode = "HELPER_CONFLICT";
|
|
824
|
+
declare class ConvertError extends Error {
|
|
825
|
+
readonly code: ConvertErrorCode;
|
|
826
|
+
constructor(code: ConvertErrorCode, message: string);
|
|
827
|
+
}
|
|
828
|
+
/** One replacement in a file this conversion is not otherwise rewriting — a drilled component. */
|
|
829
|
+
interface Splice {
|
|
830
|
+
start: number;
|
|
831
|
+
end: number;
|
|
832
|
+
text: string;
|
|
833
|
+
}
|
|
834
|
+
declare function convertSources(brief: Brief, sources: SourceFile[], options?: ConvertOptions): Promise<{
|
|
835
|
+
files: PlanFile[];
|
|
836
|
+
receipt: ConversionReceipt;
|
|
837
|
+
}>;
|
|
838
|
+
|
|
839
|
+
export { type AstNode, type AttrBinding, type Brief, type BriefPage, type BriefPath, CONTENT_DIR, type ConversionReceipt, ConvertError, type ConvertErrorCode, type ConvertOptions, DIALECT_RULES, type Dialect, type DynamicBinding, type FileDeclaration, type FindSitesResult, HELPER_PATH, HELPER_VERSION, type ImportedFrom, type LlmFallback, type LocatedPath, type Node, PARSE_FILE, type ParsedFile, type ParserError, type PathLocator, type PendingPath, type PendingReason, type PlanFile, type ReceiptFile, ReceiptInvariantError, type Rewrite, SKIP_DIRS, SKIP_FILES, SOURCE_EXTENSIONS, STUB_CONTENT, type Site, type SiteWhere, type SourceFile, type Splice, type TargetIdentity, type UnlocatedPath, aliasesFrom, assertReceipt, attrsFor, briefDigest, convertSources, convertedHere, coverageOf, declaringElements, dialectOf, exportedFunction, findSites, findSitesTolerant, flat, helperSource, importsIn, isKnownHelper, isSourceCandidate, locate, overlapping, parseFile, propsAttribute, readDeclarations, readExpr, relativeImport, resolveSpecifier, rewriteFile, stripTags, walkAst };
|