@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.
- package/README.md +46 -0
- package/dist/cli.d.ts +18 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +114 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +49 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +183 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/literal.d.ts +55 -0
- package/dist/literal.d.ts.map +1 -0
- package/dist/literal.js +845 -0
- package/dist/literal.js.map +1 -0
- package/dist/loaders/load.d.ts +74 -0
- package/dist/loaders/load.d.ts.map +1 -0
- package/dist/loaders/load.js +448 -0
- package/dist/loaders/load.js.map +1 -0
- package/dist/nav.d.ts +51 -0
- package/dist/nav.d.ts.map +1 -0
- package/dist/nav.js +182 -0
- package/dist/nav.js.map +1 -0
- package/dist/parse.d.ts +14 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/parse.js +35 -0
- package/dist/parse.js.map +1 -0
- package/dist/translate.d.ts +80 -0
- package/dist/translate.d.ts.map +1 -0
- package/dist/translate.js +882 -0
- package/dist/translate.js.map +1 -0
- package/package.json +37 -0
package/dist/literal.js
ADDED
|
@@ -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
|