round-core 0.0.3 → 0.0.4
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/.github/workflows/benchmarks.yml +44 -0
- package/README.md +78 -48
- package/benchmarks/apps/react/index.html +9 -0
- package/benchmarks/apps/react/main.jsx +25 -0
- package/benchmarks/apps/react/vite.config.js +12 -0
- package/benchmarks/apps/round/index.html +11 -0
- package/benchmarks/apps/round/main.jsx +22 -0
- package/benchmarks/apps/round/vite.config.js +15 -0
- package/benchmarks/bun.lock +497 -0
- package/benchmarks/dist-bench/react/assets/index-9KGqIPOU.js +8 -0
- package/benchmarks/dist-bench/react/index.html +10 -0
- package/benchmarks/dist-bench/round/assets/index-CBBIRhox.js +52 -0
- package/benchmarks/dist-bench/round/index.html +8 -0
- package/benchmarks/package.json +22 -0
- package/benchmarks/scripts/measure-build.js +64 -0
- package/benchmarks/tests/runtime.bench.js +51 -0
- package/benchmarks/vitest.config.js +8 -0
- package/dist/cli.js +582 -0
- package/dist/index.js +1975 -0
- package/dist/vite-plugin.js +736 -0
- package/package.json +14 -6
- package/src/cli.js +10 -3
- package/src/compiler/vite-plugin.js +5 -1
- package/src/runtime/dom.js +23 -9
- package/src/runtime/lifecycle.js +1 -1
- package/vite.config.build.js +47 -0
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
function transform(code) {
|
|
4
|
+
function parseBlock(str, startIndex) {
|
|
5
|
+
let open = 0;
|
|
6
|
+
let startBlockIndex = -1;
|
|
7
|
+
let endBlockIndex = -1;
|
|
8
|
+
let inSingle = false;
|
|
9
|
+
let inDouble = false;
|
|
10
|
+
let inTemplate = false;
|
|
11
|
+
let inCommentLine = false;
|
|
12
|
+
let inCommentMulti = false;
|
|
13
|
+
for (let i = startIndex; i < str.length; i++) {
|
|
14
|
+
const ch = str[i];
|
|
15
|
+
const prev2 = i > 0 ? str[i - 1] : "";
|
|
16
|
+
const next = i < str.length - 1 ? str[i + 1] : "";
|
|
17
|
+
if (inCommentLine) {
|
|
18
|
+
if (ch === "\n" || ch === "\r") inCommentLine = false;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (inCommentMulti) {
|
|
22
|
+
if (ch === "*" && next === "/") {
|
|
23
|
+
inCommentMulti = false;
|
|
24
|
+
i++;
|
|
25
|
+
}
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (inTemplate) {
|
|
29
|
+
if (ch === "`" && prev2 !== "\\") inTemplate = false;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (inSingle) {
|
|
33
|
+
if (ch === "'" && prev2 !== "\\") inSingle = false;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (inDouble) {
|
|
37
|
+
if (ch === '"' && prev2 !== "\\") inDouble = false;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (ch === "/" && next === "/") {
|
|
41
|
+
inCommentLine = true;
|
|
42
|
+
i++;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (ch === "/" && next === "*") {
|
|
46
|
+
inCommentMulti = true;
|
|
47
|
+
i++;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (ch === "`") {
|
|
51
|
+
inTemplate = true;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (ch === "'") {
|
|
55
|
+
inSingle = true;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (ch === '"') {
|
|
59
|
+
inDouble = true;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (ch === "{") {
|
|
63
|
+
if (open === 0) startBlockIndex = i;
|
|
64
|
+
open++;
|
|
65
|
+
} else if (ch === "}") {
|
|
66
|
+
open--;
|
|
67
|
+
if (open === 0) {
|
|
68
|
+
endBlockIndex = i;
|
|
69
|
+
return { start: startBlockIndex, end: endBlockIndex };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
let result = code;
|
|
76
|
+
function consumeWhitespace(str, i) {
|
|
77
|
+
while (i < str.length && /\s/.test(str[i])) i++;
|
|
78
|
+
return i;
|
|
79
|
+
}
|
|
80
|
+
function parseIfChain(str, ifIndex) {
|
|
81
|
+
const head = str.slice(ifIndex);
|
|
82
|
+
const m = head.match(/^if\s*\((.*?)\)\s*\{/);
|
|
83
|
+
if (!m) return null;
|
|
84
|
+
let i = ifIndex;
|
|
85
|
+
const cases = [];
|
|
86
|
+
let elseContent = null;
|
|
87
|
+
while (true) {
|
|
88
|
+
const cur = str.slice(i);
|
|
89
|
+
const mm = cur.match(/^if\s*\((.*?)\)\s*\{/);
|
|
90
|
+
if (!mm) return null;
|
|
91
|
+
let cond = mm[1];
|
|
92
|
+
const trimmedCond = String(cond).trim();
|
|
93
|
+
const isSimplePath = /^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*)*$/.test(trimmedCond);
|
|
94
|
+
if (isSimplePath && !trimmedCond.endsWith(")")) {
|
|
95
|
+
cond = `((typeof (${trimmedCond}) === 'function' && typeof (${trimmedCond}).peek === 'function' && ('value' in (${trimmedCond}))) ? (${trimmedCond})() : (${trimmedCond}))`;
|
|
96
|
+
}
|
|
97
|
+
const blockStart = i + mm[0].length - 1;
|
|
98
|
+
const block = parseBlock(str, blockStart);
|
|
99
|
+
if (!block) return null;
|
|
100
|
+
const content = str.substring(block.start + 1, block.end);
|
|
101
|
+
cases.push({ cond, content });
|
|
102
|
+
i = block.end + 1;
|
|
103
|
+
i = consumeWhitespace(str, i);
|
|
104
|
+
if (!str.startsWith("else", i)) {
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
i += 4;
|
|
108
|
+
i = consumeWhitespace(str, i);
|
|
109
|
+
if (str.startsWith("if", i)) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (str[i] !== "{") return null;
|
|
113
|
+
const elseBlock = parseBlock(str, i);
|
|
114
|
+
if (!elseBlock) return null;
|
|
115
|
+
elseContent = str.substring(elseBlock.start + 1, elseBlock.end);
|
|
116
|
+
i = elseBlock.end + 1;
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
const end = i;
|
|
120
|
+
let expr = "";
|
|
121
|
+
for (let idx = 0; idx < cases.length; idx++) {
|
|
122
|
+
const c = cases[idx];
|
|
123
|
+
const body = `<Fragment>${c.content}</Fragment>`;
|
|
124
|
+
if (idx === 0) {
|
|
125
|
+
expr = `(${c.cond}) ? (${body}) : `;
|
|
126
|
+
} else {
|
|
127
|
+
expr += `(${c.cond}) ? (${body}) : `;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (elseContent !== null) {
|
|
131
|
+
expr += `(<Fragment>${elseContent}</Fragment>)`;
|
|
132
|
+
} else {
|
|
133
|
+
expr += "null";
|
|
134
|
+
}
|
|
135
|
+
const replacement = `(() => ${expr})`;
|
|
136
|
+
return { start: ifIndex, end, replacement };
|
|
137
|
+
}
|
|
138
|
+
function parseIfStatement(str, ifIndex) {
|
|
139
|
+
if (!str.startsWith("if", ifIndex)) return null;
|
|
140
|
+
const chain = parseIfChain(str, ifIndex);
|
|
141
|
+
if (!chain) return null;
|
|
142
|
+
return {
|
|
143
|
+
start: chain.start,
|
|
144
|
+
end: chain.end,
|
|
145
|
+
replacement: `{${chain.replacement}}`
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function parseIfExpression(str, exprStart) {
|
|
149
|
+
if (str[exprStart] !== "{") return null;
|
|
150
|
+
let i = consumeWhitespace(str, exprStart + 1);
|
|
151
|
+
if (!str.startsWith("if", i)) return null;
|
|
152
|
+
const outer = parseBlock(str, exprStart);
|
|
153
|
+
if (!outer) return null;
|
|
154
|
+
const chain = parseIfChain(str, i);
|
|
155
|
+
if (!chain) return null;
|
|
156
|
+
return {
|
|
157
|
+
start: exprStart,
|
|
158
|
+
end: outer.end + 1,
|
|
159
|
+
replacement: `{${chain.replacement}}`
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
let prev = null;
|
|
163
|
+
while (prev !== result) {
|
|
164
|
+
prev = result;
|
|
165
|
+
while (true) {
|
|
166
|
+
const match = result.match(/\{\s*if\s*\(/);
|
|
167
|
+
if (!match) break;
|
|
168
|
+
const matchIndex = match.index;
|
|
169
|
+
const parsed = parseIfExpression(result, matchIndex);
|
|
170
|
+
if (!parsed) {
|
|
171
|
+
console.warn("Unbalanced IF expression found, skipping transformation.");
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
const before = result.substring(0, parsed.start);
|
|
175
|
+
const after = result.substring(parsed.end);
|
|
176
|
+
result = before + parsed.replacement + after;
|
|
177
|
+
}
|
|
178
|
+
while (true) {
|
|
179
|
+
const match = result.match(/(^|[\n\r])\s*if\s*\(/m);
|
|
180
|
+
if (!match) break;
|
|
181
|
+
const ifIndex = match.index + match[0].lastIndexOf("if");
|
|
182
|
+
const parsed = parseIfStatement(result, ifIndex);
|
|
183
|
+
if (!parsed) break;
|
|
184
|
+
const before = result.substring(0, parsed.start);
|
|
185
|
+
const after = result.substring(parsed.end);
|
|
186
|
+
result = before + parsed.replacement + after;
|
|
187
|
+
}
|
|
188
|
+
while (true) {
|
|
189
|
+
const match = result.match(/\{\s*for\s*\((.*?)\s+in\s+(.*?)\)\s*\{/);
|
|
190
|
+
if (!match) break;
|
|
191
|
+
const item = match[1];
|
|
192
|
+
const list = match[2];
|
|
193
|
+
const exprStart = match.index;
|
|
194
|
+
const outer = parseBlock(result, exprStart);
|
|
195
|
+
if (!outer) break;
|
|
196
|
+
let i = consumeWhitespace(result, exprStart + 1);
|
|
197
|
+
const head = result.slice(i);
|
|
198
|
+
const mm = head.match(/^for\s*\((.*?)\s+in\s+(.*?)\)\s*\{/);
|
|
199
|
+
if (!mm) break;
|
|
200
|
+
const forStart = i;
|
|
201
|
+
const blockStart = forStart + mm[0].length - 1;
|
|
202
|
+
const block = parseBlock(result, blockStart);
|
|
203
|
+
if (!block) break;
|
|
204
|
+
const content = result.substring(block.start + 1, block.end);
|
|
205
|
+
const replacement = `{${list}.map(${item} => (<Fragment>${content}</Fragment>))}`;
|
|
206
|
+
const before = result.substring(0, exprStart);
|
|
207
|
+
const after = result.substring(outer.end + 1);
|
|
208
|
+
result = before + replacement + after;
|
|
209
|
+
}
|
|
210
|
+
while (true) {
|
|
211
|
+
const match = result.match(/(^|[\n\r])\s*for\s*\((.*?)\s+in\s+(.*?)\)\s*\{/m);
|
|
212
|
+
if (!match) break;
|
|
213
|
+
const exprStart = match.index + match[0].lastIndexOf("for");
|
|
214
|
+
const item = match[2];
|
|
215
|
+
const list = match[3];
|
|
216
|
+
const forHead = result.slice(exprStart);
|
|
217
|
+
const mm = forHead.match(/^for\s*\((.*?)\s+in\s+(.*?)\)\s*\{/);
|
|
218
|
+
if (!mm) break;
|
|
219
|
+
const blockStart = exprStart + mm[0].length - 1;
|
|
220
|
+
const block = parseBlock(result, blockStart);
|
|
221
|
+
if (!block) break;
|
|
222
|
+
const content = result.substring(block.start + 1, block.end);
|
|
223
|
+
const replacement = `{${list}.map(${item} => (<Fragment>${content}</Fragment>))}`;
|
|
224
|
+
const before = result.substring(0, exprStart);
|
|
225
|
+
const after = result.substring(block.end + 1);
|
|
226
|
+
result = before + replacement + after;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function findJsxTagEnd(str, startIndex) {
|
|
230
|
+
let inSingle = false;
|
|
231
|
+
let inDouble = false;
|
|
232
|
+
let inTemplate = false;
|
|
233
|
+
let braceDepth = 0;
|
|
234
|
+
for (let i = startIndex; i < str.length; i++) {
|
|
235
|
+
const ch = str[i];
|
|
236
|
+
const prevCh = i > 0 ? str[i - 1] : "";
|
|
237
|
+
if (!inDouble && !inTemplate && ch === "'" && prevCh !== "\\") inSingle = !inSingle;
|
|
238
|
+
else if (!inSingle && !inTemplate && ch === '"' && prevCh !== "\\") inDouble = !inDouble;
|
|
239
|
+
else if (!inSingle && !inDouble && ch === "`" && prevCh !== "\\") inTemplate = !inTemplate;
|
|
240
|
+
if (inSingle || inDouble || inTemplate) continue;
|
|
241
|
+
if (ch === "{") braceDepth++;
|
|
242
|
+
else if (ch === "}") braceDepth = Math.max(0, braceDepth - 1);
|
|
243
|
+
else if (ch === ">" && braceDepth === 0) return i;
|
|
244
|
+
}
|
|
245
|
+
return -1;
|
|
246
|
+
}
|
|
247
|
+
function transformSuspenseBlocks(str) {
|
|
248
|
+
let out = str;
|
|
249
|
+
let cursor = 0;
|
|
250
|
+
while (true) {
|
|
251
|
+
const openIndex = out.indexOf("<Suspense", cursor);
|
|
252
|
+
if (openIndex === -1) break;
|
|
253
|
+
const openEnd = findJsxTagEnd(out, openIndex);
|
|
254
|
+
if (openEnd === -1) break;
|
|
255
|
+
const openTagText = out.slice(openIndex, openEnd + 1);
|
|
256
|
+
if (/\/>\s*$/.test(openTagText)) {
|
|
257
|
+
cursor = openEnd + 1;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
let depth = 1;
|
|
261
|
+
let i = openEnd + 1;
|
|
262
|
+
let closeStart = -1;
|
|
263
|
+
while (i < out.length) {
|
|
264
|
+
const nextOpen = out.indexOf("<Suspense", i);
|
|
265
|
+
const nextClose = out.indexOf("</Suspense>", i);
|
|
266
|
+
if (nextClose === -1) break;
|
|
267
|
+
if (nextOpen !== -1 && nextOpen < nextClose) {
|
|
268
|
+
const innerOpenEnd = findJsxTagEnd(out, nextOpen);
|
|
269
|
+
if (innerOpenEnd === -1) break;
|
|
270
|
+
const innerOpenText = out.slice(nextOpen, innerOpenEnd + 1);
|
|
271
|
+
if (!/\/>\s*$/.test(innerOpenText)) depth++;
|
|
272
|
+
i = innerOpenEnd + 1;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
depth--;
|
|
276
|
+
if (depth === 0) {
|
|
277
|
+
closeStart = nextClose;
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
i = nextClose + "</Suspense>".length;
|
|
281
|
+
}
|
|
282
|
+
if (closeStart === -1) break;
|
|
283
|
+
const inner = out.slice(openEnd + 1, closeStart);
|
|
284
|
+
const innerTrim = inner.trim();
|
|
285
|
+
if (innerTrim.startsWith("{() =>")) {
|
|
286
|
+
cursor = closeStart + "</Suspense>".length;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
const wrapped = `{() => (<Fragment>${inner}</Fragment>)}`;
|
|
290
|
+
out = out.slice(0, openEnd + 1) + wrapped + out.slice(closeStart);
|
|
291
|
+
cursor = closeStart + wrapped.length + "</Suspense>".length;
|
|
292
|
+
}
|
|
293
|
+
return out;
|
|
294
|
+
}
|
|
295
|
+
function transformProviderBlocks(str) {
|
|
296
|
+
let out = str;
|
|
297
|
+
let cursor = 0;
|
|
298
|
+
while (true) {
|
|
299
|
+
const dot = out.indexOf(".Provider", cursor);
|
|
300
|
+
if (dot === -1) break;
|
|
301
|
+
const lt = out.lastIndexOf("<", dot);
|
|
302
|
+
if (lt === -1) break;
|
|
303
|
+
const openEnd = findJsxTagEnd(out, lt);
|
|
304
|
+
if (openEnd === -1) break;
|
|
305
|
+
const openTagText = out.slice(lt, openEnd + 1);
|
|
306
|
+
if (/\/>\s*$/.test(openTagText)) {
|
|
307
|
+
cursor = openEnd + 1;
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
const m = openTagText.match(/^<\s*([A-Za-z_$][\w$]*\.Provider)\b/);
|
|
311
|
+
if (!m) {
|
|
312
|
+
cursor = openEnd + 1;
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
const tagName = m[1];
|
|
316
|
+
const closeTag = `</${tagName}>`;
|
|
317
|
+
let depth = 1;
|
|
318
|
+
let i = openEnd + 1;
|
|
319
|
+
let closeStart = -1;
|
|
320
|
+
while (i < out.length) {
|
|
321
|
+
const nextOpen = out.indexOf(`<${tagName}`, i);
|
|
322
|
+
const nextClose = out.indexOf(closeTag, i);
|
|
323
|
+
if (nextClose === -1) break;
|
|
324
|
+
if (nextOpen !== -1 && nextOpen < nextClose) {
|
|
325
|
+
const innerOpenEnd = findJsxTagEnd(out, nextOpen);
|
|
326
|
+
if (innerOpenEnd === -1) break;
|
|
327
|
+
const innerOpenText = out.slice(nextOpen, innerOpenEnd + 1);
|
|
328
|
+
if (!/\/>\s*$/.test(innerOpenText)) depth++;
|
|
329
|
+
i = innerOpenEnd + 1;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
depth--;
|
|
333
|
+
if (depth === 0) {
|
|
334
|
+
closeStart = nextClose;
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
i = nextClose + closeTag.length;
|
|
338
|
+
}
|
|
339
|
+
if (closeStart === -1) break;
|
|
340
|
+
const inner = out.slice(openEnd + 1, closeStart);
|
|
341
|
+
const innerTrim = inner.trim();
|
|
342
|
+
if (innerTrim.startsWith("{() =>")) {
|
|
343
|
+
cursor = closeStart + closeTag.length;
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
const wrapped = `{() => (<Fragment>${inner}</Fragment>)}`;
|
|
347
|
+
out = out.slice(0, openEnd + 1) + wrapped + out.slice(closeStart);
|
|
348
|
+
cursor = closeStart + wrapped.length + closeTag.length;
|
|
349
|
+
}
|
|
350
|
+
return out;
|
|
351
|
+
}
|
|
352
|
+
result = transformSuspenseBlocks(result);
|
|
353
|
+
result = transformProviderBlocks(result);
|
|
354
|
+
result = result.replace(/\{\s*([A-Za-z_$][\w$]*)\s*\(\s*\)\s*\}/g, "{() => $1()}").replace(/=\{\s*([A-Za-z_$][\w$]*)\s*\(\s*\)\s*\}/g, "={() => $1()}");
|
|
355
|
+
return result;
|
|
356
|
+
}
|
|
357
|
+
function normalizePath(p) {
|
|
358
|
+
return p.replaceAll("\\", "/");
|
|
359
|
+
}
|
|
360
|
+
function isMdRawRequest(id) {
|
|
361
|
+
return typeof id === "string" && id.includes(".md") && id.includes("?raw");
|
|
362
|
+
}
|
|
363
|
+
function stripQuery(id) {
|
|
364
|
+
return id.split("?")[0];
|
|
365
|
+
}
|
|
366
|
+
function escapeForJsString(s) {
|
|
367
|
+
return String(s).replaceAll("\\", "\\\\").replaceAll("`", "\\`").replaceAll("${", "\\${");
|
|
368
|
+
}
|
|
369
|
+
function resolveMaybeRelative(baseDir, p) {
|
|
370
|
+
if (!p) return null;
|
|
371
|
+
if (path.isAbsolute(p)) return p;
|
|
372
|
+
return path.resolve(baseDir, p);
|
|
373
|
+
}
|
|
374
|
+
function inlineMarkdownInRound(code, fileAbs, addWatchFile) {
|
|
375
|
+
if (typeof code !== "string" || typeof fileAbs !== "string") return code;
|
|
376
|
+
const dir = path.dirname(fileAbs);
|
|
377
|
+
const re = /<Markdown\b([^>]*?)\bsrc\s*=\s*("([^"]+)"|'([^']+)')([^>]*)\/>/g;
|
|
378
|
+
return code.replace(re, (full, beforeAttrs, _q, dbl, sgl, afterAttrs) => {
|
|
379
|
+
const src = dbl ?? sgl;
|
|
380
|
+
if (!src || typeof src !== "string") return full;
|
|
381
|
+
if (!src.startsWith("./") && !src.startsWith("../")) return full;
|
|
382
|
+
const mdAbs = path.resolve(dir, src);
|
|
383
|
+
try {
|
|
384
|
+
const raw = fs.readFileSync(mdAbs, "utf8");
|
|
385
|
+
if (typeof addWatchFile === "function") {
|
|
386
|
+
try {
|
|
387
|
+
addWatchFile(mdAbs);
|
|
388
|
+
} catch {
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
const content = escapeForJsString(raw);
|
|
392
|
+
const rebuilt = `<Markdown${beforeAttrs}content={\`${content}\`} ${afterAttrs} />`;
|
|
393
|
+
return rebuilt.replace(/\s+\/>$/, " />");
|
|
394
|
+
} catch (e) {
|
|
395
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
396
|
+
throw new Error(`Markdown file not found: ${src} (resolved: ${mdAbs})
|
|
397
|
+
${msg}`);
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
function isExcluded(fileAbsPath, excludeAbs) {
|
|
402
|
+
const file = normalizePath(fileAbsPath);
|
|
403
|
+
for (const pat of excludeAbs) {
|
|
404
|
+
const patNorm = normalizePath(pat);
|
|
405
|
+
const prefix = patNorm.endsWith("/**") ? patNorm.slice(0, -3) : patNorm;
|
|
406
|
+
if (file.startsWith(prefix)) return true;
|
|
407
|
+
}
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
function isIncluded(fileAbsPath, includeAbs) {
|
|
411
|
+
if (!includeAbs.length) return true;
|
|
412
|
+
const file = normalizePath(fileAbsPath);
|
|
413
|
+
for (const pat of includeAbs) {
|
|
414
|
+
const patNorm = normalizePath(pat);
|
|
415
|
+
const prefix = patNorm.endsWith("/**") ? patNorm.slice(0, -3) : patNorm;
|
|
416
|
+
if (file.startsWith(prefix)) return true;
|
|
417
|
+
}
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
function RoundPlugin(pluginOptions = {}) {
|
|
421
|
+
const state = {
|
|
422
|
+
rootDir: process.cwd(),
|
|
423
|
+
includeAbs: [],
|
|
424
|
+
excludeAbs: [],
|
|
425
|
+
configLoaded: false,
|
|
426
|
+
routingTrailingSlash: true,
|
|
427
|
+
configPathAbs: null,
|
|
428
|
+
configDir: null,
|
|
429
|
+
entryAbs: null,
|
|
430
|
+
startHead: null,
|
|
431
|
+
startHeadHtml: null
|
|
432
|
+
};
|
|
433
|
+
let lastRuntimeErrorKey = null;
|
|
434
|
+
let lastRuntimeErrorAt = 0;
|
|
435
|
+
const runtimeImport = pluginOptions.runtimeImport ?? "round-core";
|
|
436
|
+
const restartOnConfigChange = pluginOptions.restartOnConfigChange !== void 0 ? Boolean(pluginOptions.restartOnConfigChange) : true;
|
|
437
|
+
function loadConfigOnce(rootDir) {
|
|
438
|
+
if (state.configLoaded) return;
|
|
439
|
+
state.configLoaded = true;
|
|
440
|
+
const configPath = pluginOptions.configPath ? resolveMaybeRelative(rootDir, pluginOptions.configPath) : resolveMaybeRelative(rootDir, "./round.config.json");
|
|
441
|
+
state.configPathAbs = configPath;
|
|
442
|
+
const configDir = configPath ? path.dirname(configPath) : rootDir;
|
|
443
|
+
state.configDir = configDir;
|
|
444
|
+
let config = null;
|
|
445
|
+
if (configPath && fs.existsSync(configPath)) {
|
|
446
|
+
try {
|
|
447
|
+
const raw = fs.readFileSync(configPath, "utf8");
|
|
448
|
+
config = JSON.parse(raw);
|
|
449
|
+
} catch {
|
|
450
|
+
config = null;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
const trailingSlash = config?.routing?.trailingSlash;
|
|
454
|
+
state.routingTrailingSlash = trailingSlash !== void 0 ? Boolean(trailingSlash) : true;
|
|
455
|
+
const customTags = config?.htmlTags;
|
|
456
|
+
state.customTags = Array.isArray(customTags) ? customTags : [];
|
|
457
|
+
const entryRel = config?.entry;
|
|
458
|
+
state.entryAbs = entryRel ? resolveMaybeRelative(configDir, entryRel) : null;
|
|
459
|
+
const include = pluginOptions.include ?? config?.include ?? [];
|
|
460
|
+
const exclude = pluginOptions.exclude ?? config?.exclude ?? ["./node_modules", "./dist"];
|
|
461
|
+
const includeBase = pluginOptions.include ? rootDir : configDir;
|
|
462
|
+
const excludeBase = pluginOptions.exclude ? rootDir : configDir;
|
|
463
|
+
state.includeAbs = Array.isArray(include) ? include.map((p) => resolveMaybeRelative(includeBase, p)).filter(Boolean) : [];
|
|
464
|
+
state.excludeAbs = Array.isArray(exclude) ? exclude.map((p) => resolveMaybeRelative(excludeBase, p)).filter(Boolean) : [];
|
|
465
|
+
}
|
|
466
|
+
function findBlock(str, startIndex) {
|
|
467
|
+
let open = 0;
|
|
468
|
+
let inSingle = false;
|
|
469
|
+
let inDouble = false;
|
|
470
|
+
let inTemplate = false;
|
|
471
|
+
let start = -1;
|
|
472
|
+
for (let i = startIndex; i < str.length; i++) {
|
|
473
|
+
const ch = str[i];
|
|
474
|
+
const prev = i > 0 ? str[i - 1] : "";
|
|
475
|
+
if (!inDouble && !inTemplate && ch === "'" && prev !== "\\") inSingle = !inSingle;
|
|
476
|
+
else if (!inSingle && !inTemplate && ch === '"' && prev !== "\\") inDouble = !inDouble;
|
|
477
|
+
else if (!inSingle && !inDouble && ch === "`" && prev !== "\\") inTemplate = !inTemplate;
|
|
478
|
+
if (inSingle || inDouble || inTemplate) continue;
|
|
479
|
+
if (ch === "{") {
|
|
480
|
+
if (open === 0) start = i;
|
|
481
|
+
open++;
|
|
482
|
+
} else if (ch === "}") {
|
|
483
|
+
open--;
|
|
484
|
+
if (open === 0 && start !== -1) {
|
|
485
|
+
return { start, end: i };
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
function parseStartHeadCallArgument(str, fromIndex) {
|
|
492
|
+
const idx = str.indexOf("startHead", fromIndex);
|
|
493
|
+
if (idx === -1) return null;
|
|
494
|
+
const callIdx = str.indexOf("(", idx);
|
|
495
|
+
if (callIdx === -1) return null;
|
|
496
|
+
let i = callIdx;
|
|
497
|
+
let paren = 0;
|
|
498
|
+
let inSingle = false;
|
|
499
|
+
let inDouble = false;
|
|
500
|
+
let inTemplate = false;
|
|
501
|
+
for (; i < str.length; i++) {
|
|
502
|
+
const ch = str[i];
|
|
503
|
+
const prev = i > 0 ? str[i - 1] : "";
|
|
504
|
+
if (!inDouble && !inTemplate && ch === "'" && prev !== "\\") inSingle = !inSingle;
|
|
505
|
+
else if (!inSingle && !inTemplate && ch === '"' && prev !== "\\") inDouble = !inDouble;
|
|
506
|
+
else if (!inSingle && !inDouble && ch === "`" && prev !== "\\") inTemplate = !inTemplate;
|
|
507
|
+
if (inSingle || inDouble || inTemplate) continue;
|
|
508
|
+
if (ch === "(") paren++;
|
|
509
|
+
else if (ch === ")") {
|
|
510
|
+
paren--;
|
|
511
|
+
if (paren === 0) {
|
|
512
|
+
const inner = str.slice(callIdx + 1, i).trim();
|
|
513
|
+
return { arg: inner, start: idx, end: i + 1 };
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
function parseStartHeadInDefaultExport(code) {
|
|
520
|
+
const m = code.match(/export\s+default\s+function\b/);
|
|
521
|
+
const hasAnyCall = /\bstartHead\s*\(/.test(code);
|
|
522
|
+
if (!m || typeof m.index !== "number") return { headExpr: null, hasAny: hasAnyCall };
|
|
523
|
+
const fnStart = m.index;
|
|
524
|
+
const braceIdx = code.indexOf("{", fnStart);
|
|
525
|
+
if (braceIdx === -1) return { headExpr: null, hasAny: hasAnyCall };
|
|
526
|
+
const block = findBlock(code, braceIdx);
|
|
527
|
+
if (!block) return { headExpr: null, hasAny: hasAnyCall };
|
|
528
|
+
const body = code.slice(block.start + 1, block.end);
|
|
529
|
+
const call = parseStartHeadCallArgument(body, 0);
|
|
530
|
+
return { headExpr: call ? call.arg : null, hasAny: hasAnyCall, hasOutside: hasAnyCall && !call };
|
|
531
|
+
}
|
|
532
|
+
function headToHtml(head) {
|
|
533
|
+
if (!head || typeof head !== "object") return "";
|
|
534
|
+
let out = "";
|
|
535
|
+
if (typeof head.title === "string") {
|
|
536
|
+
out += `
|
|
537
|
+
<title>${head.title}</title>`;
|
|
538
|
+
}
|
|
539
|
+
const meta = head.meta;
|
|
540
|
+
const links = head.links;
|
|
541
|
+
const renderAttrs = (attrs) => {
|
|
542
|
+
if (!attrs || typeof attrs !== "object") return "";
|
|
543
|
+
return Object.entries(attrs).filter(([, v]) => v !== null && v !== void 0).map(([k, v]) => ` ${k}="${String(v).replaceAll('"', """)}"`).join("");
|
|
544
|
+
};
|
|
545
|
+
if (Array.isArray(meta)) {
|
|
546
|
+
meta.forEach((m) => {
|
|
547
|
+
if (!m) return;
|
|
548
|
+
if (Array.isArray(m) && m.length >= 2) {
|
|
549
|
+
out += `
|
|
550
|
+
<meta name="${String(m[0]).replaceAll('"', """)}" content="${String(m[1] ?? "").replaceAll('"', """)}">`;
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
if (typeof m === "object") {
|
|
554
|
+
out += `
|
|
555
|
+
<meta${renderAttrs(m)}>`;
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
} else if (meta && typeof meta === "object") {
|
|
559
|
+
Object.entries(meta).forEach(([name, content]) => {
|
|
560
|
+
out += `
|
|
561
|
+
<meta name="${String(name).replaceAll('"', """)}" content="${String(content ?? "").replaceAll('"', """)}">`;
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
if (Array.isArray(links)) {
|
|
565
|
+
links.forEach((l) => {
|
|
566
|
+
if (!l || typeof l !== "object") return;
|
|
567
|
+
out += `
|
|
568
|
+
<link${renderAttrs(l)}>`;
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
if (typeof head.raw === "string" && head.raw.trim()) {
|
|
572
|
+
out += `
|
|
573
|
+
${head.raw}`;
|
|
574
|
+
}
|
|
575
|
+
return out;
|
|
576
|
+
}
|
|
577
|
+
return {
|
|
578
|
+
name: "vite-plugin-round",
|
|
579
|
+
enforce: "pre",
|
|
580
|
+
transformIndexHtml(html) {
|
|
581
|
+
if (!state.startHeadHtml) return html;
|
|
582
|
+
if (!html.includes("</head>")) return html;
|
|
583
|
+
let next = html;
|
|
584
|
+
if (state.startHead && typeof state.startHead.title === "string") {
|
|
585
|
+
next = next.replace(/<title>[\s\S]*?<\/title>/i, "");
|
|
586
|
+
}
|
|
587
|
+
return next.replace("</head>", `${state.startHeadHtml}
|
|
588
|
+
</head>`);
|
|
589
|
+
},
|
|
590
|
+
config(userConfig, env) {
|
|
591
|
+
const rootDir = path.resolve(process.cwd(), userConfig.root ?? ".");
|
|
592
|
+
state.rootDir = rootDir;
|
|
593
|
+
loadConfigOnce(rootDir);
|
|
594
|
+
return {
|
|
595
|
+
define: {
|
|
596
|
+
__ROUND_ROUTING_TRAILING_SLASH__: JSON.stringify(state.routingTrailingSlash),
|
|
597
|
+
__ROUND_CUSTOM_TAGS__: JSON.stringify(state.customTags ?? [])
|
|
598
|
+
},
|
|
599
|
+
esbuild: {
|
|
600
|
+
include: /\.(round|js|jsx|ts|tsx)$/,
|
|
601
|
+
loader: "jsx",
|
|
602
|
+
jsxFactory: "createElement",
|
|
603
|
+
jsxFragment: "Fragment"
|
|
604
|
+
// NOTE: Inject the runtime import in transform() to avoid
|
|
605
|
+
},
|
|
606
|
+
// Ensure .round files are treated as JS/JSX
|
|
607
|
+
resolve: {
|
|
608
|
+
extensions: [".mjs", ".js", ".ts", ".jsx", ".tsx", ".json", ".round"]
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
},
|
|
612
|
+
load(id) {
|
|
613
|
+
if (!isMdRawRequest(id)) return;
|
|
614
|
+
const fileAbs = stripQuery(id);
|
|
615
|
+
try {
|
|
616
|
+
const raw = fs.readFileSync(fileAbs, "utf8");
|
|
617
|
+
this.addWatchFile(fileAbs);
|
|
618
|
+
return `export default \`${escapeForJsString(raw)}\`;`;
|
|
619
|
+
} catch {
|
|
620
|
+
this.addWatchFile(fileAbs);
|
|
621
|
+
return "export default ``;";
|
|
622
|
+
}
|
|
623
|
+
},
|
|
624
|
+
configureServer(server) {
|
|
625
|
+
loadConfigOnce(server.config.root ?? process.cwd());
|
|
626
|
+
if (state.configPathAbs) {
|
|
627
|
+
server.watcher.add(state.configPathAbs);
|
|
628
|
+
}
|
|
629
|
+
server.middlewares.use((req, res, next) => {
|
|
630
|
+
if (!req.url) return next();
|
|
631
|
+
const [urlPath] = req.url.split("?");
|
|
632
|
+
if (urlPath && urlPath.endsWith(".md")) {
|
|
633
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
634
|
+
}
|
|
635
|
+
next();
|
|
636
|
+
});
|
|
637
|
+
server.ws.on("round:runtime-error", (payload = {}) => {
|
|
638
|
+
try {
|
|
639
|
+
const message = typeof payload.message === "string" ? payload.message : "Runtime error";
|
|
640
|
+
const phase = typeof payload.phase === "string" && payload.phase ? ` (${payload.phase})` : "";
|
|
641
|
+
const component = typeof payload.component === "string" && payload.component ? ` in ${payload.component}` : "";
|
|
642
|
+
const header = `[round] Runtime error${component}${phase}: ${message}`;
|
|
643
|
+
const stack = payload.stack ? String(payload.stack) : "";
|
|
644
|
+
const key = `${header}
|
|
645
|
+
${stack}`;
|
|
646
|
+
const now = Date.now();
|
|
647
|
+
if (lastRuntimeErrorKey === key && now - lastRuntimeErrorAt < 2e3) return;
|
|
648
|
+
lastRuntimeErrorKey = key;
|
|
649
|
+
lastRuntimeErrorAt = now;
|
|
650
|
+
server.config.logger.error(header);
|
|
651
|
+
if (stack) server.config.logger.error(stack);
|
|
652
|
+
} catch {
|
|
653
|
+
server.config.logger.error("[round] Runtime error");
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
},
|
|
657
|
+
handleHotUpdate(ctx) {
|
|
658
|
+
if (state.configPathAbs && ctx.file === state.configPathAbs) {
|
|
659
|
+
if (!restartOnConfigChange) return [];
|
|
660
|
+
try {
|
|
661
|
+
if (typeof ctx.server.restart === "function") {
|
|
662
|
+
ctx.server.restart();
|
|
663
|
+
} else {
|
|
664
|
+
ctx.server.ws.send({ type: "full-reload" });
|
|
665
|
+
}
|
|
666
|
+
} catch {
|
|
667
|
+
ctx.server.ws.send({ type: "full-reload" });
|
|
668
|
+
}
|
|
669
|
+
return [];
|
|
670
|
+
}
|
|
671
|
+
},
|
|
672
|
+
configurePreviewServer(server) {
|
|
673
|
+
server.middlewares.use((req, res, next) => {
|
|
674
|
+
if (!req.url) return next();
|
|
675
|
+
const [urlPath] = req.url.split("?");
|
|
676
|
+
if (urlPath && urlPath.endsWith(".md")) {
|
|
677
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
678
|
+
}
|
|
679
|
+
next();
|
|
680
|
+
});
|
|
681
|
+
},
|
|
682
|
+
transform(code, id) {
|
|
683
|
+
if (id.endsWith(".round")) {
|
|
684
|
+
const fileAbs = path.isAbsolute(id) ? id : path.resolve(state.rootDir, id);
|
|
685
|
+
if (!isIncluded(fileAbs, state.includeAbs)) return;
|
|
686
|
+
if (isExcluded(fileAbs, state.excludeAbs)) return;
|
|
687
|
+
const isEntry = state.entryAbs && normalizePath(fileAbs) === normalizePath(state.entryAbs);
|
|
688
|
+
const parsedHead = parseStartHeadInDefaultExport(code);
|
|
689
|
+
if (parsedHead.hasAny && !isEntry) {
|
|
690
|
+
this.error(new Error(`startHead() can only be used in the entry module's export default function: ${state.entryAbs ?? "(unknown entry)"}
|
|
691
|
+
Found in: ${fileAbs}`));
|
|
692
|
+
}
|
|
693
|
+
if (isEntry && parsedHead.hasOutside) {
|
|
694
|
+
this.error(new Error(`startHead() must be called inside the entry module's export default function body (not at top-level).
|
|
695
|
+
Entry: ${fileAbs}`));
|
|
696
|
+
}
|
|
697
|
+
if (isEntry && parsedHead.headExpr) {
|
|
698
|
+
const trimmed = parsedHead.headExpr.trim();
|
|
699
|
+
if (!trimmed.startsWith("{")) {
|
|
700
|
+
this.error(new Error(`startHead(...) expects an object literal. Example: startHead({ title: 'Home' })
|
|
701
|
+
Found: ${trimmed.slice(0, 60)}...`));
|
|
702
|
+
}
|
|
703
|
+
if (/\bfunction\b|=>|\bimport\b|\brequire\b|\bprocess\b|\bglobal\b/.test(trimmed)) {
|
|
704
|
+
this.error(new Error("startHead object must be static data (no functions/imports)."));
|
|
705
|
+
}
|
|
706
|
+
let headObj = null;
|
|
707
|
+
try {
|
|
708
|
+
headObj = Function(`"use strict"; return (${trimmed});`)();
|
|
709
|
+
} catch (e) {
|
|
710
|
+
this.error(new Error(`Failed to parse startHead(...) object in ${fileAbs}: ${String(e?.message ?? e)}`));
|
|
711
|
+
}
|
|
712
|
+
state.startHead = headObj;
|
|
713
|
+
state.startHeadHtml = headToHtml(headObj);
|
|
714
|
+
}
|
|
715
|
+
let nextCode = code;
|
|
716
|
+
try {
|
|
717
|
+
nextCode = inlineMarkdownInRound(nextCode, fileAbs, (p) => this.addWatchFile(p));
|
|
718
|
+
} catch (e) {
|
|
719
|
+
this.error(e);
|
|
720
|
+
}
|
|
721
|
+
let transformedCode = transform(nextCode);
|
|
722
|
+
if (!/^\s*import\s+\{\s*createElement\s*,\s*Fragment\s*\}\s+from\s+['"][^'"]+['"];?/m.test(transformedCode)) {
|
|
723
|
+
transformedCode = `import { createElement, Fragment } from '${runtimeImport}';
|
|
724
|
+
` + transformedCode;
|
|
725
|
+
}
|
|
726
|
+
return {
|
|
727
|
+
code: transformedCode,
|
|
728
|
+
map: null
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
export {
|
|
735
|
+
RoundPlugin as default
|
|
736
|
+
};
|