@dogsbay/format-vitepress 0.2.0-beta.101

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.
@@ -0,0 +1,845 @@
1
+ /**
2
+ * A tolerant, execution-free evaluator for the literal subset of a
3
+ * TypeScript/JavaScript module.
4
+ *
5
+ * Why this exists: VitePress keeps the sidebar INSIDE `.vitepress/config.ts`,
6
+ * so — unlike Docusaurus, whose sidebars live in their own data module — we
7
+ * cannot punt nav extraction to a separate file. And a real VitePress config
8
+ * imports theme packages, Shiki transformers and Vite plugins, so *executing*
9
+ * it needs the source repo's `node_modules` installed and runs arbitrary code
10
+ * from that repo. (See `format-docusaurus/src/config.ts` for the same call.)
11
+ *
12
+ * So we read it instead. This module understands:
13
+ *
14
+ * - object / array / string / number / boolean / null literals
15
+ * - template literals with no `${}` interpolation
16
+ * - `...spread` of anything else this module can evaluate
17
+ * - module-level `const X = <literal>` identifier references
18
+ * - zero-arg calls to module-level `function f() { return <literal> }` and
19
+ * `const f = () => <literal>` (hono's `sidebar: { '/': sidebars() }`)
20
+ * - wrapper calls with a single literal argument (`defineConfig({…})`)
21
+ * - member access into an already-evaluated object (`shared.themeConfig`)
22
+ * - TS noise: type annotations, `as const`, `as T`, `satisfies T`, generics
23
+ *
24
+ * Anything else evaluates to the `UNRESOLVED` sentinel — never to a plausible
25
+ * wrong value and never to a silent omission. Callers report what they could
26
+ * not read (`EvalResult.unresolved`), because a sidebar that quietly lost a
27
+ * section looks exactly like a sidebar that never had one.
28
+ *
29
+ * The parser never desyncs: an expression it does not understand is skipped
30
+ * by balancing delimiters, so the surrounding object continues to parse.
31
+ */
32
+ /** Sentinel for an expression this evaluator cannot read. */
33
+ export const UNRESOLVED = Symbol("vitepress.unresolved");
34
+ const IDENT_START = /[A-Za-z_$]/;
35
+ const IDENT_PART = /[A-Za-z0-9_$]/;
36
+ /**
37
+ * Positions after which a `/` begins a regex literal rather than division.
38
+ * Without this, `/['"]/ ` would open a phantom string and swallow the rest of
39
+ * the file.
40
+ */
41
+ const REGEX_PRECEDERS = new Set([
42
+ "(", ",", "=", ":", "[", "!", "&", "|", "?", "{", "}", ";", "=>", "+", "-",
43
+ "*", "%", "<", ">", "===", "!==", "==", "!=", "&&", "||", "??",
44
+ ]);
45
+ const REGEX_PRECEDING_KEYWORDS = new Set([
46
+ "return", "typeof", "instanceof", "in", "of", "new", "delete", "void",
47
+ "case", "do", "else", "yield", "await",
48
+ ]);
49
+ function tokenize(src) {
50
+ const toks = [];
51
+ let i = 0;
52
+ let line = 1;
53
+ const n = src.length;
54
+ const prevSignificant = () => toks[toks.length - 1];
55
+ while (i < n) {
56
+ const c = src[i];
57
+ if (c === "\n") {
58
+ line++;
59
+ i++;
60
+ continue;
61
+ }
62
+ if (c === " " || c === "\t" || c === "\r") {
63
+ i++;
64
+ continue;
65
+ }
66
+ // Comments
67
+ if (c === "/" && src[i + 1] === "/") {
68
+ while (i < n && src[i] !== "\n")
69
+ i++;
70
+ continue;
71
+ }
72
+ if (c === "/" && src[i + 1] === "*") {
73
+ i += 2;
74
+ while (i < n && !(src[i] === "*" && src[i + 1] === "/")) {
75
+ if (src[i] === "\n")
76
+ line++;
77
+ i++;
78
+ }
79
+ i += 2;
80
+ continue;
81
+ }
82
+ // Regex literal (only where a value may start)
83
+ if (c === "/") {
84
+ const p = prevSignificant();
85
+ const isRegexPos = !p ||
86
+ (p.kind === "punct" && REGEX_PRECEDERS.has(p.text)) ||
87
+ (p.kind === "ident" && REGEX_PRECEDING_KEYWORDS.has(p.text));
88
+ if (isRegexPos) {
89
+ const start = i;
90
+ i++;
91
+ let inClass = false;
92
+ while (i < n) {
93
+ const d = src[i];
94
+ if (d === "\\") {
95
+ i += 2;
96
+ continue;
97
+ }
98
+ if (d === "[")
99
+ inClass = true;
100
+ else if (d === "]")
101
+ inClass = false;
102
+ else if (d === "/" && !inClass)
103
+ break;
104
+ else if (d === "\n")
105
+ break; // unterminated — bail rather than run away
106
+ i++;
107
+ }
108
+ i++; // closing slash
109
+ while (i < n && /[gimsuyd]/.test(src[i]))
110
+ i++;
111
+ toks.push({ kind: "punct", text: "/regex/", start, line });
112
+ continue;
113
+ }
114
+ }
115
+ // Strings
116
+ if (c === '"' || c === "'") {
117
+ const start = i;
118
+ const quote = c;
119
+ i++;
120
+ let out = "";
121
+ while (i < n && src[i] !== quote) {
122
+ if (src[i] === "\\") {
123
+ out += unescapeChar(src[i + 1]);
124
+ i += 2;
125
+ continue;
126
+ }
127
+ if (src[i] === "\n")
128
+ line++;
129
+ out += src[i];
130
+ i++;
131
+ }
132
+ i++;
133
+ toks.push({ kind: "str", text: out, start, line });
134
+ continue;
135
+ }
136
+ // Template literal
137
+ if (c === "`") {
138
+ const start = i;
139
+ i++;
140
+ let out = "";
141
+ let pure = true;
142
+ while (i < n && src[i] !== "`") {
143
+ if (src[i] === "\\") {
144
+ out += unescapeChar(src[i + 1]);
145
+ i += 2;
146
+ continue;
147
+ }
148
+ if (src[i] === "$" && src[i + 1] === "{") {
149
+ pure = false;
150
+ // Skip the interpolation, balancing braces (and nested templates).
151
+ i += 2;
152
+ let depth = 1;
153
+ while (i < n && depth > 0) {
154
+ if (src[i] === "{")
155
+ depth++;
156
+ else if (src[i] === "}")
157
+ depth--;
158
+ else if (src[i] === "\n")
159
+ line++;
160
+ i++;
161
+ }
162
+ continue;
163
+ }
164
+ if (src[i] === "\n")
165
+ line++;
166
+ out += src[i];
167
+ i++;
168
+ }
169
+ i++;
170
+ toks.push({ kind: "template", text: out, pure, start, line });
171
+ continue;
172
+ }
173
+ // Numbers
174
+ if (/[0-9]/.test(c) || (c === "." && /[0-9]/.test(src[i + 1] ?? ""))) {
175
+ const start = i;
176
+ while (i < n && /[0-9a-fA-FxXoObBeE._+-]/.test(src[i])) {
177
+ // `+`/`-` only continue a number inside an exponent.
178
+ if ((src[i] === "+" || src[i] === "-") && !/[eE]/.test(src[i - 1]))
179
+ break;
180
+ i++;
181
+ }
182
+ toks.push({ kind: "num", text: src.slice(start, i), start, line });
183
+ continue;
184
+ }
185
+ // Identifiers / keywords
186
+ if (IDENT_START.test(c)) {
187
+ const start = i;
188
+ while (i < n && IDENT_PART.test(src[i]))
189
+ i++;
190
+ toks.push({ kind: "ident", text: src.slice(start, i), start, line });
191
+ continue;
192
+ }
193
+ // Punctuation — longest match first so `=>` and `...` stay single tokens.
194
+ const three = src.slice(i, i + 3);
195
+ const two = src.slice(i, i + 2);
196
+ if (three === "..." || three === "===" || three === "!==") {
197
+ toks.push({ kind: "punct", text: three, start: i, line });
198
+ i += 3;
199
+ continue;
200
+ }
201
+ if (["=>", "==", "!=", "&&", "||", "??", "?.", "<=", ">=", "++", "--"].includes(two)) {
202
+ toks.push({ kind: "punct", text: two, start: i, line });
203
+ i += 2;
204
+ continue;
205
+ }
206
+ toks.push({ kind: "punct", text: c, start: i, line });
207
+ i++;
208
+ }
209
+ return toks;
210
+ }
211
+ function unescapeChar(c) {
212
+ switch (c) {
213
+ case "n":
214
+ return "\n";
215
+ case "t":
216
+ return "\t";
217
+ case "r":
218
+ return "\r";
219
+ case "0":
220
+ return "\0";
221
+ case undefined:
222
+ return "";
223
+ default:
224
+ return c;
225
+ }
226
+ }
227
+ // ── Parser / evaluator ────────────────────────────────────────────────────
228
+ const OPENERS = { "{": "}", "[": "]", "(": ")" };
229
+ /**
230
+ * Tokens that, appearing straight after a value, prove the expression is a
231
+ * computation rather than a literal. `[` is included: `nav[0]` is a computed
232
+ * index, and silently yielding `nav` returns a whole well-formed sidebar from
233
+ * the wrong node of the tree.
234
+ */
235
+ const OPERATORS = new Set([
236
+ "+", "-", "*", "/", "%", "?", ":", "|", "&", "^",
237
+ "||", "&&", "??", "==", "!=", "===", "!==", "<", ">", "<=", ">=", "[",
238
+ ]);
239
+ const CLOSERS = new Set(["}", "]", ")"]);
240
+ class Evaluator {
241
+ toks;
242
+ bindings = new Map();
243
+ unresolved = [];
244
+ constructor(src) {
245
+ this.toks = tokenize(src);
246
+ this.collectBindings();
247
+ }
248
+ tok(i) {
249
+ return this.toks[i];
250
+ }
251
+ is(i, text) {
252
+ const t = this.toks[i];
253
+ return !!t && t.text === text && (t.kind === "punct" || t.kind === "ident");
254
+ }
255
+ report(i, what) {
256
+ const t = this.toks[i];
257
+ const where = t ? ` (line ${t.line})` : "";
258
+ const msg = `${what}${where}`;
259
+ if (!this.unresolved.includes(msg))
260
+ this.unresolved.push(msg);
261
+ }
262
+ /**
263
+ * Skip a balanced expression starting at `i`, stopping at a top-level `,`
264
+ * or a closer. Returns the index just past it. This is what keeps an
265
+ * unreadable expression from desyncing the enclosing object.
266
+ */
267
+ /**
268
+ * Skip `i` forward by at least one token.
269
+ *
270
+ * `skipValue` deliberately returns `i` UNCHANGED when it is already sitting
271
+ * on a closer, so every caller that loops must force progress or it spins
272
+ * forever. A computed key like `{ [base + "/"]: [...] }` leaves `k` on the
273
+ * `]` and hung the whole import with no output — a config that never
274
+ * returns is strictly worse than one that fails.
275
+ */
276
+ advance(i) {
277
+ const next = this.skipValue(i);
278
+ return next === i ? i + 1 : next;
279
+ }
280
+ skipValue(i) {
281
+ let depth = 0;
282
+ while (i < this.toks.length) {
283
+ const t = this.toks[i];
284
+ if (depth === 0 && t.kind === "punct" && (t.text === "," || CLOSERS.has(t.text))) {
285
+ return i;
286
+ }
287
+ if (t.kind === "punct" && OPENERS[t.text])
288
+ depth++;
289
+ else if (t.kind === "punct" && CLOSERS.has(t.text))
290
+ depth--;
291
+ i++;
292
+ }
293
+ return i;
294
+ }
295
+ /** Skip a TS type annotation from `:` until `=`, `=>`, `,` or a closer. */
296
+ /**
297
+ * Skip a balanced `<…>` type-argument list, returning the index just past
298
+ * `>`. `skipTypeAnnotation` cannot do this: it only stops at depth 0, so
299
+ * after `>` closed the generics it kept going into the argument list and
300
+ * swallowed the whole call — losing the entire config of a site using
301
+ * VitePress's own documented `defineConfigWithTheme<ThemeConfig>({…})`.
302
+ */
303
+ skipAngles(i) {
304
+ if (!this.is(i, "<"))
305
+ return i;
306
+ let depth = 0;
307
+ while (i < this.toks.length) {
308
+ const t = this.toks[i];
309
+ if (t.kind === "punct" && t.text === "<")
310
+ depth++;
311
+ else if (t.kind === "punct" && t.text === ">") {
312
+ depth--;
313
+ if (depth === 0)
314
+ return i + 1;
315
+ }
316
+ else if (t.kind === "punct" && (t.text === "(" || t.text === "{")) {
317
+ // A `(`/`{` inside what we assumed were generics means it was not a
318
+ // type-argument list at all. Bail rather than run away.
319
+ return i;
320
+ }
321
+ i++;
322
+ }
323
+ return i;
324
+ }
325
+ /**
326
+ * From a `:` type annotation on a BINDING, find its `=`.
327
+ *
328
+ * A function type contains `=>` (`const s: () => Item[] = () => [...]`), and
329
+ * `skipTypeAnnotation` stops there — so the binding was never recorded and
330
+ * the sidebar it held was lost. `=>` is one token, so scanning for an exact
331
+ * `=` at depth 0 skips it naturally.
332
+ */
333
+ skipToAssign(i) {
334
+ let depth = 0;
335
+ while (i < this.toks.length) {
336
+ const t = this.toks[i];
337
+ if (depth === 0 && t.kind === "punct" && t.text === "=")
338
+ return i;
339
+ if (t.kind === "punct" && (OPENERS[t.text] || t.text === "<"))
340
+ depth++;
341
+ else if (t.kind === "punct" && (CLOSERS.has(t.text) || t.text === ">")) {
342
+ if (depth === 0)
343
+ return i;
344
+ depth--;
345
+ }
346
+ i++;
347
+ }
348
+ return i;
349
+ }
350
+ skipTypeAnnotation(i) {
351
+ let depth = 0;
352
+ while (i < this.toks.length) {
353
+ const t = this.toks[i];
354
+ if (depth === 0 && t.kind === "punct" && (t.text === "=" || t.text === "=>"))
355
+ return i;
356
+ if (depth === 0 && t.kind === "punct" && (t.text === "," || CLOSERS.has(t.text)))
357
+ return i;
358
+ if (t.kind === "punct" && (OPENERS[t.text] || t.text === "<"))
359
+ depth++;
360
+ else if (t.kind === "punct" && (CLOSERS.has(t.text) || t.text === ">"))
361
+ depth--;
362
+ i++;
363
+ }
364
+ return i;
365
+ }
366
+ /** Index the module's top-level `const`/`function`/`export default`. */
367
+ collectBindings() {
368
+ let depth = 0;
369
+ for (let i = 0; i < this.toks.length; i++) {
370
+ const t = this.toks[i];
371
+ if (t.kind === "punct" && OPENERS[t.text]) {
372
+ depth++;
373
+ continue;
374
+ }
375
+ if (t.kind === "punct" && CLOSERS.has(t.text)) {
376
+ depth--;
377
+ continue;
378
+ }
379
+ if (depth !== 0)
380
+ continue;
381
+ if (t.kind !== "ident")
382
+ continue;
383
+ // `export default <expr>` — also the wrapped `export default defineConfig({…})`.
384
+ if (t.text === "export" && this.is(i + 1, "default")) {
385
+ this.bindings.set("default", { at: i + 2 });
386
+ continue;
387
+ }
388
+ // Skip the `export` modifier so `export const x = …` is indexed too.
389
+ const j = t.text === "export" ? i + 1 : i;
390
+ const kw = this.tok(j);
391
+ if (!kw || kw.kind !== "ident")
392
+ continue;
393
+ if (kw.text === "const" || kw.text === "let" || kw.text === "var") {
394
+ const name = this.tok(j + 1);
395
+ if (!name || name.kind !== "ident")
396
+ continue;
397
+ let k = j + 2;
398
+ if (this.is(k, ":"))
399
+ k = this.skipToAssign(k + 1);
400
+ if (!this.is(k, "="))
401
+ continue;
402
+ this.bindings.set(name.text, { at: k + 1 });
403
+ continue;
404
+ }
405
+ if (kw.text === "function") {
406
+ // `function f() { … return <expr> … }`
407
+ let k = j + 1;
408
+ if (this.is(k, "*"))
409
+ k++;
410
+ const name = this.tok(k);
411
+ if (!name || name.kind !== "ident")
412
+ continue;
413
+ const bodyStart = this.findFunctionBody(k + 1);
414
+ if (bodyStart < 0)
415
+ continue;
416
+ const ret = this.findReturn(bodyStart);
417
+ if (ret >= 0)
418
+ this.bindings.set(name.text, { at: ret });
419
+ }
420
+ }
421
+ }
422
+ /** From after a function name, find the index just past `{` of its body. */
423
+ findFunctionBody(i) {
424
+ // params
425
+ if (!this.is(i, "(")) {
426
+ // Possibly a generic `<T>` first.
427
+ if (this.is(i, "<"))
428
+ i = this.skipTypeAnnotation(i);
429
+ if (!this.is(i, "("))
430
+ return -1;
431
+ }
432
+ i = this.skipBalanced(i);
433
+ if (this.is(i, ":"))
434
+ i = this.skipTypeAnnotation(i + 1);
435
+ if (!this.is(i, "{"))
436
+ return -1;
437
+ return i + 1;
438
+ }
439
+ /** Index just past the group opened at `i`. */
440
+ skipBalanced(i) {
441
+ if (!this.toks[i] || !OPENERS[this.toks[i].text])
442
+ return i;
443
+ let depth = 0;
444
+ while (i < this.toks.length) {
445
+ const t = this.toks[i];
446
+ if (t.kind === "punct" && OPENERS[t.text])
447
+ depth++;
448
+ else if (t.kind === "punct" && CLOSERS.has(t.text)) {
449
+ depth--;
450
+ if (depth === 0)
451
+ return i + 1;
452
+ }
453
+ i++;
454
+ }
455
+ return i;
456
+ }
457
+ /** First `return` at body depth 0, from `i` (already inside the body). */
458
+ findReturn(i) {
459
+ let depth = 0;
460
+ while (i < this.toks.length) {
461
+ const t = this.toks[i];
462
+ if (t.kind === "punct" && OPENERS[t.text])
463
+ depth++;
464
+ else if (t.kind === "punct" && CLOSERS.has(t.text)) {
465
+ if (depth === 0)
466
+ return -1; // end of function body
467
+ depth--;
468
+ }
469
+ else if (depth === 0 && t.kind === "ident" && t.text === "return") {
470
+ return i + 1;
471
+ }
472
+ i++;
473
+ }
474
+ return -1;
475
+ }
476
+ /** Evaluate a named module binding (memoized, cycle-safe). */
477
+ resolveBinding(name) {
478
+ const b = this.bindings.get(name);
479
+ if (!b)
480
+ return UNRESOLVED;
481
+ if (b.computing)
482
+ return UNRESOLVED;
483
+ if (b.value !== undefined)
484
+ return b.value;
485
+ b.computing = true;
486
+ const v = this.parseValue(b.at).value;
487
+ b.computing = false;
488
+ b.value = v;
489
+ return v;
490
+ }
491
+ hasBinding(name) {
492
+ return this.bindings.has(name);
493
+ }
494
+ /**
495
+ * The default export, with one extra allowance: VitePress accepts a config
496
+ * that is a FUNCTION of the build context (`export default ({ mode }) => ({…})`
497
+ * — vitest's shape). We cannot supply that context, so we read the function's
498
+ * return expression directly.
499
+ *
500
+ * This is deliberately scoped to the default export. Treating any
501
+ * parameterized arrow as its own body would evaluate `.map(x => …)`
502
+ * callbacks as data, which is how a wrong value gets into a sidebar.
503
+ */
504
+ resolveDefaultExport() {
505
+ const b = this.bindings.get("default");
506
+ if (!b)
507
+ return UNRESOLVED;
508
+ let k = b.at;
509
+ if (this.is(k, "async"))
510
+ k++;
511
+ if (this.is(k, "function")) {
512
+ let j = k + 1;
513
+ if (this.toks[j]?.kind === "ident")
514
+ j++;
515
+ const body = this.findFunctionBody(j);
516
+ const ret = body < 0 ? -1 : this.findReturn(body);
517
+ return ret < 0 ? UNRESOLVED : this.parseValue(ret).value;
518
+ }
519
+ if (this.is(k, "(")) {
520
+ let j = this.skipBalanced(k);
521
+ if (this.is(j, ":"))
522
+ j = this.skipTypeAnnotation(j + 1);
523
+ if (this.is(j, "=>")) {
524
+ const body = j + 1;
525
+ if (this.is(body, "{")) {
526
+ const ret = this.findReturn(body + 1);
527
+ return ret < 0 ? UNRESOLVED : this.parseValue(ret).value;
528
+ }
529
+ return this.parseValue(body).value;
530
+ }
531
+ }
532
+ return this.resolveBinding("default");
533
+ }
534
+ /** Parse the expression at `i`. Returns its value and the next index. */
535
+ parseValue(i) {
536
+ const t = this.toks[i];
537
+ if (!t)
538
+ return { value: UNRESOLVED, next: i };
539
+ // Unary minus on a number.
540
+ if (t.kind === "punct" && t.text === "-" && this.toks[i + 1]?.kind === "num") {
541
+ return this.postfix(-Number(this.toks[i + 1].text), i + 2);
542
+ }
543
+ if (t.kind === "str")
544
+ return this.postfix(t.text, i + 1);
545
+ if (t.kind === "num")
546
+ return this.postfix(Number(t.text), i + 1);
547
+ if (t.kind === "template") {
548
+ if (t.pure)
549
+ return this.postfix(t.text, i + 1);
550
+ this.report(i, "template literal with interpolation");
551
+ return { value: UNRESOLVED, next: this.skipValue(i) };
552
+ }
553
+ if (t.kind === "ident") {
554
+ if (t.text === "true")
555
+ return this.postfix(true, i + 1);
556
+ if (t.text === "false")
557
+ return this.postfix(false, i + 1);
558
+ if (t.text === "null")
559
+ return this.postfix(null, i + 1);
560
+ if (t.text === "undefined")
561
+ return this.postfix(UNRESOLVED, i + 1);
562
+ // Arrow with no params: `() => <expr>` is handled at `(`; a single
563
+ // bare param `x => …` is a function value we cannot call — unresolved.
564
+ if (this.is(i + 1, "=>")) {
565
+ this.report(i, `arrow function \`${t.text} => …\``);
566
+ return { value: UNRESOLVED, next: this.skipValue(i) };
567
+ }
568
+ // Call expression.
569
+ if (this.is(i + 1, "(") || this.is(i + 1, "<")) {
570
+ return this.parseCall(t.text, i);
571
+ }
572
+ // Identifier reference.
573
+ if (this.bindings.has(t.text)) {
574
+ return this.postfix(this.resolveBinding(t.text), i + 1);
575
+ }
576
+ // An imported or otherwise unknown identifier.
577
+ this.report(i, `identifier \`${t.text}\` (imported or computed)`);
578
+ return { value: UNRESOLVED, next: this.skipValue(i) };
579
+ }
580
+ if (t.kind === "punct") {
581
+ if (t.text === "[")
582
+ return this.parseArray(i);
583
+ if (t.text === "{")
584
+ return this.parseObject(i);
585
+ if (t.text === "(")
586
+ return this.parseParenOrArrow(i);
587
+ }
588
+ this.report(i, `unsupported expression starting with \`${t.text}\``);
589
+ return { value: UNRESOLVED, next: this.skipValue(i) };
590
+ }
591
+ /**
592
+ * `(…)` is either a parenthesized expression, or the parameter list of an
593
+ * arrow function. A ZERO-ARG arrow is callable by this evaluator, so it is
594
+ * treated as its own body: `const f = () => [ … ]` binds `f` to the array.
595
+ */
596
+ parseParenOrArrow(i) {
597
+ const afterParens = this.skipBalanced(i);
598
+ let k = afterParens;
599
+ if (this.is(k, ":"))
600
+ k = this.skipTypeAnnotation(k + 1);
601
+ if (this.is(k, "=>")) {
602
+ const zeroArg = this.is(i + 1, ")");
603
+ if (!zeroArg) {
604
+ this.report(i, "arrow function with parameters");
605
+ return { value: UNRESOLVED, next: this.skipValue(i) };
606
+ }
607
+ let body = k + 1;
608
+ if (this.is(body, "{")) {
609
+ const ret = this.findReturn(body + 1);
610
+ if (ret < 0) {
611
+ this.report(i, "arrow function body with no literal `return`");
612
+ return { value: UNRESOLVED, next: this.skipValue(i) };
613
+ }
614
+ const v = this.parseValue(ret).value;
615
+ return { value: v, next: this.skipBalanced(body) };
616
+ }
617
+ const { value } = this.parseValue(body);
618
+ return { value, next: this.skipValue(body) };
619
+ }
620
+ // Plain parenthesized expression.
621
+ const inner = this.parseValue(i + 1);
622
+ return this.postfix(inner.value, afterParens);
623
+ }
624
+ /**
625
+ * A call. Two shapes are readable:
626
+ * - zero args on a module-level function/arrow → its literal body
627
+ * (hono: `sidebar: { '/': sidebars() }`)
628
+ * - one literal argument on an unknown function → the argument
629
+ * (`defineConfig({…})`, `defineConfigWithTheme({…})`, `withMermaid({…})`)
630
+ */
631
+ parseCall(name, i) {
632
+ let open = i + 1;
633
+ if (this.is(open, "<"))
634
+ open = this.skipAngles(open); // generic type args
635
+ if (!this.is(open, "(")) {
636
+ this.report(i, `call to \`${name}\``);
637
+ return { value: UNRESOLVED, next: this.skipValue(i) };
638
+ }
639
+ const after = this.skipBalanced(open);
640
+ // Zero args → the callee's own literal body.
641
+ if (this.is(open + 1, ")")) {
642
+ if (this.bindings.has(name)) {
643
+ return this.postfix(this.resolveBinding(name), after);
644
+ }
645
+ this.report(i, `call to unknown function \`${name}()\``);
646
+ return { value: UNRESOLVED, next: after };
647
+ }
648
+ // A single-argument call on an unknown function is unwrapped ONLY in the
649
+ // config-wrapper shape: the argument is an object literal, or another call
650
+ // (wrappers nest — vitest's export is
651
+ // `withPwa(extendConfig(defineConfig({…})))`).
652
+ //
653
+ // It must NOT unwrap a single ARRAY argument. `vitepress-sidebar`, the most
654
+ // common sidebar plugin, is called `generateSidebar([{…}, {…}])` — and
655
+ // returning its options array as the sidebar produced a complete,
656
+ // plausible-looking nav made of plugin CONFIG, reported as fully resolved.
657
+ // A wrong sidebar that declares itself correct is the worst output this
658
+ // module can produce.
659
+ const arg = this.toks[open + 1];
660
+ const isObjectLiteral = arg?.kind === "punct" && arg.text === "{";
661
+ const isNestedCall = arg?.kind === "ident" && (this.is(open + 2, "(") || this.is(open + 2, "<"));
662
+ if (isObjectLiteral || isNestedCall) {
663
+ const { value, next } = this.parseValue(open + 1);
664
+ if (this.is(next, ")"))
665
+ return this.postfix(value, after);
666
+ }
667
+ this.report(i, `call to \`${name}(…)\``);
668
+ return { value: UNRESOLVED, next: after };
669
+ }
670
+ parseArray(i) {
671
+ const out = [];
672
+ let k = i + 1;
673
+ while (k < this.toks.length && !this.is(k, "]")) {
674
+ if (this.is(k, ",")) {
675
+ k++;
676
+ continue;
677
+ }
678
+ if (this.is(k, "...")) {
679
+ const { value, next } = this.parseValue(k + 1);
680
+ if (Array.isArray(value))
681
+ out.push(...value);
682
+ else if (value !== UNRESOLVED)
683
+ this.report(k, "spread of a non-array");
684
+ k = next;
685
+ continue;
686
+ }
687
+ const { value, next } = this.parseValue(k);
688
+ out.push(value);
689
+ k = next === k ? k + 1 : next;
690
+ }
691
+ return this.postfix(out, k + 1);
692
+ }
693
+ parseObject(i) {
694
+ const out = {};
695
+ let k = i + 1;
696
+ while (k < this.toks.length && !this.is(k, "}")) {
697
+ if (this.is(k, ",") || this.is(k, ";")) {
698
+ k++;
699
+ continue;
700
+ }
701
+ if (this.is(k, "...")) {
702
+ const { value, next } = this.parseValue(k + 1);
703
+ // `typeof x === "object"` already excludes the UNRESOLVED symbol.
704
+ if (value && typeof value === "object" && !Array.isArray(value)) {
705
+ Object.assign(out, value);
706
+ }
707
+ else if (value !== UNRESOLVED) {
708
+ this.report(k, "spread of a non-object");
709
+ }
710
+ k = next;
711
+ continue;
712
+ }
713
+ // Key.
714
+ const kt = this.toks[k];
715
+ if (!kt)
716
+ break;
717
+ let key = null;
718
+ if (kt.kind === "str" || kt.kind === "template")
719
+ key = kt.text;
720
+ else if (kt.kind === "ident" || kt.kind === "num")
721
+ key = kt.text;
722
+ else if (kt.kind === "punct" && kt.text === "[") {
723
+ // Computed key — read it if it is a literal, else skip the entry.
724
+ const { value, next } = this.parseValue(k + 1);
725
+ k = next;
726
+ if (this.is(k, "]"))
727
+ k++;
728
+ key = typeof value === "string" || typeof value === "number" ? String(value) : null;
729
+ if (key === null) {
730
+ this.report(kt.start, "computed object key");
731
+ k = this.is(k, ":") ? this.advance(k + 1) : this.advance(k);
732
+ continue;
733
+ }
734
+ if (this.is(k, ":")) {
735
+ const v = this.parseValue(k + 1);
736
+ out[key] = v.value;
737
+ k = v.next;
738
+ }
739
+ continue;
740
+ }
741
+ else {
742
+ // Something unexpected in key position — skip one entry.
743
+ k = this.advance(k);
744
+ continue;
745
+ }
746
+ k++;
747
+ // Method shorthand `name() { … }` — a function, not data.
748
+ if (this.is(k, "(")) {
749
+ const bodyStart = this.findFunctionBody(k);
750
+ k = bodyStart < 0 ? this.skipValue(k) : this.skipBalanced(bodyStart - 1);
751
+ continue;
752
+ }
753
+ if (!this.is(k, ":")) {
754
+ // Shorthand `{ foo }`.
755
+ out[key] = this.bindings.has(key) ? this.resolveBinding(key) : UNRESOLVED;
756
+ if (!this.bindings.has(key))
757
+ this.report(k, `shorthand property \`${key}\``);
758
+ continue;
759
+ }
760
+ const v = this.parseValue(k + 1);
761
+ out[key] = v.value;
762
+ k = v.next === k + 1 ? this.skipValue(k + 1) : v.next;
763
+ }
764
+ return this.postfix(out, k + 1);
765
+ }
766
+ /**
767
+ * Postfix operators after a value: TS `as`/`satisfies`, and member access
768
+ * into a value we already have.
769
+ */
770
+ postfix(value, i) {
771
+ let v = value;
772
+ let k = i;
773
+ for (;;) {
774
+ if (this.is(k, "as") || this.is(k, "satisfies")) {
775
+ k = this.skipTypeAnnotation(k + 1);
776
+ continue;
777
+ }
778
+ if (this.is(k, "!")) {
779
+ k++;
780
+ continue;
781
+ }
782
+ if ((this.is(k, ".") || this.is(k, "?.")) && this.toks[k + 1]?.kind === "ident") {
783
+ const prop = this.toks[k + 1].text;
784
+ const obj = v && typeof v === "object" && !Array.isArray(v)
785
+ ? v
786
+ : undefined;
787
+ if (!obj || obj[prop] === undefined) {
788
+ // Includes every METHOD call — `.map(…)`, `.concat(…)`. Reporting
789
+ // matters: `config.ts` drops UNRESOLVED leaves, so an unreported
790
+ // one vanishes from the config AND from the "could not read" list,
791
+ // which is precisely the silent omission this module forbids.
792
+ this.report(k, `property access \`.${prop}\` on a value this reader could not resolve`);
793
+ v = UNRESOLVED;
794
+ }
795
+ else {
796
+ v = obj[prop];
797
+ }
798
+ k += 2;
799
+ continue;
800
+ }
801
+ break;
802
+ }
803
+ // An operator after the value means the real expression is a computation
804
+ // (`base + '/a'`, `isProd ? a : b`, `a || fallback`, `nav[0].items`).
805
+ // Returning just the LEFT operand handed back a confident, wrong answer —
806
+ // `link: base + '/a'` became `link: "/guide"`, and `sidebar: nav[0].items`
807
+ // became the whole `nav` array — with nothing recorded as unresolved.
808
+ if (OPERATORS.has(this.toks[k]?.text ?? "") && this.toks[k]?.kind === "punct") {
809
+ this.report(k, `expression using \`${this.toks[k].text}\` (computed, not a literal)`);
810
+ return { value: UNRESOLVED, next: this.skipValue(k) };
811
+ }
812
+ return { value: v, next: k };
813
+ }
814
+ }
815
+ /**
816
+ * Evaluate a module's default export as far as the literal subset allows.
817
+ *
818
+ * Falls back to the binding named by `fallbackExport` (default `config`) when
819
+ * there is no `export default`, which covers
820
+ * `const config = { … }; export { config }`.
821
+ */
822
+ export function evaluateModule(source, fallbackExport = "config") {
823
+ const ev = new Evaluator(source);
824
+ let value = UNRESOLVED;
825
+ if (ev.hasBinding("default"))
826
+ value = ev.resolveDefaultExport();
827
+ if (value === UNRESOLVED && ev.hasBinding(fallbackExport)) {
828
+ value = ev.resolveBinding(fallbackExport);
829
+ }
830
+ return { value, unresolved: ev.unresolved };
831
+ }
832
+ /** Narrowing helper: a plain object with no UNRESOLVED identity. */
833
+ export function asObject(v) {
834
+ if (v && typeof v === "object" && !Array.isArray(v)) {
835
+ return v;
836
+ }
837
+ return undefined;
838
+ }
839
+ export function asArray(v) {
840
+ return Array.isArray(v) ? v : undefined;
841
+ }
842
+ export function asString(v) {
843
+ return typeof v === "string" ? v : undefined;
844
+ }
845
+ //# sourceMappingURL=literal.js.map