round-core 0.0.1
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 +354 -0
- package/Round.png +0 -0
- package/bun.lock +414 -0
- package/cli.js +2 -0
- package/index.html +19 -0
- package/index.js +2 -0
- package/package.json +40 -0
- package/src/cli.js +599 -0
- package/src/compiler/index.js +2 -0
- package/src/compiler/transformer.js +395 -0
- package/src/compiler/vite-plugin.js +461 -0
- package/src/index.js +45 -0
- package/src/runtime/context.js +62 -0
- package/src/runtime/dom.js +345 -0
- package/src/runtime/error-boundary.js +48 -0
- package/src/runtime/error-reporter.js +13 -0
- package/src/runtime/error-store.js +85 -0
- package/src/runtime/errors.js +152 -0
- package/src/runtime/lifecycle.js +142 -0
- package/src/runtime/markdown.js +72 -0
- package/src/runtime/router.js +371 -0
- package/src/runtime/signals.js +510 -0
- package/src/runtime/store.js +208 -0
- package/src/runtime/suspense.js +106 -0
- package/vite.config.js +10 -0
- package/vitest.config.js +8 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
// Transformer for .round files
|
|
2
|
+
// Handles custom syntax like:
|
|
3
|
+
// {if(cond){ ... }} -> {cond ? (...) : null}
|
|
4
|
+
// {for(item in list){ ... }} -> {list.map(item => (...))}
|
|
5
|
+
|
|
6
|
+
export function transform(code) {
|
|
7
|
+
// Process "if" blocks first, then "for" blocks (or vice versa, order matters if nested)
|
|
8
|
+
// Actually, simple sequential processing might miss nested ones if we replace outside-in vs inside-out.
|
|
9
|
+
// Let's do a loop that finds the *first* occurrence, replaces it, and repeats until none found.
|
|
10
|
+
// This handles nesting naturally if we restart search? No, replacing outer first might break inner logic if we simple-replace string.
|
|
11
|
+
// But if we construct valid JS, inner logic is just string content initially?
|
|
12
|
+
// Wait, if we replace `if(x){ if(y){} }` -> `x ? ( if(y){} ) : null`.
|
|
13
|
+
// Then next pass sees `if(y){}` and replaces it. `x ? ( y ? ... : null ) : null`.
|
|
14
|
+
// Valid.
|
|
15
|
+
|
|
16
|
+
// Helper to find balanced block starting at index
|
|
17
|
+
function parseBlock(str, startIndex) {
|
|
18
|
+
let open = 0;
|
|
19
|
+
let startBlockIndex = -1;
|
|
20
|
+
let endBlockIndex = -1;
|
|
21
|
+
|
|
22
|
+
// Find the opening { of the block
|
|
23
|
+
// The regex gives us the start of "if/for (...) {"
|
|
24
|
+
// We need to verify we found the brace.
|
|
25
|
+
|
|
26
|
+
for (let i = startIndex; i < str.length; i++) {
|
|
27
|
+
if (str[i] === '{') {
|
|
28
|
+
if (open === 0) startBlockIndex = i;
|
|
29
|
+
open++;
|
|
30
|
+
} else if (str[i] === '}') {
|
|
31
|
+
open--;
|
|
32
|
+
if (open === 0) {
|
|
33
|
+
endBlockIndex = i;
|
|
34
|
+
return { start: startBlockIndex, end: endBlockIndex };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let result = code;
|
|
42
|
+
|
|
43
|
+
function consumeWhitespace(str, i) {
|
|
44
|
+
while (i < str.length && /\s/.test(str[i])) i++;
|
|
45
|
+
return i;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseIfChain(str, ifIndex) {
|
|
49
|
+
const head = str.slice(ifIndex);
|
|
50
|
+
const m = head.match(/^if\s*\((.*?)\)\s*\{/);
|
|
51
|
+
if (!m) return null;
|
|
52
|
+
|
|
53
|
+
let i = ifIndex;
|
|
54
|
+
const cases = [];
|
|
55
|
+
let elseContent = null;
|
|
56
|
+
|
|
57
|
+
while (true) {
|
|
58
|
+
const cur = str.slice(i);
|
|
59
|
+
const mm = cur.match(/^if\s*\((.*?)\)\s*\{/);
|
|
60
|
+
if (!mm) return null;
|
|
61
|
+
let cond = mm[1];
|
|
62
|
+
|
|
63
|
+
// Allow {if(signal){...}} where signal is a simple identifier/member path.
|
|
64
|
+
// For those cases, auto-unwrap signal-like values by calling them.
|
|
65
|
+
// Examples supported:
|
|
66
|
+
// - if(flags.showCounter){...}
|
|
67
|
+
// - if(user.loggedIn){...}
|
|
68
|
+
// Complex expressions are left untouched.
|
|
69
|
+
const trimmedCond = String(cond).trim();
|
|
70
|
+
const isSimplePath = /^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*)*$/.test(trimmedCond);
|
|
71
|
+
if (isSimplePath && !trimmedCond.endsWith(')')) {
|
|
72
|
+
cond = `((typeof (${trimmedCond}) === 'function' && typeof (${trimmedCond}).peek === 'function' && ('value' in (${trimmedCond}))) ? (${trimmedCond})() : (${trimmedCond}))`;
|
|
73
|
+
}
|
|
74
|
+
const blockStart = i + mm[0].length - 1;
|
|
75
|
+
const block = parseBlock(str, blockStart);
|
|
76
|
+
if (!block) return null;
|
|
77
|
+
|
|
78
|
+
const content = str.substring(block.start + 1, block.end);
|
|
79
|
+
cases.push({ cond, content });
|
|
80
|
+
|
|
81
|
+
i = block.end + 1;
|
|
82
|
+
i = consumeWhitespace(str, i);
|
|
83
|
+
|
|
84
|
+
if (!str.startsWith('else', i)) {
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
i += 4;
|
|
89
|
+
i = consumeWhitespace(str, i);
|
|
90
|
+
|
|
91
|
+
if (str.startsWith('if', i)) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (str[i] !== '{') return null;
|
|
96
|
+
const elseBlock = parseBlock(str, i);
|
|
97
|
+
if (!elseBlock) return null;
|
|
98
|
+
elseContent = str.substring(elseBlock.start + 1, elseBlock.end);
|
|
99
|
+
i = elseBlock.end + 1;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const end = i;
|
|
104
|
+
|
|
105
|
+
let expr = '';
|
|
106
|
+
for (let idx = 0; idx < cases.length; idx++) {
|
|
107
|
+
const c = cases[idx];
|
|
108
|
+
const body = `<Fragment>${c.content}</Fragment>`;
|
|
109
|
+
if (idx === 0) {
|
|
110
|
+
expr = `(${c.cond}) ? (${body}) : `;
|
|
111
|
+
} else {
|
|
112
|
+
expr += `(${c.cond}) ? (${body}) : `;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (elseContent !== null) {
|
|
116
|
+
expr += `(<Fragment>${elseContent}</Fragment>)`;
|
|
117
|
+
} else {
|
|
118
|
+
expr += 'null';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const replacement = `(() => ${expr})`;
|
|
122
|
+
return { start: ifIndex, end, replacement };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function parseIfStatement(str, ifIndex) {
|
|
126
|
+
if (!str.startsWith('if', ifIndex)) return null;
|
|
127
|
+
const chain = parseIfChain(str, ifIndex);
|
|
128
|
+
if (!chain) return null;
|
|
129
|
+
return {
|
|
130
|
+
start: chain.start,
|
|
131
|
+
end: chain.end,
|
|
132
|
+
replacement: `{${chain.replacement}}`
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function parseIfExpression(str, exprStart) {
|
|
137
|
+
if (str[exprStart] !== '{') return null;
|
|
138
|
+
|
|
139
|
+
let i = consumeWhitespace(str, exprStart + 1);
|
|
140
|
+
if (!str.startsWith('if', i)) return null;
|
|
141
|
+
|
|
142
|
+
const outer = parseBlock(str, exprStart);
|
|
143
|
+
if (!outer) return null;
|
|
144
|
+
|
|
145
|
+
const chain = parseIfChain(str, i);
|
|
146
|
+
if (!chain) return null;
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
start: exprStart,
|
|
150
|
+
end: outer.end + 1,
|
|
151
|
+
replacement: `{${chain.replacement}}`
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let prev = null;
|
|
156
|
+
while (prev !== result) {
|
|
157
|
+
prev = result;
|
|
158
|
+
|
|
159
|
+
while (true) {
|
|
160
|
+
const match = result.match(/\{\s*if\s*\(/);
|
|
161
|
+
if (!match) break;
|
|
162
|
+
const matchIndex = match.index;
|
|
163
|
+
|
|
164
|
+
const parsed = parseIfExpression(result, matchIndex);
|
|
165
|
+
if (!parsed) {
|
|
166
|
+
console.warn('Unbalanced IF expression found, skipping transformation.');
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const before = result.substring(0, parsed.start);
|
|
171
|
+
const after = result.substring(parsed.end);
|
|
172
|
+
result = before + parsed.replacement + after;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
while (true) {
|
|
176
|
+
const match = result.match(/(^|[\n\r])\s*if\s*\(/m);
|
|
177
|
+
if (!match) break;
|
|
178
|
+
const ifIndex = match.index + match[0].lastIndexOf('if');
|
|
179
|
+
|
|
180
|
+
const parsed = parseIfStatement(result, ifIndex);
|
|
181
|
+
if (!parsed) break;
|
|
182
|
+
|
|
183
|
+
const before = result.substring(0, parsed.start);
|
|
184
|
+
const after = result.substring(parsed.end);
|
|
185
|
+
result = before + parsed.replacement + after;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
while (true) {
|
|
189
|
+
const match = result.match(/\{\s*for\s*\((.*?)\s+in\s+(.*?)\)\s*\{/);
|
|
190
|
+
if (!match) break;
|
|
191
|
+
|
|
192
|
+
const item = match[1];
|
|
193
|
+
const list = match[2];
|
|
194
|
+
const exprStart = match.index;
|
|
195
|
+
|
|
196
|
+
const outer = parseBlock(result, exprStart);
|
|
197
|
+
if (!outer) break;
|
|
198
|
+
|
|
199
|
+
let i = consumeWhitespace(result, exprStart + 1);
|
|
200
|
+
const head = result.slice(i);
|
|
201
|
+
const mm = head.match(/^for\s*\((.*?)\s+in\s+(.*?)\)\s*\{/);
|
|
202
|
+
if (!mm) break;
|
|
203
|
+
const forStart = i;
|
|
204
|
+
const blockStart = forStart + mm[0].length - 1;
|
|
205
|
+
const block = parseBlock(result, blockStart);
|
|
206
|
+
if (!block) break;
|
|
207
|
+
|
|
208
|
+
const content = result.substring(block.start + 1, block.end);
|
|
209
|
+
const replacement = `{${list}.map(${item} => (<Fragment>${content}</Fragment>))}`;
|
|
210
|
+
|
|
211
|
+
const before = result.substring(0, exprStart);
|
|
212
|
+
const after = result.substring(outer.end + 1);
|
|
213
|
+
|
|
214
|
+
result = before + replacement + after;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
while (true) {
|
|
218
|
+
const match = result.match(/(^|[\n\r])\s*for\s*\((.*?)\s+in\s+(.*?)\)\s*\{/m);
|
|
219
|
+
if (!match) break;
|
|
220
|
+
|
|
221
|
+
const exprStart = match.index + match[0].lastIndexOf('for');
|
|
222
|
+
const item = match[2];
|
|
223
|
+
const list = match[3];
|
|
224
|
+
|
|
225
|
+
const forHead = result.slice(exprStart);
|
|
226
|
+
const mm = forHead.match(/^for\s*\((.*?)\s+in\s+(.*?)\)\s*\{/);
|
|
227
|
+
if (!mm) break;
|
|
228
|
+
|
|
229
|
+
const blockStart = exprStart + mm[0].length - 1;
|
|
230
|
+
const block = parseBlock(result, blockStart);
|
|
231
|
+
if (!block) break;
|
|
232
|
+
|
|
233
|
+
const content = result.substring(block.start + 1, block.end);
|
|
234
|
+
const replacement = `{${list}.map(${item} => (<Fragment>${content}</Fragment>))}`;
|
|
235
|
+
|
|
236
|
+
const before = result.substring(0, exprStart);
|
|
237
|
+
const after = result.substring(block.end + 1);
|
|
238
|
+
result = before + replacement + after;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function findJsxTagEnd(str, startIndex) {
|
|
243
|
+
let inSingle = false;
|
|
244
|
+
let inDouble = false;
|
|
245
|
+
let inTemplate = false;
|
|
246
|
+
let braceDepth = 0;
|
|
247
|
+
|
|
248
|
+
for (let i = startIndex; i < str.length; i++) {
|
|
249
|
+
const ch = str[i];
|
|
250
|
+
const prevCh = i > 0 ? str[i - 1] : '';
|
|
251
|
+
|
|
252
|
+
if (!inDouble && !inTemplate && ch === '\'' && prevCh !== '\\') inSingle = !inSingle;
|
|
253
|
+
else if (!inSingle && !inTemplate && ch === '"' && prevCh !== '\\') inDouble = !inDouble;
|
|
254
|
+
else if (!inSingle && !inDouble && ch === '`' && prevCh !== '\\') inTemplate = !inTemplate;
|
|
255
|
+
|
|
256
|
+
if (inSingle || inDouble || inTemplate) continue;
|
|
257
|
+
|
|
258
|
+
if (ch === '{') braceDepth++;
|
|
259
|
+
else if (ch === '}') braceDepth = Math.max(0, braceDepth - 1);
|
|
260
|
+
else if (ch === '>' && braceDepth === 0) return i;
|
|
261
|
+
}
|
|
262
|
+
return -1;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function transformSuspenseBlocks(str) {
|
|
266
|
+
let out = str;
|
|
267
|
+
let cursor = 0;
|
|
268
|
+
while (true) {
|
|
269
|
+
const openIndex = out.indexOf('<Suspense', cursor);
|
|
270
|
+
if (openIndex === -1) break;
|
|
271
|
+
const openEnd = findJsxTagEnd(out, openIndex);
|
|
272
|
+
if (openEnd === -1) break;
|
|
273
|
+
|
|
274
|
+
const openTagText = out.slice(openIndex, openEnd + 1);
|
|
275
|
+
if (/\/>\s*$/.test(openTagText)) {
|
|
276
|
+
cursor = openEnd + 1;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
let depth = 1;
|
|
281
|
+
let i = openEnd + 1;
|
|
282
|
+
let closeStart = -1;
|
|
283
|
+
while (i < out.length) {
|
|
284
|
+
const nextOpen = out.indexOf('<Suspense', i);
|
|
285
|
+
const nextClose = out.indexOf('</Suspense>', i);
|
|
286
|
+
if (nextClose === -1) break;
|
|
287
|
+
if (nextOpen !== -1 && nextOpen < nextClose) {
|
|
288
|
+
const innerOpenEnd = findJsxTagEnd(out, nextOpen);
|
|
289
|
+
if (innerOpenEnd === -1) break;
|
|
290
|
+
const innerOpenText = out.slice(nextOpen, innerOpenEnd + 1);
|
|
291
|
+
if (!/\/>\s*$/.test(innerOpenText)) depth++;
|
|
292
|
+
i = innerOpenEnd + 1;
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
depth--;
|
|
297
|
+
if (depth === 0) {
|
|
298
|
+
closeStart = nextClose;
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
i = nextClose + '</Suspense>'.length;
|
|
302
|
+
}
|
|
303
|
+
if (closeStart === -1) break;
|
|
304
|
+
|
|
305
|
+
const inner = out.slice(openEnd + 1, closeStart);
|
|
306
|
+
const innerTrim = inner.trim();
|
|
307
|
+
if (innerTrim.startsWith('{() =>')) {
|
|
308
|
+
cursor = closeStart + '</Suspense>'.length;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const wrapped = `{() => (<Fragment>${inner}</Fragment>)}`;
|
|
313
|
+
out = out.slice(0, openEnd + 1) + wrapped + out.slice(closeStart);
|
|
314
|
+
cursor = closeStart + wrapped.length + '</Suspense>'.length;
|
|
315
|
+
}
|
|
316
|
+
return out;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function transformProviderBlocks(str) {
|
|
320
|
+
let out = str;
|
|
321
|
+
let cursor = 0;
|
|
322
|
+
while (true) {
|
|
323
|
+
const dot = out.indexOf('.Provider', cursor);
|
|
324
|
+
if (dot === -1) break;
|
|
325
|
+
const lt = out.lastIndexOf('<', dot);
|
|
326
|
+
if (lt === -1) break;
|
|
327
|
+
const openEnd = findJsxTagEnd(out, lt);
|
|
328
|
+
if (openEnd === -1) break;
|
|
329
|
+
|
|
330
|
+
const openTagText = out.slice(lt, openEnd + 1);
|
|
331
|
+
if (/\/>\s*$/.test(openTagText)) {
|
|
332
|
+
cursor = openEnd + 1;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const m = openTagText.match(/^<\s*([A-Za-z_$][\w$]*\.Provider)\b/);
|
|
337
|
+
if (!m) {
|
|
338
|
+
cursor = openEnd + 1;
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
const tagName = m[1];
|
|
342
|
+
const closeTag = `</${tagName}>`;
|
|
343
|
+
|
|
344
|
+
let depth = 1;
|
|
345
|
+
let i = openEnd + 1;
|
|
346
|
+
let closeStart = -1;
|
|
347
|
+
while (i < out.length) {
|
|
348
|
+
const nextOpen = out.indexOf(`<${tagName}`, i);
|
|
349
|
+
const nextClose = out.indexOf(closeTag, i);
|
|
350
|
+
if (nextClose === -1) break;
|
|
351
|
+
if (nextOpen !== -1 && nextOpen < nextClose) {
|
|
352
|
+
const innerOpenEnd = findJsxTagEnd(out, nextOpen);
|
|
353
|
+
if (innerOpenEnd === -1) break;
|
|
354
|
+
const innerOpenText = out.slice(nextOpen, innerOpenEnd + 1);
|
|
355
|
+
if (!/\/>\s*$/.test(innerOpenText)) depth++;
|
|
356
|
+
i = innerOpenEnd + 1;
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
depth--;
|
|
361
|
+
if (depth === 0) {
|
|
362
|
+
closeStart = nextClose;
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
i = nextClose + closeTag.length;
|
|
366
|
+
}
|
|
367
|
+
if (closeStart === -1) break;
|
|
368
|
+
|
|
369
|
+
const inner = out.slice(openEnd + 1, closeStart);
|
|
370
|
+
const innerTrim = inner.trim();
|
|
371
|
+
if (innerTrim.startsWith('{() =>')) {
|
|
372
|
+
cursor = closeStart + closeTag.length;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const wrapped = `{() => (<Fragment>${inner}</Fragment>)}`;
|
|
377
|
+
out = out.slice(0, openEnd + 1) + wrapped + out.slice(closeStart);
|
|
378
|
+
cursor = closeStart + wrapped.length + closeTag.length;
|
|
379
|
+
}
|
|
380
|
+
return out;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
result = transformSuspenseBlocks(result);
|
|
384
|
+
result = transformProviderBlocks(result);
|
|
385
|
+
|
|
386
|
+
// Make `signal()` reactive in JSX by passing a function to the runtime.
|
|
387
|
+
// `{count()}` -> `{() => count()}``
|
|
388
|
+
// `value={count()}` -> `value={() => count()}``
|
|
389
|
+
// This is intentionally limited to zero-arg identifier calls.
|
|
390
|
+
result = result
|
|
391
|
+
.replace(/\{\s*([A-Za-z_$][\w$]*)\s*\(\s*\)\s*\}/g, '{() => $1()}')
|
|
392
|
+
.replace(/=\{\s*([A-Za-z_$][\w$]*)\s*\(\s*\)\s*\}/g, '={() => $1()}');
|
|
393
|
+
|
|
394
|
+
return result;
|
|
395
|
+
}
|