@appthreat/atom-parsetools 1.4.0 → 1.6.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/svelteAst.js ADDED
@@ -0,0 +1,1291 @@
1
+ // Svelte single-file component support for astgen.
2
+ //
3
+ // A `.svelte` file mixes a JavaScript/TypeScript `<script>` block with an HTML
4
+ // template that uses `{...}` expression tags and `{#if}`/`{#each}`/... logic
5
+ // blocks. Neither half parses as the other, so a single Babel parse of the raw
6
+ // document cannot work. Instead `svelte/compiler`'s `parse(src, { modern: true })`
7
+ // is used purely as a *segmenter*: it returns a tree whose every node carries
8
+ // an absolute byte range into the original file. From that tree we produce a
9
+ // regular Babel `File` AST in three steps:
10
+ //
11
+ // 1. Script blocks are parsed by Babel over a masked buffer - a same-length
12
+ // copy of the file where every character outside the script bodies is
13
+ // blanked to a space (newlines preserved). Because nothing moves, the
14
+ // resulting statement offsets are already absolute.
15
+ // 2. The template is walked and re-emitted as standard Babel JSX nodes
16
+ // (JSXElement, JSXExpressionContainer, JSXFragment, ...). Zero new node
17
+ // types are introduced, so downstream consumers that understand Babel JSX
18
+ // need no changes.
19
+ // 3. Every template expression/pattern/declaration is sub-parsed with Babel
20
+ // from its own source substring and its offsets shifted back into file
21
+ // coordinates.
22
+ //
23
+ // The result keeps two invariants everywhere:
24
+ //
25
+ // * every `start`/`end` is an absolute byte offset into the `.svelte` source
26
+ // and `loc` is rebuilt from those offsets (`loc.start.index === start`), so
27
+ // `src.slice(node.start, node.end)` is always the node's original text;
28
+ // * every emitted `type` is a stock Babel node type.
29
+ //
30
+ // Synthesized (non-Babel-parsed) nodes additionally carry two additive keys for
31
+ // traceability: `svelteKind` (the originating Svelte node type, e.g.
32
+ // "EachBlock") and, where meaningful, `svelteName` (e.g. the tag name).
33
+ // Consumers ignore unknown keys, so these are safe to emit.
34
+ //
35
+ // Limitations, by design: `<style>` blocks and HTML comments are dropped, and
36
+ // `{@const}` is modelled as an assignment rather than a declaration (see
37
+ // docs/ASTGEN.md, "Svelte" for the full list of accepted losses).
38
+
39
+ import { parse as svelteCompilerParse } from "svelte/compiler";
40
+ import { parse as babelParse, parseExpression } from "@babel/parser";
41
+
42
+ // Identifier substituted for a template expression that Babel could not
43
+ // sub-parse. The file is never aborted over one bad expression; the failure is
44
+ // recorded in `File.errors` instead.
45
+ const UNPARSED_IDENTIFIER_NAME = "__astgen_unparsed";
46
+
47
+ const TRANSITION_DIRECTIVE_PREFIXES = ["transition", "in", "out"];
48
+
49
+ // `name_loc` in Svelte's modern AST is a { line, column, character } pair, not
50
+ // a plain offset pair; the byte offsets live under `.character`.
51
+ const nameLocStart = (node) => node.name_loc?.start?.character ?? node.start;
52
+ const nameLocEnd = (node) => node.name_loc?.end?.character ?? node.end;
53
+
54
+ /** Per-file parse context threaded through all helpers below. */
55
+ class SvelteParseContext {
56
+ constructor(file, src, options, errors) {
57
+ this.file = file;
58
+ this.src = src;
59
+ // Babel options for every sub-parse; supplied by the caller so there is a
60
+ // single option set shared with regular JS/TS parsing in astgen.js.
61
+ this.options = options;
62
+ this.errors = errors;
63
+ // Offsets of the first character of every line, for rebuilding `loc`.
64
+ this.lineStarts = [0];
65
+ for (let i = 0; i < src.length; i++) {
66
+ if (src[i] === "\n") {
67
+ this.lineStarts.push(i + 1);
68
+ }
69
+ }
70
+ }
71
+
72
+ /** { line (1-based), column (0-based), index } for an absolute offset. */
73
+ posOf(offset) {
74
+ const lineStarts = this.lineStarts;
75
+ let lo = 0;
76
+ let hi = lineStarts.length - 1;
77
+ while (lo < hi) {
78
+ const mid = (lo + hi + 1) >> 1;
79
+ if (lineStarts[mid] <= offset) {
80
+ lo = mid;
81
+ } else {
82
+ hi = mid - 1;
83
+ }
84
+ }
85
+ return { line: lo + 1, column: offset - lineStarts[lo], index: offset };
86
+ }
87
+
88
+ slice(start, end) {
89
+ return this.src.slice(start, end);
90
+ }
91
+
92
+ recordSubParseFailure(message, start, end) {
93
+ this.errors.push({ svelteSubParse: true, message, start, end });
94
+ }
95
+
96
+ /** Fresh loc object for a synthesized node spanning [start, end). */
97
+ locBetween(start, end) {
98
+ return { start: this.posOf(start), end: this.posOf(end) };
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Recursively add `delta` to every start/end/range on `node` (in place) and
104
+ * rebuild `loc` from the original file's line index. Value fields such as
105
+ * `extra.raw` or `TemplateElement.value` hold source strings, not offsets, and
106
+ * are left untouched.
107
+ */
108
+ const shiftNode = (ctx, node, delta) => {
109
+ if (Array.isArray(node)) {
110
+ for (const child of node) {
111
+ shiftNode(ctx, child, delta);
112
+ }
113
+ return node;
114
+ }
115
+ if (!node || typeof node !== "object") {
116
+ return node;
117
+ }
118
+ if (typeof node.start === "number" && typeof node.end === "number") {
119
+ node.start += delta;
120
+ node.end += delta;
121
+ if (Array.isArray(node.range) && node.range.length === 2) {
122
+ node.range = [node.range[0] + delta, node.range[1] + delta];
123
+ }
124
+ if (node.loc) {
125
+ node.loc = { start: ctx.posOf(node.start), end: ctx.posOf(node.end) };
126
+ }
127
+ }
128
+ for (const key of Object.keys(node)) {
129
+ if (key === "loc" || key === "range" || key === "start" || key === "end") {
130
+ continue;
131
+ }
132
+ shiftNode(ctx, node[key], delta);
133
+ }
134
+ return node;
135
+ };
136
+
137
+ /** Placeholder emitted when a sub-parse fails; keeps ranges sane for consumers. */
138
+ const unparsedIdentifier = (ctx, start, end) => ({
139
+ type: "Identifier",
140
+ name: UNPARSED_IDENTIFIER_NAME,
141
+ start,
142
+ end,
143
+ loc: ctx.locBetween(start, end)
144
+ });
145
+
146
+ /**
147
+ * Run a sub-parse, converting any throw into a recorded error plus an
148
+ * `__astgen_unparsed` identifier. One malformed expression costs exactly that
149
+ * expression, never the file.
150
+ */
151
+ const guardedSubParse = (ctx, start, end, parseFn) => {
152
+ try {
153
+ return parseFn();
154
+ } catch (err) {
155
+ ctx.recordSubParseFailure(err?.message || String(err), start, end);
156
+ return unparsedIdentifier(ctx, start, end);
157
+ }
158
+ };
159
+
160
+ /**
161
+ * Rebuild every `loc` on a Babel-parsed tree with fresh position objects.
162
+ * Babel shares position objects between neighbouring nodes (`nodeA.loc.end ===
163
+ * nodeB.loc.start`), and the JSON writer's circular-reference guard drops a
164
+ * repeated object on its second appearance - leaving some nodes with a
165
+ * half-missing `loc` (this also affects plain `.js` output). Rebuilding from
166
+ * the node's own offsets makes the Svelte output fully self-consistent:
167
+ * `loc.start.index === start` and `loc.end.index === end` hold everywhere.
168
+ */
169
+ const relocTree = (ctx, node) => {
170
+ if (Array.isArray(node)) {
171
+ for (const child of node) {
172
+ relocTree(ctx, child);
173
+ }
174
+ return;
175
+ }
176
+ if (!node || typeof node !== "object") {
177
+ return;
178
+ }
179
+ if (typeof node.start === "number" && typeof node.end === "number") {
180
+ node.loc = ctx.locBetween(node.start, node.end);
181
+ }
182
+ for (const key of Object.keys(node)) {
183
+ if (key === "loc" || key === "range" || key === "start" || key === "end") {
184
+ continue;
185
+ }
186
+ relocTree(ctx, node[key]);
187
+ }
188
+ };
189
+
190
+ /** Sub-parse a standalone expression such as `count > 10` or `'<b>' + x`. */
191
+ const subExpr = (ctx, start, end) =>
192
+ guardedSubParse(ctx, start, end, () =>
193
+ shiftNode(
194
+ ctx,
195
+ parseExpression(ctx.slice(start, end), ctx.options),
196
+ start
197
+ )
198
+ );
199
+
200
+ /**
201
+ * Sub-parse a binding pattern such as `item`, `{ a, b }` or `[x, ...rest]`.
202
+ * Patterns are not standalone expressions, so the text is embedded as the
203
+ * parameter of a throwaway arrow function; the pattern starts at index 1 of
204
+ * that snippet, hence the `start - 1` shift.
205
+ */
206
+ const subPattern = (ctx, start, end) =>
207
+ guardedSubParse(ctx, start, end, () => {
208
+ const fn = babelParse(`(${ctx.slice(start, end)})=>0`, ctx.options);
209
+ return shiftNode(ctx, fn.program.body[0].expression.params[0], start - 1);
210
+ });
211
+
212
+
213
+ // ---------------------------------------------------------------------------
214
+ // Source-range helpers for synthesized element structure
215
+ // ---------------------------------------------------------------------------
216
+
217
+ /**
218
+ * End of an element's opening tag (index just past its `>`), derived from the
219
+ * source. Svelte reports the tag-name range and attribute ranges but not the
220
+ * tag boundary itself. Scanning for `>` from the last attribute's end is safe
221
+ * against quoted `>` characters because a quoted value belongs to an attribute
222
+ * node and therefore ends before the scan starts.
223
+ */
224
+ const openTagEnd = (ctx, node) => {
225
+ let from = nameLocEnd(node);
226
+ for (const attribute of node.attributes ?? []) {
227
+ if (attribute.end > from) {
228
+ from = attribute.end;
229
+ }
230
+ }
231
+ const gt = ctx.src.indexOf(">", from);
232
+ return gt === -1 ? node.end : gt + 1;
233
+ };
234
+
235
+ /**
236
+ * True for `<br/>` and for unclosed void elements such as `<br>` or
237
+ * `<img src=x>` whose node ends at the opening tag because they have no
238
+ * children and no closing tag.
239
+ */
240
+ const isSelfClosing = (ctx, node) => {
241
+ const end = openTagEnd(ctx, node);
242
+ return ctx.src[end - 2] === "/" || end >= node.end;
243
+ };
244
+
245
+ /** Range of the closing tag (`</div>`), or null for self-closing/void elements. */
246
+ const closeTagRange = (ctx, node) => {
247
+ const openEnd = openTagEnd(ctx, node);
248
+ if (isSelfClosing(ctx, node) || node.end <= openEnd) {
249
+ return null;
250
+ }
251
+ // An element with children ends exactly at its closing tag's `>`. Scanning
252
+ // backwards for `</` alone is not enough: the next sibling's closing tag can
253
+ // start precisely at this element's end, so each candidate is validated by
254
+ // requiring its `>` to land on node.end - 1.
255
+ let lt = ctx.src.lastIndexOf("</", node.end - 1);
256
+ while (lt >= openEnd) {
257
+ const gt = ctx.src.indexOf(">", lt);
258
+ if (gt === node.end - 1) {
259
+ return { start: lt, end: node.end };
260
+ }
261
+ lt = ctx.src.lastIndexOf("</", lt - 1);
262
+ }
263
+ return null;
264
+ };
265
+
266
+ // ---------------------------------------------------------------------------
267
+ // Names
268
+ // ---------------------------------------------------------------------------
269
+
270
+ /**
271
+ * Map a tag/attribute name onto Babel JSX name nodes:
272
+ * "div" -> JSXIdentifier
273
+ * "svelte:head" -> JSXNamespacedName(svelte, head)
274
+ * "Foo.Bar" -> JSXMemberExpression(Foo, Bar)
275
+ * Offsets come from the source so every part spans its real text; chen never
276
+ * visits these nodes, but keeping ranges honest costs nothing.
277
+ */
278
+ const jsxName = (ctx, name, start, end) => {
279
+ const identifier = (idName, s, e) => ({
280
+ type: "JSXIdentifier",
281
+ name: idName,
282
+ start: s,
283
+ end: e,
284
+ loc: ctx.locBetween(s, e)
285
+ });
286
+ if (name.includes(":")) {
287
+ const [namespaceName, localName] = name.split(":");
288
+ const colon = ctx.src.indexOf(":", start);
289
+ const namespaceEnd =
290
+ colon === -1 || colon >= end ? start + namespaceName.length : colon;
291
+ const localStart = namespaceEnd + 1;
292
+ return {
293
+ type: "JSXNamespacedName",
294
+ namespace: identifier(namespaceName, start, namespaceEnd),
295
+ name: identifier(localName, localStart, Math.max(localStart, end)),
296
+ start,
297
+ end,
298
+ loc: ctx.locBetween(start, end)
299
+ };
300
+ }
301
+ if (name.includes(".")) {
302
+ const lastDot = name.lastIndexOf(".");
303
+ const dot = ctx.src.lastIndexOf(".", end - 1);
304
+ const objectEnd = dot <= start || dot >= end ? start + lastDot : dot;
305
+ return {
306
+ type: "JSXMemberExpression",
307
+ object: jsxName(ctx, name.slice(0, lastDot), start, objectEnd),
308
+ property: identifier(name.slice(lastDot + 1), objectEnd + 1, end),
309
+ start,
310
+ end,
311
+ loc: ctx.locBetween(start, end)
312
+ };
313
+ }
314
+ return identifier(name, start, end);
315
+ };
316
+
317
+ // ---------------------------------------------------------------------------
318
+ // Attributes and directives
319
+ // ---------------------------------------------------------------------------
320
+
321
+ /**
322
+ * Map an attribute/directive value onto the `value` of a Babel JSXAttribute.
323
+ *
324
+ * Svelte shapes: `true` (boolean shorthand), a single node (ExpressionTag for
325
+ * `{expr}`), or an array of Text/ExpressionTag parts (mixed content such as
326
+ * class="a {b} c", or a plain quoted string which arrives as [Text]).
327
+ */
328
+ const mapAttributeValue = (ctx, value, valueStart, valueEnd) => {
329
+ if (value === true || value == null) {
330
+ return null;
331
+ }
332
+ if (!Array.isArray(value)) {
333
+ if (value.type === "ExpressionTag") {
334
+ return expressionTagContainer(ctx, value, value.expression);
335
+ }
336
+ if (value.type === "Text") {
337
+ return stringLiteralForText(ctx, value, value.start, value.end);
338
+ }
339
+ return subExpr(ctx, value.start, value.end);
340
+ }
341
+ if (value.length === 1 && value[0].type === "Text") {
342
+ return stringLiteralForText(ctx, value[0], valueStart, valueEnd);
343
+ }
344
+ if (value.length === 1 && value[0].type === "ExpressionTag") {
345
+ return expressionTagContainer(ctx, value[0], value[0].expression);
346
+ }
347
+ // Mixed text/interpolation: a JSXExpressionContainer over the whole (quoted)
348
+ // value wrapping a TemplateLiteral built from the parts - the shape a Babel
349
+ // parse of the equivalent JSX attribute would produce.
350
+ const [regionStart, regionEnd] = trimmedValueRange(ctx, valueStart, valueEnd);
351
+ const template = mixedValueTemplateLiteral(ctx, value);
352
+ return {
353
+ type: "JSXExpressionContainer",
354
+ start: regionStart,
355
+ end: regionEnd,
356
+ loc: ctx.locBetween(regionStart, regionEnd),
357
+ expression: template
358
+ };
359
+ };
360
+
361
+ /**
362
+ * The attribute value's own region: after the `=`, trimmed of surrounding
363
+ * whitespace, so a quoted value keeps both quotes (`class="a"` -> `"a"`) the
364
+ * way Babel's own attribute values do.
365
+ */
366
+ const trimmedValueRange = (ctx, valueStart, valueEnd) => {
367
+ let start = valueStart;
368
+ let end = valueEnd;
369
+ while (start < end && /\s/.test(ctx.src[start])) {
370
+ start++;
371
+ }
372
+ while (end > start && /\s/.test(ctx.src[end - 1])) {
373
+ end--;
374
+ }
375
+ return [start, end];
376
+ };
377
+
378
+ /** A plain quoted value as a StringLiteral spanning the trimmed value region. */
379
+ const stringLiteralForText = (ctx, text, regionStart, regionEnd) => {
380
+ const [start, end] = trimmedValueRange(ctx, regionStart, regionEnd);
381
+ return {
382
+ type: "StringLiteral",
383
+ value: text.data,
384
+ start,
385
+ end,
386
+ loc: ctx.locBetween(start, end)
387
+ };
388
+ };
389
+
390
+ /**
391
+ * Build the TemplateLiteral for a mixed text/expression attribute value. Babel
392
+ * requires `quasis.length === expressions.length + 1`, so empty zero-width
393
+ * quasis are inserted where two expressions are adjacent and at the head/tail
394
+ * when the value starts/ends with an expression.
395
+ */
396
+ const mixedValueTemplateLiteral = (ctx, parts) => {
397
+ const expressions = [];
398
+ const quasis = [];
399
+ for (const part of parts) {
400
+ if (part.type === "Text") {
401
+ if (quasis.length === expressions.length) {
402
+ // No quasi is pending (start of the value, or an expression was just
403
+ // pushed): this Text opens a new quasi.
404
+ quasis.push(templateElement(ctx, part.data, part.start, part.end, false));
405
+ } else if (part.data.length > 0) {
406
+ // Svelte can split text runs (e.g. around decoded entities); merge the
407
+ // run into the trailing quasi so the arity invariant holds.
408
+ const previous = quasis[quasis.length - 1];
409
+ previous.value = {
410
+ raw: previous.value.raw + part.data,
411
+ cooked: previous.value.cooked + part.data
412
+ };
413
+ previous.end = part.end;
414
+ previous.loc = ctx.locBetween(previous.start, part.end);
415
+ }
416
+ } else if (part.type === "ExpressionTag") {
417
+ if (quasis.length === expressions.length) {
418
+ // Two adjacent expressions (or a leading one): insert an empty
419
+ // separator quasi at this position.
420
+ quasis.push(templateElement(ctx, "", part.start, part.start, false));
421
+ }
422
+ expressions.push(subExpr(ctx, part.expression.start, part.expression.end));
423
+ }
424
+ }
425
+ if (quasis.length === expressions.length) {
426
+ // The value ends with an expression: close with an empty tail quasi. Its
427
+ // position - and the literal's overall end - is the last *part*'s end (an
428
+ // ExpressionTag's end includes its closing `}`); using the inner
429
+ // expression's end would truncate the literal by one character.
430
+ const at = parts[parts.length - 1].end;
431
+ quasis.push(templateElement(ctx, "", at, at, false));
432
+ }
433
+ quasis[quasis.length - 1].tail = true;
434
+ const start = quasis[0].start;
435
+ const end = parts[parts.length - 1].end;
436
+ return {
437
+ type: "TemplateLiteral",
438
+ quasis,
439
+ expressions,
440
+ start,
441
+ end,
442
+ loc: ctx.locBetween(start, end)
443
+ };
444
+ };
445
+
446
+ const templateElement = (ctx, raw, start, end, tail) => ({
447
+ type: "TemplateElement",
448
+ start,
449
+ end,
450
+ tail,
451
+ value: { raw, cooked: raw },
452
+ loc: ctx.locBetween(start, end)
453
+ });
454
+
455
+ /**
456
+ * A JSXExpressionContainer that spans a `{...}` tag's brace range and wraps the
457
+ * sub-parsed inner expression. `tag` supplies the outer range (which for
458
+ * shorthand attributes is narrower than the braces themselves); `expression`
459
+ * supplies the inner range.
460
+ */
461
+ const expressionTagContainer = (ctx, tag, expression) => {
462
+ const inner = subExpr(ctx, expression.start, expression.end);
463
+ return {
464
+ type: "JSXExpressionContainer",
465
+ start: tag.start,
466
+ end: tag.end,
467
+ loc: ctx.locBetween(tag.start, tag.end),
468
+ expression: inner
469
+ };
470
+ };
471
+
472
+ /** Container whose range is exactly the braces around [start, end). */
473
+ const bracedContainer = (ctx, expressionNode, start, end) => ({
474
+ type: "JSXExpressionContainer",
475
+ start,
476
+ end,
477
+ loc: ctx.locBetween(start, end),
478
+ expression: expressionNode
479
+ });
480
+
481
+ /** Zero-width node at `offset` - for invented identifiers and markers. */
482
+ const zeroWidth = (ctx, offset, fields) => ({
483
+ start: offset,
484
+ end: offset,
485
+ loc: ctx.locBetween(offset, offset),
486
+ ...fields
487
+ });
488
+
489
+ /**
490
+ * Directives (`on:click={h}`, `bind:value={q}`, ...) become JSXAttributes whose
491
+ * name is a JSXNamespacedName, which Babel's JSX parser produces natively for
492
+ * `on:click`-style names. Unlike the Vue masking path nothing is rewritten, so
493
+ * the attribute's [start, end) slices to the directive's exact source text -
494
+ * including modifiers such as `on:click|preventDefault`.
495
+ */
496
+ const mapDirective = (ctx, node) => {
497
+ const prefix = directivePrefix(ctx, node);
498
+ const nameStart = node.start;
499
+ const colon = ctx.src.indexOf(":", nameStart);
500
+ // The local name stops at the first modifier pipe, `=`, `{` or whitespace.
501
+ const afterColon = ctx.slice(colon + 1, node.end);
502
+ const stop = afterColon.search(/[\s={|]/);
503
+ const nameEnd =
504
+ stop === -1 ? colon + 1 + node.name.length : colon + 1 + stop;
505
+ const name = {
506
+ type: "JSXNamespacedName",
507
+ namespace: {
508
+ type: "JSXIdentifier",
509
+ name: prefix,
510
+ start: nameStart,
511
+ end: colon,
512
+ loc: ctx.locBetween(nameStart, colon)
513
+ },
514
+ name: {
515
+ type: "JSXIdentifier",
516
+ name: node.name,
517
+ start: colon + 1,
518
+ end: nameEnd,
519
+ loc: ctx.locBetween(colon + 1, nameEnd)
520
+ },
521
+ start: nameStart,
522
+ end: nameEnd,
523
+ loc: ctx.locBetween(nameStart, nameEnd)
524
+ };
525
+ return {
526
+ type: "JSXAttribute",
527
+ start: node.start,
528
+ end: node.end,
529
+ loc: ctx.locBetween(node.start, node.end),
530
+ svelteKind: node.type,
531
+ name,
532
+ value: directiveValue(ctx, node)
533
+ };
534
+ };
535
+
536
+ /**
537
+ * `transition:`/`in:`/`out:` share one Svelte node type; the actual keyword is
538
+ * only in the source text, so read it back rather than guessing.
539
+ */
540
+ const directivePrefix = (ctx, node) => {
541
+ if (node.type === "TransitionDirective") {
542
+ for (const prefix of TRANSITION_DIRECTIVE_PREFIXES) {
543
+ if (ctx.src.startsWith(prefix, node.start)) {
544
+ return prefix;
545
+ }
546
+ }
547
+ }
548
+ return {
549
+ OnDirective: "on",
550
+ BindDirective: "bind",
551
+ ClassDirective: "class",
552
+ StyleDirective: "style",
553
+ UseDirective: "use",
554
+ AnimateDirective: "animate",
555
+ LetDirective: "let"
556
+ }[node.type];
557
+ };
558
+
559
+ const directiveValue = (ctx, node) => {
560
+ if (node.type === "LetDirective") {
561
+ // `let:` values are binding patterns (`let:item`, `let:{a, b}`).
562
+ if (!node.expression) {
563
+ return null;
564
+ }
565
+ return bracedContainer(
566
+ ctx,
567
+ subPattern(ctx, node.expression.start, node.expression.end),
568
+ node.expression.start - 1,
569
+ node.expression.end + 1
570
+ );
571
+ }
572
+ if (node.type === "StyleDirective") {
573
+ // StyleDirective keeps its payload in `value`, with the same shapes an
574
+ // Attribute value can have.
575
+ return mapAttributeValue(ctx, node.value, nameLocEnd(node) + 1, node.end);
576
+ }
577
+ if (!node.expression) {
578
+ return null;
579
+ }
580
+ return bracedContainer(
581
+ ctx,
582
+ subExpr(ctx, node.expression.start, node.expression.end),
583
+ node.expression.start - 1,
584
+ node.expression.end + 1
585
+ );
586
+ };
587
+
588
+ /** `Attribute` -> JSXAttribute; `SpreadAttribute` -> JSXSpreadAttribute. */
589
+ const mapAttribute = (ctx, node) => {
590
+ if (node.type === "SpreadAttribute") {
591
+ return {
592
+ type: "JSXSpreadAttribute",
593
+ start: node.start,
594
+ end: node.end,
595
+ loc: ctx.locBetween(node.start, node.end),
596
+ svelteKind: "SpreadAttribute",
597
+ argument: subExpr(ctx, node.expression.start, node.expression.end)
598
+ };
599
+ }
600
+ if (node.type === "AttachTag") {
601
+ // `{@attach fn}` rides along in the attributes array in Svelte's AST.
602
+ const name = zeroWidth(ctx, node.start, {
603
+ type: "JSXIdentifier",
604
+ name: "attach"
605
+ });
606
+ return {
607
+ type: "JSXAttribute",
608
+ start: node.start,
609
+ end: node.end,
610
+ loc: ctx.locBetween(node.start, node.end),
611
+ svelteKind: "AttachTag",
612
+ name,
613
+ value: node.expression
614
+ ? bracedContainer(
615
+ ctx,
616
+ subExpr(ctx, node.expression.start, node.expression.end),
617
+ node.start,
618
+ node.end
619
+ )
620
+ : null
621
+ };
622
+ }
623
+ if (node.type === "Attribute") {
624
+ return {
625
+ type: "JSXAttribute",
626
+ start: node.start,
627
+ end: node.end,
628
+ loc: ctx.locBetween(node.start, node.end),
629
+ svelteKind: "Attribute",
630
+ name: jsxName(ctx, node.name, nameLocStart(node), nameLocEnd(node)),
631
+ value: mapAttributeValue(ctx, node.value, nameLocEnd(node) + 1, node.end)
632
+ };
633
+ }
634
+ return mapDirective(ctx, node);
635
+ };
636
+
637
+ // ---------------------------------------------------------------------------
638
+ // Elements
639
+ // ---------------------------------------------------------------------------
640
+
641
+ /**
642
+ * Every element-like Svelte node (`RegularElement`, `Component`, `SlotElement`,
643
+ * `TitleElement`, `SvelteHead`, `SvelteWindow`, `SvelteBody`,
644
+ * `SvelteDocument`, `SvelteFragment`, `SvelteSelf`, `SvelteBoundary`,
645
+ * `SvelteOptions`, `SvelteElement`, `SvelteComponent`) maps to a JSXElement.
646
+ */
647
+ const mapElement = (ctx, node) => {
648
+ const attributes = node.attributes?.map((a) => mapAttribute(ctx, a)) ?? [];
649
+ // `<svelte:element this={...}>` and `<svelte:component this={...}>` keep their
650
+ // dynamic tag outside `attributes`; surface it as a `this` attribute so the
651
+ // expression is not lost.
652
+ const dynamicThis = node.type === "SvelteElement" ? node.tag : node.type === "SvelteComponent" ? node.expression : null;
653
+ if (dynamicThis) {
654
+ attributes.unshift(thisAttribute(ctx, node, dynamicThis));
655
+ }
656
+ const openEnd = openTagEnd(ctx, node);
657
+ const closeRange = closeTagRange(ctx, node);
658
+ const children = (node.fragment?.nodes ?? [])
659
+ .map((child) => mapChild(ctx, child))
660
+ .flat()
661
+ .filter(Boolean);
662
+ return {
663
+ type: "JSXElement",
664
+ start: node.start,
665
+ end: node.end,
666
+ loc: ctx.locBetween(node.start, node.end),
667
+ svelteKind: node.type,
668
+ svelteName: node.name,
669
+ openingElement: {
670
+ type: "JSXOpeningElement",
671
+ start: node.start,
672
+ end: openEnd,
673
+ loc: ctx.locBetween(node.start, openEnd),
674
+ svelteKind: node.type,
675
+ svelteName: node.name,
676
+ name: jsxName(ctx, node.name, nameLocStart(node), nameLocEnd(node)),
677
+ attributes,
678
+ selfClosing: isSelfClosing(ctx, node)
679
+ },
680
+ closingElement: closeRange
681
+ ? {
682
+ type: "JSXClosingElement",
683
+ start: closeRange.start,
684
+ end: closeRange.end,
685
+ loc: ctx.locBetween(closeRange.start, closeRange.end),
686
+ svelteKind: node.type,
687
+ svelteName: node.name,
688
+ name: jsxName(ctx, node.name, closeRange.start + 2, closeRange.end - 1)
689
+ }
690
+ : null,
691
+ children
692
+ };
693
+ };
694
+
695
+ /** Synthesized `this={expr}` attribute for svelte:element / svelte:component. */
696
+ const thisAttribute = (ctx, node, expression) => {
697
+ const openEnd = openTagEnd(ctx, node);
698
+ const nameStart = nameLocEnd(node);
699
+ const thisIndex = ctx.slice(nameStart, openEnd).indexOf("this");
700
+ const attrStart = thisIndex === -1 ? nameStart : nameStart + thisIndex;
701
+ const attrEnd = expression.end + 1;
702
+ return {
703
+ type: "JSXAttribute",
704
+ start: attrStart,
705
+ end: attrEnd,
706
+ loc: ctx.locBetween(attrStart, attrEnd),
707
+ svelteKind: node.type,
708
+ name: {
709
+ type: "JSXIdentifier",
710
+ name: "this",
711
+ start: attrStart,
712
+ end: attrStart + (thisIndex === -1 ? 0 : 4),
713
+ loc: ctx.locBetween(attrStart, attrStart + (thisIndex === -1 ? 0 : 4))
714
+ },
715
+ value: bracedContainer(
716
+ ctx,
717
+ subExpr(ctx, expression.start, expression.end),
718
+ expression.start - 1,
719
+ expression.end + 1
720
+ )
721
+ };
722
+ };
723
+
724
+ // ---------------------------------------------------------------------------
725
+ // Template children
726
+ // ---------------------------------------------------------------------------
727
+
728
+ /**
729
+ * Map one template child onto Babel JSX. Returns a node, an array of nodes
730
+ * (blocks that emit siblings), or null for dropped content (comments).
731
+ */
732
+ const mapChild = (ctx, node) => {
733
+ switch (node.type) {
734
+ case "Text":
735
+ return {
736
+ type: "JSXText",
737
+ value: node.data,
738
+ start: node.start,
739
+ end: node.end,
740
+ loc: ctx.locBetween(node.start, node.end)
741
+ };
742
+ case "Comment":
743
+ // HTML comments carry no code semantics; dropped by design.
744
+ return null;
745
+ case "ExpressionTag":
746
+ return { ...expressionTagContainer(ctx, node, node.expression), svelteKind: "ExpressionTag" };
747
+ case "HtmlTag":
748
+ return {
749
+ ...expressionTagContainer(ctx, node, node.expression),
750
+ svelteKind: "HtmlTag"
751
+ };
752
+ case "RenderTag":
753
+ return {
754
+ type: "JSXExpressionContainer",
755
+ start: node.start,
756
+ end: node.end,
757
+ loc: ctx.locBetween(node.start, node.end),
758
+ svelteKind: "RenderTag",
759
+ expression: node.expression
760
+ ? subExpr(ctx, node.expression.start, node.expression.end)
761
+ : zeroWidth(ctx, node.start, { type: "JSXEmptyExpression" })
762
+ };
763
+ case "DebugTag":
764
+ return debugTagContainer(ctx, node);
765
+ case "ConstTag":
766
+ return constTagContainer(ctx, node);
767
+ case "IfBlock":
768
+ return ifBlockContainer(ctx, node);
769
+ case "EachBlock":
770
+ return eachBlockNodes(ctx, node);
771
+ case "AwaitBlock":
772
+ return awaitBlockNodes(ctx, node);
773
+ case "KeyBlock":
774
+ return keyBlockNodes(ctx, node);
775
+ case "SnippetBlock":
776
+ return snippetBlockContainer(ctx, node);
777
+ default:
778
+ // Remaining child types are the element-like nodes (and any future
779
+ // element kind, which still has a name and a fragment).
780
+ if (node.fragment || node.name_loc) {
781
+ return mapElement(ctx, node);
782
+ }
783
+ return null;
784
+ }
785
+ };
786
+
787
+ /**
788
+ * `{@debug a, b}` -> a container over a SequenceExpression of the identifiers
789
+ * (or the bare identifier when there is only one).
790
+ */
791
+ const debugTagContainer = (ctx, node) => {
792
+ const identifiers = node.identifiers ?? [];
793
+ const start = identifiers[0]?.start ?? node.start;
794
+ const end = identifiers[identifiers.length - 1]?.end ?? node.end;
795
+ const parsed = identifiers.map((i) => subExpr(ctx, i.start, i.end));
796
+ const expression =
797
+ parsed.length > 1
798
+ ? {
799
+ type: "SequenceExpression",
800
+ expressions: parsed,
801
+ start,
802
+ end,
803
+ loc: ctx.locBetween(start, end)
804
+ }
805
+ : parsed[0] ?? zeroWidth(ctx, node.start, { type: "JSXEmptyExpression" });
806
+ return {
807
+ type: "JSXExpressionContainer",
808
+ start: node.start,
809
+ end: node.end,
810
+ loc: ctx.locBetween(node.start, node.end),
811
+ svelteKind: "DebugTag",
812
+ expression
813
+ };
814
+ };
815
+
816
+ /**
817
+ * `{@const label = expr}` -> a container over `pattern = expr`. A declaration
818
+ * is not legal in a JSX child position, so the binding is modelled as an
819
+ * assignment; the trade-off (the binding looks like an implicit global
820
+ * downstream) is documented in docs/ASTGEN.md.
821
+ */
822
+ const constTagContainer = (ctx, node) => {
823
+ const declaration = node.declaration;
824
+ const declarator = declaration?.declarations?.[0];
825
+ if (!declarator) {
826
+ return null;
827
+ }
828
+ return {
829
+ type: "JSXExpressionContainer",
830
+ start: node.start,
831
+ end: node.end,
832
+ loc: ctx.locBetween(node.start, node.end),
833
+ svelteKind: "ConstTag",
834
+ expression: {
835
+ type: "AssignmentExpression",
836
+ operator: "=",
837
+ start: declaration.start,
838
+ end: declaration.end,
839
+ loc: ctx.locBetween(declaration.start, declaration.end),
840
+ left: subPattern(ctx, declarator.id.start, declarator.id.end),
841
+ right: subExpr(ctx, declarator.init.start, declarator.init.end)
842
+ }
843
+ };
844
+ };
845
+
846
+ /**
847
+ * `{#if test}A{:else if t2}B{:else}C{/if}` -> a container over a chain of
848
+ * ConditionalExpressions. An `{:else if}` alternate holds exactly one nested
849
+ * IfBlock; that block's conditional is spliced in directly so the chain is a
850
+ * proper `a ? ... : b ? ... : ...` nesting rather than a fragment wrapper.
851
+ */
852
+ const ifBlockContainer = (ctx, node) => ({
853
+ type: "JSXExpressionContainer",
854
+ start: node.start,
855
+ end: node.end,
856
+ loc: ctx.locBetween(node.start, node.end),
857
+ svelteKind: "IfBlock",
858
+ expression: ifBlockConditional(ctx, node)
859
+ });
860
+
861
+ const ifBlockConditional = (ctx, node) => {
862
+ let alternate;
863
+ const alternateNodes = node.alternate?.nodes ?? [];
864
+ const nestedElseIf =
865
+ alternateNodes.length === 1 &&
866
+ alternateNodes[0].type === "IfBlock" &&
867
+ alternateNodes[0].elseif === true;
868
+ if (!node.alternate) {
869
+ alternate = zeroWidth(ctx, node.end, { type: "NullLiteral" });
870
+ } else if (nestedElseIf) {
871
+ alternate = ifBlockConditional(ctx, alternateNodes[0]);
872
+ } else {
873
+ alternate = fragmentToJsx(ctx, node.alternate, node.end);
874
+ }
875
+ return {
876
+ type: "ConditionalExpression",
877
+ start: node.test.start,
878
+ end: node.end,
879
+ loc: ctx.locBetween(node.test.start, node.end),
880
+ test: subExpr(ctx, node.test.start, node.test.end),
881
+ consequent: fragmentToJsx(ctx, node.consequent, node.start),
882
+ alternate
883
+ };
884
+ };
885
+
886
+ /**
887
+ * `{#each list as item, i (key)}body{:else}fallback{/each}` ->
888
+ * a `list.map((item, i) => body)` container. The `{:else}` fallback is
889
+ * deliberately emitted as a second sibling fragment rather than folded into a
890
+ * conditional: re-using `list` in a conditional test would sub-parse the same
891
+ * expression twice and double-count its identifiers downstream.
892
+ *
893
+ * `node.index` is a plain string in Svelte's AST; its source range is located
894
+ * by searching between the context pattern and the key/group end.
895
+ */
896
+ const eachBlockNodes = (ctx, node) => {
897
+ const mapCall = {
898
+ type: "CallExpression",
899
+ start: node.expression.start,
900
+ end: node.end,
901
+ loc: ctx.locBetween(node.expression.start, node.end),
902
+ callee: {
903
+ type: "MemberExpression",
904
+ computed: false,
905
+ object: subExpr(ctx, node.expression.start, node.expression.end),
906
+ property: zeroWidth(ctx, node.expression.end, {
907
+ type: "Identifier",
908
+ name: "map"
909
+ }),
910
+ start: node.expression.start,
911
+ end: node.expression.end,
912
+ loc: ctx.locBetween(node.expression.start, node.expression.end)
913
+ },
914
+ arguments: [eachArrowFunction(ctx, node)]
915
+ };
916
+ const container = {
917
+ type: "JSXExpressionContainer",
918
+ start: node.start,
919
+ end: node.end,
920
+ loc: ctx.locBetween(node.start, node.end),
921
+ svelteKind: "EachBlock",
922
+ expression: mapCall
923
+ };
924
+ return node.fallback ? [container, fragmentToJsx(ctx, node.fallback, node.end)] : container;
925
+ };
926
+
927
+ const eachArrowFunction = (ctx, node) => {
928
+ const params = [subPattern(ctx, node.context.start, node.context.end)];
929
+ if (node.index) {
930
+ params.push(eachIndexIdentifier(ctx, node));
931
+ }
932
+ const body = fragmentToJsx(ctx, node.body, node.start);
933
+ // The `(key)` expression becomes the first child of the arrow's fragment so
934
+ // its identifiers stay reachable without polluting the map() signature.
935
+ if (node.key) {
936
+ body.children.unshift(
937
+ bracedContainer(
938
+ ctx,
939
+ subExpr(ctx, node.key.start, node.key.end),
940
+ node.key.start - 1,
941
+ node.key.end + 1
942
+ )
943
+ );
944
+ }
945
+ return {
946
+ type: "ArrowFunctionExpression",
947
+ start: node.context.start,
948
+ end: node.end,
949
+ loc: ctx.locBetween(node.context.start, node.end),
950
+ id: null,
951
+ async: false,
952
+ generator: false,
953
+ params,
954
+ body,
955
+ expression: false
956
+ };
957
+ };
958
+
959
+ const eachIndexIdentifier = (ctx, node) => {
960
+ const searchEnd = node.key ? node.key.start : node.end;
961
+ const region = ctx.slice(node.context.end, searchEnd);
962
+ const match = new RegExp(
963
+ `[,\\s]${node.index.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s({]|[,\\s]${node.index.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`
964
+ ).exec(region);
965
+ if (match) {
966
+ const start = node.context.end + match.index + 1;
967
+ const end = start + node.index.length;
968
+ return {
969
+ type: "Identifier",
970
+ name: node.index,
971
+ start,
972
+ end,
973
+ loc: ctx.locBetween(start, end)
974
+ };
975
+ }
976
+ return zeroWidth(ctx, node.context.end, {
977
+ type: "Identifier",
978
+ name: node.index
979
+ });
980
+ };
981
+
982
+ /**
983
+ * `{#await expr}pending{:then value}then{:catch error}catch{/await}` ->
984
+ * the pending children emitted as plain preceding siblings (they are static
985
+ * DOM), followed by a container over `expr.then(onFulfilled, onRejected?)`.
986
+ * A `NullLiteral` keeps the rejection handler in argument position when only
987
+ * `{:catch}` is present.
988
+ */
989
+ const awaitBlockNodes = (ctx, node) => {
990
+ const pendingChildren = (node.pending?.nodes ?? [])
991
+ .map((child) => mapChild(ctx, child))
992
+ .flat()
993
+ .filter(Boolean);
994
+ const argumentsList = [];
995
+ if (node.then) {
996
+ argumentsList.push(awaitHandler(ctx, node.value, node.then, node));
997
+ } else if (node.catch) {
998
+ argumentsList.push(zeroWidth(ctx, node.end, { type: "NullLiteral" }));
999
+ }
1000
+ if (node.catch) {
1001
+ argumentsList.push(awaitHandler(ctx, node.error, node.catch, node));
1002
+ }
1003
+ const call = {
1004
+ type: "CallExpression",
1005
+ start: node.expression.start,
1006
+ end: node.end,
1007
+ loc: ctx.locBetween(node.expression.start, node.end),
1008
+ callee: {
1009
+ type: "MemberExpression",
1010
+ computed: false,
1011
+ object: subExpr(ctx, node.expression.start, node.expression.end),
1012
+ property: zeroWidth(ctx, node.expression.end, {
1013
+ type: "Identifier",
1014
+ name: "then"
1015
+ }),
1016
+ start: node.expression.start,
1017
+ end: node.expression.end,
1018
+ loc: ctx.locBetween(node.expression.start, node.expression.end)
1019
+ },
1020
+ arguments: argumentsList
1021
+ };
1022
+ const container = {
1023
+ type: "JSXExpressionContainer",
1024
+ start: node.start,
1025
+ end: node.end,
1026
+ loc: ctx.locBetween(node.start, node.end),
1027
+ svelteKind: "AwaitBlock",
1028
+ expression: call
1029
+ };
1030
+ return [...pendingChildren, container];
1031
+ };
1032
+
1033
+ const awaitHandler = (ctx, pattern, fragment, owner) => ({
1034
+ type: "ArrowFunctionExpression",
1035
+ start: pattern?.start ?? fragment.nodes[0]?.start ?? owner.start,
1036
+ end: fragment.end ?? owner.end,
1037
+ loc: ctx.locBetween(
1038
+ pattern?.start ?? fragment.nodes[0]?.start ?? owner.start,
1039
+ fragment.end ?? owner.end
1040
+ ),
1041
+ id: null,
1042
+ async: false,
1043
+ generator: false,
1044
+ params: pattern ? [subPattern(ctx, pattern.start, pattern.end)] : [],
1045
+ body: fragmentToJsx(ctx, fragment, owner.start),
1046
+ expression: false
1047
+ });
1048
+
1049
+ /**
1050
+ * `{#key expr}fragment{/key}` -> the key expression container followed by the
1051
+ * fragment as a sibling, mirroring how the each-block fallback is emitted.
1052
+ */
1053
+ const keyBlockNodes = (ctx, node) => [
1054
+ bracedContainer(
1055
+ ctx,
1056
+ subExpr(ctx, node.expression.start, node.expression.end),
1057
+ node.expression.start - 1,
1058
+ node.expression.end + 1
1059
+ ),
1060
+ fragmentToJsx(ctx, node.fragment, node.start)
1061
+ ];
1062
+
1063
+ /**
1064
+ * `{#snippet name(params)}body{/snippet}` -> an assignment of an arrow function
1065
+ * to the snippet's name, so the name binding and the closure body are both
1066
+ * visible downstream.
1067
+ */
1068
+ const snippetBlockContainer = (ctx, node) => ({
1069
+ type: "JSXExpressionContainer",
1070
+ start: node.start,
1071
+ end: node.end,
1072
+ loc: ctx.locBetween(node.start, node.end),
1073
+ svelteKind: "SnippetBlock",
1074
+ svelteName: node.expression?.name,
1075
+ expression: {
1076
+ type: "AssignmentExpression",
1077
+ operator: "=",
1078
+ start: node.expression.start,
1079
+ end: node.end,
1080
+ loc: ctx.locBetween(node.expression.start, node.end),
1081
+ left: {
1082
+ type: "Identifier",
1083
+ name: node.expression.name,
1084
+ start: node.expression.start,
1085
+ end: node.expression.end,
1086
+ loc: ctx.locBetween(node.expression.start, node.expression.end)
1087
+ },
1088
+ right: {
1089
+ type: "ArrowFunctionExpression",
1090
+ start: node.expression.start,
1091
+ end: node.end,
1092
+ loc: ctx.locBetween(node.expression.start, node.end),
1093
+ id: null,
1094
+ async: false,
1095
+ generator: false,
1096
+ params: (node.parameters ?? []).map((p) =>
1097
+ subPattern(ctx, p.start, p.end)
1098
+ ),
1099
+ body: fragmentToJsx(ctx, node.body, node.start),
1100
+ expression: false
1101
+ }
1102
+ }
1103
+ });
1104
+
1105
+ /**
1106
+ * A Svelte fragment (a list of sibling template nodes) becomes a JSXFragment
1107
+ * spanning its first..last mapped child. Empty fragments get a zero-width
1108
+ * range at `fallbackOffset` (the enclosing block's start/end) so consumers
1109
+ * never see an unset position. Takes already-mapped children so a fragment is
1110
+ * never mapped twice (sub-parses have side effects on the error list).
1111
+ */
1112
+ const jsxFragmentFromChildren = (ctx, children, fallbackOffset) => {
1113
+ const start = children.length
1114
+ ? children[0].start
1115
+ : (fallbackOffset ?? 0);
1116
+ const end = children.length
1117
+ ? children[children.length - 1].end
1118
+ : (fallbackOffset ?? 0);
1119
+ return {
1120
+ type: "JSXFragment",
1121
+ start,
1122
+ end,
1123
+ loc: ctx.locBetween(start, end),
1124
+ svelteKind: "Fragment",
1125
+ openingFragment: {
1126
+ type: "JSXOpeningFragment",
1127
+ start,
1128
+ end: start,
1129
+ loc: ctx.locBetween(start, start),
1130
+ svelteKind: "Fragment"
1131
+ },
1132
+ closingFragment: {
1133
+ type: "JSXClosingFragment",
1134
+ start: end,
1135
+ end,
1136
+ loc: ctx.locBetween(end, end),
1137
+ svelteKind: "Fragment"
1138
+ },
1139
+ children
1140
+ };
1141
+ };
1142
+
1143
+ const fragmentToJsx = (ctx, fragment, fallbackOffset) =>
1144
+ jsxFragmentFromChildren(
1145
+ ctx,
1146
+ (fragment?.nodes ?? [])
1147
+ .map((child) => mapChild(ctx, child))
1148
+ .flat()
1149
+ .filter(Boolean),
1150
+ fallbackOffset
1151
+ );
1152
+
1153
+ // ---------------------------------------------------------------------------
1154
+ // File assembly
1155
+ // ---------------------------------------------------------------------------
1156
+
1157
+ /**
1158
+ * Build the masked buffer for script parsing: a same-length copy of the file
1159
+ * where only the script bodies keep their bytes. Newlines survive so line
1160
+ * numbers are preserved for every other region.
1161
+ */
1162
+ const maskedScriptBuffer = (root, src) => {
1163
+ const buffer = src.replace(/[^\r\n]/g, " ").split("");
1164
+ for (const script of [root.instance, root.module].filter(Boolean)) {
1165
+ const content = script.content;
1166
+ for (let i = content.start; i < content.end; i++) {
1167
+ buffer[i] = src[i];
1168
+ }
1169
+ }
1170
+ return buffer.join("");
1171
+ };
1172
+
1173
+ /**
1174
+ * Parse a `.svelte` source into a Babel `File` AST with absolute offsets.
1175
+ * Throws if `svelte/compiler` cannot segment the file (the caller falls back
1176
+ * to the legacy masking parser) or if the script buffer cannot be parsed.
1177
+ *
1178
+ * @param {string} file absolute path, used for sourceFilename
1179
+ * @param {string} src file content
1180
+ * @param {object} options Babel parser options shared with astgen.js
1181
+ */
1182
+ const assembleFile = (ctx, src, body) => ({
1183
+ type: "File",
1184
+ start: 0,
1185
+ end: src.length,
1186
+ loc: { start: ctx.posOf(0), end: ctx.posOf(src.length) },
1187
+ errors: ctx.errors,
1188
+ comments: [],
1189
+ program: {
1190
+ type: "Program",
1191
+ start: 0,
1192
+ end: src.length,
1193
+ loc: { start: ctx.posOf(0), end: ctx.posOf(src.length) },
1194
+ sourceType: "module",
1195
+ interpreter: null,
1196
+ directives: [],
1197
+ body
1198
+ }
1199
+ });
1200
+
1201
+ export const parseSvelteFile = (file, src, options) => {
1202
+ const root = svelteCompilerParse(src, {
1203
+ modern: true,
1204
+ filename: file
1205
+ });
1206
+ const babelOptions = { ...options, sourceFilename: file };
1207
+ const errors = [];
1208
+ const ctx = new SvelteParseContext(file, src, babelOptions, errors);
1209
+
1210
+ // Step A: script statements via one Babel parse of the masked buffer. Both
1211
+ // the instance and module scripts land in the same Program, flattened; the
1212
+ // distinction is lost and documented.
1213
+ const scriptFile = babelParse(maskedScriptBuffer(root, src), babelOptions);
1214
+ if (Array.isArray(scriptFile.errors)) {
1215
+ errors.push(...scriptFile.errors);
1216
+ }
1217
+ const scriptStatements = scriptFile.program.body;
1218
+ relocTree(ctx, scriptStatements);
1219
+
1220
+ // Step B: the template as one JSXFragment expression statement appended
1221
+ // after the scripts; body is sorted by start because `<script>` may legally
1222
+ // follow the markup. A template with no mapped content (or whitespace text
1223
+ // only) contributes nothing.
1224
+ const templateChildren = (root.fragment?.nodes ?? [])
1225
+ .map((child) => mapChild(ctx, child))
1226
+ .flat()
1227
+ .filter(Boolean);
1228
+ const hasTemplateContent = templateChildren.some(
1229
+ (child) => !(child.type === "JSXText" && child.value.trim() === "")
1230
+ );
1231
+ // Script statements keep source order among themselves; the template statement is
1232
+ // always appended last, never sorted in by offset.
1233
+ //
1234
+ // A component's markup renders after its instance script has run - Svelte hoists the
1235
+ // script regardless of where the `<script>` tag sits textually - so "template last" is
1236
+ // the execution order. Sorting by offset instead was actively wrong: the root fragment's
1237
+ // first child is whatever text precedes `<script>`, so a single leading newline gave the
1238
+ // template statement start=0 and placed it ahead of every script statement. Downstream
1239
+ // that inverts the CFG (method entry -> template -> script), which means no script-side
1240
+ // definition reaches a template use and every template dataflow query silently returns
1241
+ // nothing.
1242
+ const body = [...scriptStatements].sort((a, b) => a.start - b.start);
1243
+ if (hasTemplateContent) {
1244
+ const start = templateChildren[0].start;
1245
+ const end = templateChildren[templateChildren.length - 1].end;
1246
+ body.push({
1247
+ type: "ExpressionStatement",
1248
+ start,
1249
+ end,
1250
+ loc: ctx.locBetween(start, end),
1251
+ expression: jsxFragmentFromChildren(ctx, templateChildren, 0)
1252
+ });
1253
+ }
1254
+
1255
+ return assembleFile(ctx, src, body);
1256
+ };
1257
+
1258
+ /**
1259
+ * Fallback for files `svelte/compiler` rejects outright: parse only the script
1260
+ * blocks, over a position-preserving masked buffer (every byte outside the
1261
+ * `<script>` contents blanked to a space, newlines kept - the same masking
1262
+ * astgen.js applies to build virtual type sources). Because nothing moves, the
1263
+ * statement offsets are absolute byte positions into the original file, so a
1264
+ * scanner keeps correct line numbers for the script even though the template
1265
+ * is unrecoverable. The whole-file failure is recorded on `File.errors`.
1266
+ *
1267
+ * @param {string} file absolute path, used for sourceFilename
1268
+ * @param {string} src original file content (for loc computation)
1269
+ * @param {string} maskedSource same-length buffer with script contents verbatim
1270
+ * @param {object} options Babel parser options shared with astgen.js
1271
+ * @param {string} parseErrorMessage the svelte/compiler failure to record
1272
+ */
1273
+ export const parseSvelteScriptBuffer = (
1274
+ file,
1275
+ src,
1276
+ maskedSource,
1277
+ options,
1278
+ parseErrorMessage
1279
+ ) => {
1280
+ const babelOptions = { ...options, sourceFilename: file };
1281
+ const errors = [];
1282
+ const ctx = new SvelteParseContext(file, src, babelOptions, errors);
1283
+ const scriptFile = babelParse(maskedSource, babelOptions);
1284
+ if (Array.isArray(scriptFile.errors)) {
1285
+ errors.push(...scriptFile.errors);
1286
+ }
1287
+ errors.push({ svelteParse: true, message: parseErrorMessage });
1288
+ const statements = scriptFile.program.body;
1289
+ relocTree(ctx, statements);
1290
+ return assembleFile(ctx, src, statements);
1291
+ };