@hashrock/ono 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -0
- package/package.json +14 -13
- package/src/barrels.js +230 -0
- package/src/browser/compiler.js +71 -193
- package/src/browser/unocss.js +5 -1
- package/src/builder.js +109 -230
- package/src/bundler.js +215 -64
- package/src/cli.js +22 -191
- package/src/commands/build.js +141 -0
- package/src/commands/dev.js +54 -0
- package/src/constants.js +74 -0
- package/src/jsx-runtime.js +15 -5
- package/src/parser.js +555 -0
- package/src/renderer.js +29 -48
- package/src/server.js +88 -74
- package/src/transformer.js +1 -18
- package/src/unocss.js +20 -24
- package/src/utils.js +55 -0
- package/src/watcher.js +98 -125
- package/src/content.js +0 -272
- package/src/resolver.js +0 -137
package/src/parser.js
ADDED
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
// @ts-nocheck — parser combinators return anonymous tuples that TypeScript
|
|
2
|
+
// can't usefully infer; checkJs adds noise here without catching real bugs.
|
|
3
|
+
/**
|
|
4
|
+
* Parser combinators and an ES module-syntax parser.
|
|
5
|
+
*
|
|
6
|
+
* Parses only what the bundler needs — import/export declarations and
|
|
7
|
+
* top-level function names — using parser combinators instead of regular
|
|
8
|
+
* expressions. The rest of the source is skipped by a small scanner that
|
|
9
|
+
* understands strings, template literals, comments, and (heuristically)
|
|
10
|
+
* regex literals, so an "import" inside a string is never misparsed.
|
|
11
|
+
*
|
|
12
|
+
* Browser-compatible: no Node.js APIs.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// --- Combinator core -------------------------------------------------------
|
|
16
|
+
// A parser is a function (input, pos) => result.
|
|
17
|
+
// Success: { ok: true, value, pos } Failure: { ok: false, expected, pos }
|
|
18
|
+
|
|
19
|
+
const ok = (value, pos) => ({ ok: true, value, pos });
|
|
20
|
+
const fail = (expected, pos) => ({ ok: false, expected, pos });
|
|
21
|
+
|
|
22
|
+
/** Match an exact string */
|
|
23
|
+
export function str(expected) {
|
|
24
|
+
return (input, pos) =>
|
|
25
|
+
input.startsWith(expected, pos)
|
|
26
|
+
? ok(expected, pos + expected.length)
|
|
27
|
+
: fail(expected, pos);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Run parsers in order, collecting their values */
|
|
31
|
+
export function seq(...parsers) {
|
|
32
|
+
return (input, pos) => {
|
|
33
|
+
const values = [];
|
|
34
|
+
let current = pos;
|
|
35
|
+
for (const parser of parsers) {
|
|
36
|
+
const result = parser(input, current);
|
|
37
|
+
if (!result.ok) return result;
|
|
38
|
+
values.push(result.value);
|
|
39
|
+
current = result.pos;
|
|
40
|
+
}
|
|
41
|
+
return ok(values, current);
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Try parsers in order, returning the first success */
|
|
46
|
+
export function alt(...parsers) {
|
|
47
|
+
return (input, pos) => {
|
|
48
|
+
let furthest = null;
|
|
49
|
+
for (const parser of parsers) {
|
|
50
|
+
const result = parser(input, pos);
|
|
51
|
+
if (result.ok) return result;
|
|
52
|
+
if (!furthest || result.pos > furthest.pos) furthest = result;
|
|
53
|
+
}
|
|
54
|
+
return furthest || fail("no alternative matched", pos);
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Match zero or more repetitions */
|
|
59
|
+
export function many(parser) {
|
|
60
|
+
return (input, pos) => {
|
|
61
|
+
const values = [];
|
|
62
|
+
let current = pos;
|
|
63
|
+
for (;;) {
|
|
64
|
+
const result = parser(input, current);
|
|
65
|
+
if (!result.ok || result.pos === current) return ok(values, current);
|
|
66
|
+
values.push(result.value);
|
|
67
|
+
current = result.pos;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Make a parser optional (yields null when it fails) */
|
|
73
|
+
export function opt(parser) {
|
|
74
|
+
return (input, pos) => {
|
|
75
|
+
const result = parser(input, pos);
|
|
76
|
+
return result.ok ? result : ok(null, pos);
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Transform a parser's value */
|
|
81
|
+
export function map(parser, fn) {
|
|
82
|
+
return (input, pos) => {
|
|
83
|
+
const result = parser(input, pos);
|
|
84
|
+
return result.ok ? ok(fn(result.value), result.pos) : result;
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Zero or more items separated by a separator */
|
|
89
|
+
export function sepBy(item, separator) {
|
|
90
|
+
return (input, pos) => {
|
|
91
|
+
const first = item(input, pos);
|
|
92
|
+
if (!first.ok) return ok([], pos);
|
|
93
|
+
const rest = many(map(seq(separator, item), ([, value]) => value));
|
|
94
|
+
const result = rest(input, first.pos);
|
|
95
|
+
return ok([first.value, ...result.value], result.pos);
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// --- Lexical helpers -------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
const isWs = (c) => c === " " || c === "\t" || c === "\n" || c === "\r" || c === "\f" || c === "\v";
|
|
102
|
+
const isIdStart = (c) => (c >= "a" && c <= "z") || (c >= "A" && c <= "Z") || c === "_" || c === "$";
|
|
103
|
+
const isIdChar = (c) => isIdStart(c) || (c >= "0" && c <= "9");
|
|
104
|
+
|
|
105
|
+
/** True when the whole string is a valid (ASCII) identifier name */
|
|
106
|
+
export function isIdentifierName(name) {
|
|
107
|
+
if (name.length === 0 || !isIdStart(name[0])) return false;
|
|
108
|
+
for (let i = 1; i < name.length; i++) {
|
|
109
|
+
if (!isIdChar(name[i])) return false;
|
|
110
|
+
}
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Skip whitespace and comments, returning the new position */
|
|
115
|
+
export function skipTrivia(input, pos) {
|
|
116
|
+
for (;;) {
|
|
117
|
+
while (pos < input.length && isWs(input[pos])) pos++;
|
|
118
|
+
if (input.startsWith("//", pos)) {
|
|
119
|
+
while (pos < input.length && input[pos] !== "\n") pos++;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (input.startsWith("/*", pos)) {
|
|
123
|
+
const end = input.indexOf("*/", pos + 2);
|
|
124
|
+
pos = end === -1 ? input.length : end + 2;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
return pos;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Wrap a parser so it skips leading trivia */
|
|
132
|
+
function token(parser) {
|
|
133
|
+
return (input, pos) => parser(input, skipTrivia(input, pos));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Any identifier-like word (including reserved words) */
|
|
137
|
+
const wordRaw = (input, pos) => {
|
|
138
|
+
if (pos >= input.length || !isIdStart(input[pos])) return fail("identifier", pos);
|
|
139
|
+
let end = pos + 1;
|
|
140
|
+
while (end < input.length && isIdChar(input[end])) end++;
|
|
141
|
+
return ok(input.slice(pos, end), end);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const word = token(wordRaw);
|
|
145
|
+
|
|
146
|
+
/** A specific keyword (whole word) */
|
|
147
|
+
const kw = (expected) =>
|
|
148
|
+
token((input, pos) => {
|
|
149
|
+
const result = wordRaw(input, pos);
|
|
150
|
+
return result.ok && result.value === expected ? result : fail(`"${expected}"`, pos);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const punct = (s) => token(str(s));
|
|
154
|
+
|
|
155
|
+
/** A single- or double-quoted string literal, yielding its contents */
|
|
156
|
+
const stringLit = token((input, pos) => {
|
|
157
|
+
const quote = input[pos];
|
|
158
|
+
if (quote !== '"' && quote !== "'") return fail("string literal", pos);
|
|
159
|
+
let out = "";
|
|
160
|
+
let i = pos + 1;
|
|
161
|
+
while (i < input.length) {
|
|
162
|
+
const c = input[i];
|
|
163
|
+
if (c === "\\") {
|
|
164
|
+
out += input[i + 1] ?? "";
|
|
165
|
+
i += 2;
|
|
166
|
+
} else if (c === quote) {
|
|
167
|
+
return ok(out, i + 1);
|
|
168
|
+
} else if (c === "\n") {
|
|
169
|
+
break;
|
|
170
|
+
} else {
|
|
171
|
+
out += c;
|
|
172
|
+
i++;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return fail("closing quote", pos);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// --- Import declaration grammar -------------------------------------------
|
|
179
|
+
|
|
180
|
+
// { a, b as c, default as d }
|
|
181
|
+
const importSpecifier = map(
|
|
182
|
+
seq(word, opt(seq(kw("as"), word))),
|
|
183
|
+
([imported, alias]) => ({ imported, local: alias ? alias[1] : imported }),
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
const namedList = (specifier) =>
|
|
187
|
+
map(
|
|
188
|
+
seq(punct("{"), sepBy(specifier, punct(",")), opt(punct(",")), punct("}")),
|
|
189
|
+
([, specs]) => specs,
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const namespaceImport = map(seq(punct("*"), kw("as"), word), ([, , name]) => name);
|
|
193
|
+
|
|
194
|
+
const importClause = alt(
|
|
195
|
+
map(namespaceImport, (namespace) => ({ namespace })),
|
|
196
|
+
map(namedList(importSpecifier), (named) => ({ named })),
|
|
197
|
+
map(
|
|
198
|
+
seq(
|
|
199
|
+
word,
|
|
200
|
+
opt(
|
|
201
|
+
seq(
|
|
202
|
+
punct(","),
|
|
203
|
+
alt(
|
|
204
|
+
map(namespaceImport, (namespace) => ({ namespace })),
|
|
205
|
+
map(namedList(importSpecifier), (named) => ({ named })),
|
|
206
|
+
),
|
|
207
|
+
),
|
|
208
|
+
),
|
|
209
|
+
),
|
|
210
|
+
([defaultBinding, rest]) => ({ defaultBinding, ...(rest ? rest[1] : {}) }),
|
|
211
|
+
),
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const importDecl = map(
|
|
215
|
+
seq(
|
|
216
|
+
kw("import"),
|
|
217
|
+
alt(
|
|
218
|
+
map(stringLit, (specifier) => ({ specifier, sideEffect: true })),
|
|
219
|
+
map(seq(importClause, kw("from"), stringLit), ([clause, , specifier]) => ({
|
|
220
|
+
...clause,
|
|
221
|
+
specifier,
|
|
222
|
+
})),
|
|
223
|
+
),
|
|
224
|
+
opt(punct(";")),
|
|
225
|
+
),
|
|
226
|
+
([, decl]) => ({ type: "import", ...decl }),
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
// --- Export declaration grammar --------------------------------------------
|
|
230
|
+
|
|
231
|
+
// { a, b as c }
|
|
232
|
+
const exportSpecifier = map(
|
|
233
|
+
seq(word, opt(seq(kw("as"), word))),
|
|
234
|
+
([local, alias]) => ({ local, exported: alias ? alias[1] : local }),
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
const exportStarFrom = map(
|
|
238
|
+
seq(punct("*"), opt(seq(kw("as"), word)), kw("from"), stringLit, opt(punct(";"))),
|
|
239
|
+
([, alias, , specifier]) => ({
|
|
240
|
+
type: "exportStarFrom",
|
|
241
|
+
specifier,
|
|
242
|
+
alias: alias ? alias[1] : null,
|
|
243
|
+
}),
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const exportNamedFrom = map(
|
|
247
|
+
seq(namedList(exportSpecifier), kw("from"), stringLit, opt(punct(";"))),
|
|
248
|
+
([named, , specifier]) => ({ type: "exportNamedFrom", named, specifier }),
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
const exportNamed = map(
|
|
252
|
+
seq(namedList(exportSpecifier), opt(punct(";"))),
|
|
253
|
+
([named]) => ({ type: "exportNamed", named }),
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
// function Foo / async function Foo / class Foo (name optional)
|
|
257
|
+
const functionOrClassHead = alt(
|
|
258
|
+
map(seq(opt(kw("async")), kw("function"), opt(punct("*")), opt(word)), ([, , , name]) => ({
|
|
259
|
+
kind: "function",
|
|
260
|
+
name,
|
|
261
|
+
})),
|
|
262
|
+
map(seq(kw("class"), opt(word)), ([, name]) => ({ kind: "class", name })),
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Scan `const a = ..., b = ...;` collecting declared names.
|
|
267
|
+
* Initializers are skipped with depth/string tracking, not parsed.
|
|
268
|
+
*/
|
|
269
|
+
function scanDeclaratorNames(input, pos) {
|
|
270
|
+
const names = [];
|
|
271
|
+
for (;;) {
|
|
272
|
+
pos = skipTrivia(input, pos);
|
|
273
|
+
const c = input[pos];
|
|
274
|
+
if (c === "{" || c === "[") {
|
|
275
|
+
throw new Error("Destructuring in an exported declaration is not supported by the Ono bundler");
|
|
276
|
+
}
|
|
277
|
+
const nameResult = wordRaw(input, pos);
|
|
278
|
+
if (!nameResult.ok) break;
|
|
279
|
+
names.push(nameResult.value);
|
|
280
|
+
pos = nameResult.pos;
|
|
281
|
+
|
|
282
|
+
// Skip to the next top-level "," (next declarator) or ";" (end)
|
|
283
|
+
let depth = 0;
|
|
284
|
+
let done = false;
|
|
285
|
+
while (pos < input.length) {
|
|
286
|
+
pos = skipTrivia(input, pos);
|
|
287
|
+
const ch = input[pos];
|
|
288
|
+
if (ch === undefined) break;
|
|
289
|
+
if (ch === '"' || ch === "'") {
|
|
290
|
+
pos = skipStringLiteral(input, pos);
|
|
291
|
+
} else if (ch === "`") {
|
|
292
|
+
pos = skipTemplateLiteral(input, pos);
|
|
293
|
+
} else if (ch === "(" || ch === "[" || ch === "{") {
|
|
294
|
+
depth++;
|
|
295
|
+
pos++;
|
|
296
|
+
} else if (ch === ")" || ch === "]" || ch === "}") {
|
|
297
|
+
depth--;
|
|
298
|
+
pos++;
|
|
299
|
+
} else if (depth === 0 && ch === ",") {
|
|
300
|
+
pos++;
|
|
301
|
+
break;
|
|
302
|
+
} else if (depth === 0 && ch === ";") {
|
|
303
|
+
done = true;
|
|
304
|
+
break;
|
|
305
|
+
} else {
|
|
306
|
+
pos++;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (done || pos >= input.length) break;
|
|
310
|
+
}
|
|
311
|
+
return names;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Parse an export declaration starting at the `export` keyword.
|
|
316
|
+
* For `export <declaration>` forms only the header span is consumed
|
|
317
|
+
* (the declaration itself stays in place); `consumeTo` says where the
|
|
318
|
+
* scanner should continue.
|
|
319
|
+
*/
|
|
320
|
+
function exportDecl(input, pos) {
|
|
321
|
+
const head = kw("export")(input, pos);
|
|
322
|
+
if (!head.ok) return head;
|
|
323
|
+
const afterExport = head.pos;
|
|
324
|
+
|
|
325
|
+
const fromForm = alt(exportStarFrom, exportNamedFrom, exportNamed)(input, afterExport);
|
|
326
|
+
if (fromForm.ok) {
|
|
327
|
+
return ok({ ...fromForm.value, consumeTo: fromForm.pos }, fromForm.pos);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const defaultKw = kw("default")(input, afterExport);
|
|
331
|
+
if (defaultKw.ok) {
|
|
332
|
+
const headerEnd = defaultKw.pos;
|
|
333
|
+
const decl = functionOrClassHead(input, headerEnd);
|
|
334
|
+
if (decl.ok && decl.value.name) {
|
|
335
|
+
// export default function Foo() {} — keep the declaration, strip keywords
|
|
336
|
+
return ok(
|
|
337
|
+
{
|
|
338
|
+
type: "exportDefaultDeclaration",
|
|
339
|
+
name: decl.value.name,
|
|
340
|
+
declarationKind: decl.value.kind,
|
|
341
|
+
headerEnd,
|
|
342
|
+
consumeTo: headerEnd,
|
|
343
|
+
},
|
|
344
|
+
headerEnd,
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
// export default <expression> (or anonymous function/class)
|
|
348
|
+
return ok({ type: "exportDefaultExpression", headerEnd, consumeTo: headerEnd }, headerEnd);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const fnHead = functionOrClassHead(input, afterExport);
|
|
352
|
+
if (fnHead.ok && fnHead.value.name) {
|
|
353
|
+
return ok(
|
|
354
|
+
{
|
|
355
|
+
type: "exportDeclaration",
|
|
356
|
+
names: [fnHead.value.name],
|
|
357
|
+
declarationKind: fnHead.value.kind,
|
|
358
|
+
headerEnd: afterExport,
|
|
359
|
+
consumeTo: afterExport,
|
|
360
|
+
},
|
|
361
|
+
afterExport,
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const kind = alt(kw("const"), kw("let"), kw("var"))(input, afterExport);
|
|
366
|
+
if (kind.ok) {
|
|
367
|
+
const names = scanDeclaratorNames(input, kind.pos);
|
|
368
|
+
if (names.length > 0) {
|
|
369
|
+
return ok(
|
|
370
|
+
{
|
|
371
|
+
type: "exportDeclaration",
|
|
372
|
+
names,
|
|
373
|
+
declarationKind: "variable",
|
|
374
|
+
headerEnd: afterExport,
|
|
375
|
+
consumeTo: afterExport,
|
|
376
|
+
},
|
|
377
|
+
afterExport,
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return fail("export declaration", afterExport);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// --- Source scanner ---------------------------------------------------------
|
|
386
|
+
|
|
387
|
+
function skipStringLiteral(input, pos) {
|
|
388
|
+
const quote = input[pos];
|
|
389
|
+
let i = pos + 1;
|
|
390
|
+
while (i < input.length) {
|
|
391
|
+
const c = input[i];
|
|
392
|
+
if (c === "\\") i += 2;
|
|
393
|
+
else if (c === quote) return i + 1;
|
|
394
|
+
else if (c === "\n") return i; // unterminated — bail out
|
|
395
|
+
else i++;
|
|
396
|
+
}
|
|
397
|
+
return i;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function skipTemplateLiteral(input, pos) {
|
|
401
|
+
let i = pos + 1;
|
|
402
|
+
while (i < input.length) {
|
|
403
|
+
const c = input[i];
|
|
404
|
+
if (c === "\\") {
|
|
405
|
+
i += 2;
|
|
406
|
+
} else if (c === "`") {
|
|
407
|
+
return i + 1;
|
|
408
|
+
} else if (c === "$" && input[i + 1] === "{") {
|
|
409
|
+
// Skip the embedded expression (may contain nested strings/templates)
|
|
410
|
+
i += 2;
|
|
411
|
+
let depth = 1;
|
|
412
|
+
while (i < input.length && depth > 0) {
|
|
413
|
+
i = skipTrivia(input, i);
|
|
414
|
+
const e = input[i];
|
|
415
|
+
if (e === undefined) break;
|
|
416
|
+
if (e === '"' || e === "'") i = skipStringLiteral(input, i);
|
|
417
|
+
else if (e === "`") i = skipTemplateLiteral(input, i);
|
|
418
|
+
else {
|
|
419
|
+
if (e === "{") depth++;
|
|
420
|
+
else if (e === "}") depth--;
|
|
421
|
+
i++;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
} else {
|
|
425
|
+
i++;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return i;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function skipRegexLiteral(input, pos) {
|
|
432
|
+
let i = pos + 1;
|
|
433
|
+
let inClass = false;
|
|
434
|
+
while (i < input.length) {
|
|
435
|
+
const c = input[i];
|
|
436
|
+
if (c === "\\") i += 2;
|
|
437
|
+
else if (c === "[") { inClass = true; i++; }
|
|
438
|
+
else if (c === "]") { inClass = false; i++; }
|
|
439
|
+
else if (c === "/" && !inClass) {
|
|
440
|
+
i++;
|
|
441
|
+
while (i < input.length && isIdChar(input[i])) i++; // flags
|
|
442
|
+
return i;
|
|
443
|
+
} else if (c === "\n") {
|
|
444
|
+
return i; // not a regex after all — bail out
|
|
445
|
+
} else {
|
|
446
|
+
i++;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return i;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const REGEX_ALLOWED_AFTER_WORD = new Set([
|
|
453
|
+
"return", "typeof", "case", "in", "of", "new", "delete", "void",
|
|
454
|
+
"instanceof", "do", "else", "yield", "await",
|
|
455
|
+
]);
|
|
456
|
+
const REGEX_ALLOWED_AFTER_CHAR = new Set([...("=([{,;:!&|?+-*%^~<>")]);
|
|
457
|
+
// Like the regex set, but without the statement-boundary chars `;` and `{`:
|
|
458
|
+
// after those, `function` opens a *declaration*, not an expression.
|
|
459
|
+
const EXPR_BEFORE_CHAR = new Set([...("=([,:!&|?+-*%^~<>")]);
|
|
460
|
+
|
|
461
|
+
/** True when the previous token puts us in expression position */
|
|
462
|
+
function isExpressionContext(lastToken) {
|
|
463
|
+
return (
|
|
464
|
+
EXPR_BEFORE_CHAR.has(lastToken) ||
|
|
465
|
+
REGEX_ALLOWED_AFTER_WORD.has(lastToken)
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Parse the module syntax of a JavaScript source file.
|
|
471
|
+
* @param {string} source - JavaScript source (post JSX transform)
|
|
472
|
+
* @returns {{imports: object[], exports: object[], topLevelFunctions: string[]}}
|
|
473
|
+
* Each statement carries a `start`/`end` (or `headerEnd`) span into `source`.
|
|
474
|
+
*/
|
|
475
|
+
export function parseModule(source) {
|
|
476
|
+
const imports = [];
|
|
477
|
+
const exportStatements = [];
|
|
478
|
+
const topLevelFunctions = [];
|
|
479
|
+
|
|
480
|
+
let pos = 0;
|
|
481
|
+
let braceDepth = 0;
|
|
482
|
+
let lastToken = ""; // previous significant token (word or single char)
|
|
483
|
+
|
|
484
|
+
while (pos < source.length) {
|
|
485
|
+
pos = skipTrivia(source, pos);
|
|
486
|
+
if (pos >= source.length) break;
|
|
487
|
+
const c = source[pos];
|
|
488
|
+
|
|
489
|
+
if (c === '"' || c === "'") {
|
|
490
|
+
pos = skipStringLiteral(source, pos);
|
|
491
|
+
lastToken = c;
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
if (c === "`") {
|
|
495
|
+
pos = skipTemplateLiteral(source, pos);
|
|
496
|
+
lastToken = c;
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
if (c === "/") {
|
|
500
|
+
// skipTrivia already handled comments, so this is division or a regex
|
|
501
|
+
if (lastToken === "" || REGEX_ALLOWED_AFTER_CHAR.has(lastToken) || REGEX_ALLOWED_AFTER_WORD.has(lastToken)) {
|
|
502
|
+
pos = skipRegexLiteral(source, pos);
|
|
503
|
+
} else {
|
|
504
|
+
pos++;
|
|
505
|
+
}
|
|
506
|
+
lastToken = "/";
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (isIdStart(c)) {
|
|
511
|
+
const start = pos;
|
|
512
|
+
let end = pos + 1;
|
|
513
|
+
while (end < source.length && isIdChar(source[end])) end++;
|
|
514
|
+
const wordText = source.slice(start, end);
|
|
515
|
+
|
|
516
|
+
if (wordText === "import") {
|
|
517
|
+
const result = importDecl(source, start);
|
|
518
|
+
if (result.ok) {
|
|
519
|
+
imports.push({ ...result.value, start, end: result.pos });
|
|
520
|
+
pos = result.pos;
|
|
521
|
+
lastToken = ";";
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
// dynamic import() or import.meta — fall through
|
|
525
|
+
} else if (wordText === "export" && braceDepth === 0) {
|
|
526
|
+
const result = exportDecl(source, start);
|
|
527
|
+
if (result.ok) {
|
|
528
|
+
const { consumeTo, ...statement } = result.value;
|
|
529
|
+
exportStatements.push({ ...statement, start, end: result.pos });
|
|
530
|
+
pos = consumeTo;
|
|
531
|
+
lastToken = ";";
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
} else if (wordText === "function" && braceDepth === 0 && lastToken !== "async" && !isExpressionContext(lastToken)) {
|
|
535
|
+
// A function *declaration* — `const f = function foo() {}` is skipped
|
|
536
|
+
const name = word(source, end);
|
|
537
|
+
if (name.ok) topLevelFunctions.push(name.value);
|
|
538
|
+
} else if (wordText === "async" && braceDepth === 0 && !isExpressionContext(lastToken)) {
|
|
539
|
+
const fn = seq(kw("function"), opt(punct("*")), word)(source, end);
|
|
540
|
+
if (fn.ok) topLevelFunctions.push(fn.value[2]);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
pos = end;
|
|
544
|
+
lastToken = wordText;
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (c === "{") braceDepth++;
|
|
549
|
+
else if (c === "}") braceDepth = Math.max(0, braceDepth - 1);
|
|
550
|
+
lastToken = c;
|
|
551
|
+
pos++;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return { imports, exports: exportStatements, topLevelFunctions };
|
|
555
|
+
}
|
package/src/renderer.js
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Renderer - Convert VNodes to HTML strings
|
|
3
3
|
*/
|
|
4
|
+
import { SELF_CLOSING_TAGS } from "./constants.js";
|
|
5
|
+
import { Fragment } from "./jsx-runtime.js";
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
const SELF_CLOSING_TAGS = new Set([
|
|
7
|
-
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
|
|
8
|
-
'link', 'meta', 'param', 'source', 'track', 'wbr'
|
|
9
|
-
]);
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Escape HTML special characters to prevent XSS
|
|
13
|
-
*/
|
|
7
|
+
/** @param {unknown} text */
|
|
14
8
|
function escapeHtml(text) {
|
|
9
|
+
/** @type {Record<string, string>} */
|
|
15
10
|
const map = {
|
|
16
11
|
'&': '&',
|
|
17
12
|
'<': '<',
|
|
@@ -22,16 +17,12 @@ function escapeHtml(text) {
|
|
|
22
17
|
return String(text).replace(/[&<>"']/g, (char) => map[char]);
|
|
23
18
|
}
|
|
24
19
|
|
|
25
|
-
/**
|
|
26
|
-
* Convert camelCase to kebab-case
|
|
27
|
-
*/
|
|
20
|
+
/** @param {string} str */
|
|
28
21
|
function camelToKebab(str) {
|
|
29
22
|
return str.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
|
|
30
23
|
}
|
|
31
24
|
|
|
32
|
-
/**
|
|
33
|
-
* Convert style object to CSS string
|
|
34
|
-
*/
|
|
25
|
+
/** @param {string | Record<string, unknown>} style */
|
|
35
26
|
function styleToString(style) {
|
|
36
27
|
if (typeof style === 'string') {
|
|
37
28
|
return style;
|
|
@@ -42,40 +33,30 @@ function styleToString(style) {
|
|
|
42
33
|
.join('; ');
|
|
43
34
|
}
|
|
44
35
|
|
|
45
|
-
/**
|
|
46
|
-
* Render attributes to string
|
|
47
|
-
*/
|
|
36
|
+
/** @param {Record<string, any> | null | undefined} props */
|
|
48
37
|
function renderAttributes(props) {
|
|
49
38
|
if (!props) return '';
|
|
50
39
|
|
|
51
40
|
const attributes = [];
|
|
52
41
|
|
|
53
42
|
for (const [key, value] of Object.entries(props)) {
|
|
54
|
-
// Skip special props
|
|
55
43
|
if (key === 'children' || key === 'dangerouslySetInnerHTML') continue;
|
|
56
44
|
|
|
57
|
-
// Handle className -> class conversion
|
|
58
45
|
if (key === 'className') {
|
|
59
46
|
attributes.push(`class="${escapeHtml(value)}"`);
|
|
60
47
|
continue;
|
|
61
48
|
}
|
|
62
49
|
|
|
63
|
-
// Handle style object
|
|
64
50
|
if (key === 'style') {
|
|
65
|
-
|
|
66
|
-
attributes.push(`style="${escapeHtml(styleStr)}"`);
|
|
51
|
+
attributes.push(`style="${escapeHtml(styleToString(value))}"`);
|
|
67
52
|
continue;
|
|
68
53
|
}
|
|
69
54
|
|
|
70
|
-
// Handle boolean attributes
|
|
71
55
|
if (typeof value === 'boolean') {
|
|
72
|
-
if (value)
|
|
73
|
-
attributes.push(key);
|
|
74
|
-
}
|
|
56
|
+
if (value) attributes.push(key);
|
|
75
57
|
continue;
|
|
76
58
|
}
|
|
77
59
|
|
|
78
|
-
// Handle regular attributes
|
|
79
60
|
if (value != null) {
|
|
80
61
|
attributes.push(`${key}="${escapeHtml(value)}"`);
|
|
81
62
|
}
|
|
@@ -85,10 +66,20 @@ function renderAttributes(props) {
|
|
|
85
66
|
}
|
|
86
67
|
|
|
87
68
|
/**
|
|
88
|
-
*
|
|
69
|
+
* @param {any[] | undefined} children
|
|
70
|
+
* @returns {string}
|
|
71
|
+
*/
|
|
72
|
+
function renderChildren(children) {
|
|
73
|
+
return children && children.length > 0
|
|
74
|
+
? children.map(renderToString).join('')
|
|
75
|
+
: '';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @param {any} vnode
|
|
80
|
+
* @returns {string}
|
|
89
81
|
*/
|
|
90
82
|
export function renderToString(vnode) {
|
|
91
|
-
// Handle primitive values
|
|
92
83
|
if (vnode == null || typeof vnode === 'boolean') {
|
|
93
84
|
return '';
|
|
94
85
|
}
|
|
@@ -97,39 +88,29 @@ export function renderToString(vnode) {
|
|
|
97
88
|
return escapeHtml(vnode);
|
|
98
89
|
}
|
|
99
90
|
|
|
100
|
-
// Handle VNode object
|
|
101
91
|
const { tag, props, children } = vnode;
|
|
102
92
|
|
|
103
|
-
|
|
93
|
+
if (tag === Fragment) {
|
|
94
|
+
return renderChildren(children);
|
|
95
|
+
}
|
|
96
|
+
|
|
104
97
|
if (typeof tag === 'function') {
|
|
105
|
-
// Pass props with children
|
|
106
98
|
const componentProps = { ...props };
|
|
107
99
|
if (children && children.length > 0) {
|
|
108
100
|
componentProps.children = children;
|
|
109
101
|
}
|
|
110
|
-
|
|
111
|
-
// Call component function and render result
|
|
112
|
-
const result = tag(componentProps);
|
|
113
|
-
return renderToString(result);
|
|
102
|
+
return renderToString(tag(componentProps));
|
|
114
103
|
}
|
|
115
104
|
|
|
116
|
-
// Handle HTML elements
|
|
117
105
|
const attrs = renderAttributes(props);
|
|
118
|
-
const isSelfClosing = SELF_CLOSING_TAGS.has(tag);
|
|
119
106
|
|
|
120
|
-
if (
|
|
107
|
+
if (SELF_CLOSING_TAGS.has(tag)) {
|
|
121
108
|
return `<${tag}${attrs} />`;
|
|
122
109
|
}
|
|
123
110
|
|
|
124
|
-
|
|
125
|
-
if (props && props.dangerouslySetInnerHTML && props.dangerouslySetInnerHTML.__html) {
|
|
111
|
+
if (props?.dangerouslySetInnerHTML?.__html) {
|
|
126
112
|
return `<${tag}${attrs}>${props.dangerouslySetInnerHTML.__html}</${tag}>`;
|
|
127
113
|
}
|
|
128
114
|
|
|
129
|
-
|
|
130
|
-
const childrenHtml = children && children.length > 0
|
|
131
|
-
? children.map(child => renderToString(child)).join('')
|
|
132
|
-
: '';
|
|
133
|
-
|
|
134
|
-
return `<${tag}${attrs}>${childrenHtml}</${tag}>`;
|
|
115
|
+
return `<${tag}${attrs}>${renderChildren(children)}</${tag}>`;
|
|
135
116
|
}
|