round-core 0.0.1 → 0.0.3

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 ZtaMDev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/Round.png CHANGED
Binary file
package/bun.lock CHANGED
@@ -4,19 +4,19 @@
4
4
  "": {
5
5
  "name": "round-core",
6
6
  "dependencies": {
7
- "marked": "^12.0.2"
7
+ "marked": "^12.0.2",
8
+ "vite": "^5.0.0",
9
+ "vitest": "^1.6.0",
8
10
  },
9
11
  "devDependencies": {
10
12
  "@types/node": "latest",
11
13
  "bun-types": "latest",
12
14
  "jsdom": "^24.0.0",
13
- "vite": "^5.0.0",
14
- "vitest": "^1.6.0"
15
15
  },
16
16
  "peerDependencies": {
17
- "bun": ">=1.0.0"
18
- }
19
- }
17
+ "bun": ">=1.0.0",
18
+ },
19
+ },
20
20
  },
21
21
  "packages": {
22
22
  "@asamuzakjp/css-color": ["@asamuzakjp/css-color@3.2.0", "", { "dependencies": { "@csstools/css-calc": "^2.1.3", "@csstools/css-color-parser": "^3.0.9", "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "lru-cache": "^10.4.3" } }, "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw=="],
@@ -175,7 +175,7 @@
175
175
 
176
176
  "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
177
177
 
178
- "bun": ["bun@1.3.4", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.4", "@oven/bun-darwin-x64": "1.3.4", "@oven/bun-darwin-x64-baseline": "1.3.4", "@oven/bun-linux-aarch64": "1.3.4", "@oven/bun-linux-aarch64-musl": "1.3.4", "@oven/bun-linux-x64": "1.3.4", "@oven/bun-linux-x64-baseline": "1.3.4", "@oven/bun-linux-x64-musl": "1.3.4", "@oven/bun-linux-x64-musl-baseline": "1.3.4", "@oven/bun-windows-x64": "1.3.4", "@oven/bun-windows-x64-baseline": "1.3.4" }, "os": [ "linux", "win32", "darwin" ], "cpu": [ "x64", "arm64" ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-xV6KgD5ImquuKsoghzbWmYzeCXmmSgN6yJGz444hri2W+NGKNRFUNrEhy9+/rRXbvNA2qF0K0jAwqFNy1/GhBg=="],
178
+ "bun": ["bun@1.3.4", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.4", "@oven/bun-darwin-x64": "1.3.4", "@oven/bun-darwin-x64-baseline": "1.3.4", "@oven/bun-linux-aarch64": "1.3.4", "@oven/bun-linux-aarch64-musl": "1.3.4", "@oven/bun-linux-x64": "1.3.4", "@oven/bun-linux-x64-baseline": "1.3.4", "@oven/bun-linux-x64-musl": "1.3.4", "@oven/bun-linux-x64-musl-baseline": "1.3.4", "@oven/bun-windows-x64": "1.3.4", "@oven/bun-windows-x64-baseline": "1.3.4" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-xV6KgD5ImquuKsoghzbWmYzeCXmmSgN6yJGz444hri2W+NGKNRFUNrEhy9+/rRXbvNA2qF0K0jAwqFNy1/GhBg=="],
179
179
 
180
180
  "bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="],
181
181
 
@@ -409,6 +409,6 @@
409
409
 
410
410
  "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
411
411
 
412
- "pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="]
412
+ "pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
413
413
  }
414
414
  }
package/logo.svg ADDED
@@ -0,0 +1,10 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="500" height="500" viewBox="0 0 16 16">
2
+ <defs>
3
+ <linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
4
+ <stop offset="0" stop-color="#6d28d9"/>
5
+ <stop offset="1" stop-color="#16a34a"/>
6
+ </linearGradient>
7
+ </defs>
8
+ <rect x="1" y="1" width="14" height="14" rx="4" fill="#ffffff" stroke="url(#g)" stroke-width="1.25"/>
9
+ <path d="M5.2 11.3V4.7h2.65c.9 0 1.62.2 2.16.62.55.41.82 1.02.82 1.82 0 .56-.14 1.02-.43 1.37-.28.35-.68.6-1.18.73l1.6 2.06H9.58L8.15 9.45H6.45v1.85H5.2zm1.25-2.9h1.3c.55 0 .96-.1 1.22-.32.26-.22.4-.55.4-.99 0-.44-.13-.76-.39-.96-.26-.2-.67-.3-1.23-.3H6.45v2.57z" fill="#111827"/>
10
+ </svg>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "round-core",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "A lightweight frontend framework for SPA with signals and fine grained reactivity",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -25,11 +25,11 @@
25
25
  "author": "Round Framework Team",
26
26
  "license": "MIT",
27
27
  "dependencies": {
28
- "marked": "^12.0.2"
28
+ "marked": "^12.0.2",
29
+ "vite": "^5.0.0",
30
+ "vitest": "^1.6.0"
29
31
  },
30
32
  "devDependencies": {
31
- "vite": "^5.0.0",
32
- "vitest": "^1.6.0",
33
33
  "jsdom": "^24.0.0",
34
34
  "bun-types": "latest",
35
35
  "@types/node": "latest"
@@ -5,13 +5,6 @@
5
5
 
6
6
  export function transform(code) {
7
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
8
 
16
9
  // Helper to find balanced block starting at index
17
10
  function parseBlock(str, startIndex) {
@@ -19,15 +12,70 @@ export function transform(code) {
19
12
  let startBlockIndex = -1;
20
13
  let endBlockIndex = -1;
21
14
 
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.
15
+ let inSingle = false;
16
+ let inDouble = false;
17
+ let inTemplate = false;
18
+ let inCommentLine = false;
19
+ let inCommentMulti = false;
25
20
 
26
21
  for (let i = startIndex; i < str.length; i++) {
27
- if (str[i] === '{') {
22
+ const ch = str[i];
23
+ const prev = i > 0 ? str[i - 1] : '';
24
+ const next = i < str.length - 1 ? str[i + 1] : '';
25
+
26
+ // Handle strings and comments
27
+ if (inCommentLine) {
28
+ if (ch === '\n' || ch === '\r') inCommentLine = false;
29
+ continue;
30
+ }
31
+ if (inCommentMulti) {
32
+ if (ch === '*' && next === '/') {
33
+ inCommentMulti = false;
34
+ i++;
35
+ }
36
+ continue;
37
+ }
38
+ if (inTemplate) {
39
+ if (ch === '`' && prev !== '\\') inTemplate = false;
40
+ continue;
41
+ }
42
+ if (inSingle) {
43
+ if (ch === '\'' && prev !== '\\') inSingle = false;
44
+ continue;
45
+ }
46
+ if (inDouble) {
47
+ if (ch === '"' && prev !== '\\') inDouble = false;
48
+ continue;
49
+ }
50
+
51
+ // Check for start of strings/comments
52
+ if (ch === '/' && next === '/') {
53
+ inCommentLine = true;
54
+ i++;
55
+ continue;
56
+ }
57
+ if (ch === '/' && next === '*') {
58
+ inCommentMulti = true;
59
+ i++;
60
+ continue;
61
+ }
62
+ if (ch === '`') {
63
+ inTemplate = true;
64
+ continue;
65
+ }
66
+ if (ch === '\'') {
67
+ inSingle = true;
68
+ continue;
69
+ }
70
+ if (ch === '"') {
71
+ inDouble = true;
72
+ continue;
73
+ }
74
+
75
+ if (ch === '{') {
28
76
  if (open === 0) startBlockIndex = i;
29
77
  open++;
30
- } else if (str[i] === '}') {
78
+ } else if (ch === '}') {
31
79
  open--;
32
80
  if (open === 0) {
33
81
  endBlockIndex = i;
@@ -311,7 +311,6 @@ export default function RoundPlugin(pluginOptions = {}) {
311
311
  jsxFactory: 'createElement',
312
312
  jsxFragment: 'Fragment'
313
313
  // NOTE: Inject the runtime import in transform() to avoid
314
- // double-inject issues and because jsxInject isn't always applied to custom extensions.
315
314
  },
316
315
  // Ensure .round files are treated as JS/JSX
317
316
  resolve: {
@@ -11,7 +11,7 @@ function popContext() {
11
11
  contextStack.pop();
12
12
  }
13
13
 
14
- function readContext(ctx) {
14
+ export function readContext(ctx) {
15
15
  for (let i = contextStack.length - 1; i >= 0; i--) {
16
16
  const layer = contextStack[i];
17
17
  if (layer && Object.prototype.hasOwnProperty.call(layer, ctx.id)) {
@@ -60,3 +60,19 @@ export function bindContext(ctx) {
60
60
  return provided;
61
61
  };
62
62
  }
63
+
64
+ export function captureContext() {
65
+ return contextStack.slice();
66
+ }
67
+
68
+ export function runInContext(snapshot, fn) {
69
+ const prev = contextStack.slice();
70
+ contextStack.length = 0;
71
+ contextStack.push(...snapshot);
72
+ try {
73
+ return fn();
74
+ } finally {
75
+ contextStack.length = 0;
76
+ contextStack.push(...prev);
77
+ }
78
+ }
@@ -1,6 +1,9 @@
1
1
  import { effect, untrack } from './signals.js';
2
- import { runInContext, createComponentInstance, mountComponent, initLifecycleRoot } from './lifecycle.js';
2
+ import { runInContext as runInLifecycle, createComponentInstance, mountComponent, initLifecycleRoot } from './lifecycle.js';
3
3
  import { reportErrorSafe } from './error-reporter.js';
4
+ import { captureContext, runInContext, readContext } from './context.js';
5
+ import { SuspenseContext } from './suspense.js';
6
+
4
7
 
5
8
  let isObserverInitialized = false;
6
9
 
@@ -32,14 +35,20 @@ export function createElement(tag, props = {}, ...children) {
32
35
  const componentName = tag?.name ?? 'Anonymous';
33
36
  componentInstance.name = componentName;
34
37
 
35
- let node = runInContext(componentInstance, () => {
38
+ let node = runInLifecycle(componentInstance, () => {
36
39
  const componentProps = { ...props, children };
37
40
  try {
38
41
  const res = untrack(() => tag(componentProps));
39
42
  if (isPromiseLike(res)) throw res;
40
43
  return res;
41
44
  } catch (e) {
42
- if (isPromiseLike(e)) throw e;
45
+ if (isPromiseLike(e)) {
46
+ const suspense = readContext(SuspenseContext);
47
+ if (!suspense) {
48
+ throw new Error("cannot instance a lazy component outside a suspense");
49
+ }
50
+ throw e;
51
+ }
43
52
  reportErrorSafe(e, { phase: 'component.render', component: componentName });
44
53
  return createElement('div', { style: { padding: '16px' } }, `Error in ${componentName}`);
45
54
  }
@@ -65,6 +74,20 @@ export function createElement(tag, props = {}, ...children) {
65
74
  return node;
66
75
  }
67
76
 
77
+ if (typeof tag === 'string') {
78
+ const isCustomElement = tag.includes('-');
79
+ // Simple check: if it looks like a component (no hyphen, lowercase start)
80
+ // and it's not a standard tag, it's likely an error.
81
+ // We use a small heuristic list or regex.
82
+ // Actually, user requested: "custom components when they start with lowercase... should give error".
83
+ // Using a whitelist of standard tags is robust.
84
+ const isStandard = /^(a|abbr|address|area|article|aside|audio|b|base|bdi|bdo|blockquote|body|br|button|canvas|caption|cite|code|col|colgroup|data|datalist|dd|del|details|dfn|dialog|div|dl|dt|em|embed|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|head|header|hgroup|hr|html|i|iframe|img|input|ins|kbd|label|legend|li|link|main|map|mark|meta|meter|nav|noscript|object|ol|optgroup|option|output|p|param|picture|pre|progress|q|rp|rt|ruby|s|samp|script|section|select|slot|small|source|span|strong|style|sub|summary|sup|svg|table|tbody|td|template|textarea|tfoot|th|thead|time|title|tr|track|u|ul|var|video|wbr|path|circle|rect|line|g|defs|linearGradient|stop|radialGradient|text|tspan)$/.test(tag);
85
+
86
+ if (!isCustomElement && !isStandard && /^[a-z]/.test(tag)) {
87
+ throw new Error(`Component names must start with an uppercase letter: <${tag} />`);
88
+ }
89
+ }
90
+
68
91
  const element = document.createElement(tag);
69
92
 
70
93
  if (props) {
@@ -282,54 +305,65 @@ function appendChild(parent, child) {
282
305
 
283
306
  let currentNode = placeholder;
284
307
 
285
- effect(() => {
286
- let val;
287
- try {
288
- val = child();
289
- if (isPromiseLike(val)) throw val;
290
- } catch (e) {
291
- if (isPromiseLike(e)) throw e;
292
- reportErrorSafe(e, { phase: 'child.dynamic' });
293
- val = createElement('div', { style: { padding: '16px' } }, 'Error');
294
- }
308
+ const ctxSnapshot = captureContext();
295
309
 
296
- if (Array.isArray(val)) {
297
- if (!(currentNode instanceof Element) || !currentNode._roundArrayWrapper) {
298
- const wrapper = document.createElement('span');
299
- wrapper.style.display = 'contents';
300
- wrapper._roundArrayWrapper = true;
301
- if (currentNode.parentNode) {
302
- currentNode.parentNode.replaceChild(wrapper, currentNode);
303
- currentNode = wrapper;
310
+ effect(() => {
311
+ runInContext(ctxSnapshot, () => {
312
+ let val;
313
+ try {
314
+ val = child();
315
+ if (isPromiseLike(val)) throw val;
316
+ } catch (e) {
317
+ if (isPromiseLike(e)) {
318
+ const suspense = readContext(SuspenseContext);
319
+ if (suspense && typeof suspense.register === 'function') {
320
+ suspense.register(e);
321
+ return;
322
+ }
323
+ throw new Error("cannot instance a lazy component outside a suspense");
304
324
  }
325
+ reportErrorSafe(e, { phase: 'child.dynamic' });
326
+ val = createElement('div', { style: { padding: '16px' } }, 'Error');
305
327
  }
306
328
 
307
- while (currentNode.firstChild) currentNode.removeChild(currentNode.firstChild);
308
- val.forEach(v => appendChild(currentNode, v));
309
- return;
310
- }
329
+ if (Array.isArray(val)) {
330
+ if (!(currentNode instanceof Element) || !currentNode._roundArrayWrapper) {
331
+ const wrapper = document.createElement('span');
332
+ wrapper.style.display = 'contents';
333
+ wrapper._roundArrayWrapper = true;
334
+ if (currentNode.parentNode) {
335
+ currentNode.parentNode.replaceChild(wrapper, currentNode);
336
+ currentNode = wrapper;
337
+ }
338
+ }
311
339
 
312
- if (val instanceof Node) {
313
- if (currentNode !== val) {
314
- if (currentNode.parentNode) {
315
- currentNode.parentNode.replaceChild(val, currentNode);
316
- currentNode = val;
340
+ while (currentNode.firstChild) currentNode.removeChild(currentNode.firstChild);
341
+ val.forEach(v => appendChild(currentNode, v));
342
+ return;
343
+ }
344
+
345
+ if (val instanceof Node) {
346
+ if (currentNode !== val) {
347
+ if (currentNode.parentNode) {
348
+ currentNode.parentNode.replaceChild(val, currentNode);
349
+ currentNode = val;
350
+ }
317
351
  }
318
352
  }
319
- }
320
- else {
321
- const textContent = (val === null || val === undefined) ? '' : val;
322
-
323
- if (currentNode instanceof Element) {
324
- const newText = document.createTextNode(textContent);
325
- if (currentNode.parentNode) {
326
- currentNode.parentNode.replaceChild(newText, currentNode);
327
- currentNode = newText;
353
+ else {
354
+ const textContent = (val === null || val === undefined) ? '' : val;
355
+
356
+ if (currentNode instanceof Element) {
357
+ const newText = document.createTextNode(textContent);
358
+ if (currentNode.parentNode) {
359
+ currentNode.parentNode.replaceChild(newText, currentNode);
360
+ currentNode = newText;
361
+ }
362
+ } else {
363
+ currentNode.textContent = textContent;
328
364
  }
329
- } else {
330
- currentNode.textContent = textContent;
331
365
  }
332
- }
366
+ });
333
367
  }, { onLoad: false });
334
368
  return;
335
369
  }
@@ -1,10 +1,14 @@
1
1
  import { signal } from './signals.js';
2
2
  import { createElement, Fragment } from './dom.js';
3
+ import { createContext } from './context.js';
3
4
 
4
5
  function isPromiseLike(v) {
5
6
  return v && (typeof v === 'object' || typeof v === 'function') && typeof v.then === 'function';
6
7
  }
7
8
 
9
+ const SuspenseContext = createContext(null);
10
+ export { SuspenseContext };
11
+
8
12
  export function lazy(loader) {
9
13
  if (typeof loader !== 'function') {
10
14
  throw new Error('lazy(loader) expects a function that returns a Promise');
@@ -67,37 +71,55 @@ export function lazy(loader) {
67
71
 
68
72
  export function Suspense(props = {}) {
69
73
  const tick = signal(0);
70
- let lastPromise = null;
74
+ const pending = new Set();
75
+
76
+ // Track promises we are currently waiting for to avoid re-adding them or flickering
77
+ const waiting = new Set();
71
78
 
72
79
  const child = Array.isArray(props.children) ? props.children[0] : props.children;
73
80
  const childFn = typeof child === 'function' ? child : () => child;
74
81
 
75
- return createElement('span', { style: { display: 'contents' } }, () => {
82
+ const register = (promise) => {
83
+ if (!waiting.has(promise)) {
84
+ waiting.add(promise);
85
+ pending.add(promise);
86
+ promise.then(
87
+ () => {
88
+ waiting.delete(promise);
89
+ pending.delete(promise);
90
+ tick(tick.peek() + 1);
91
+ },
92
+ () => {
93
+ waiting.delete(promise);
94
+ pending.delete(promise);
95
+ tick(tick.peek() + 1);
96
+ }
97
+ );
98
+ }
99
+ };
100
+
101
+ return createElement(SuspenseContext.Provider, {
102
+ value: { register }
103
+ }, () => {
104
+ // Read tick to re-render when promises resolve
76
105
  tick();
77
106
 
107
+ // If pending promises, show fallback depending on strategy.
108
+
109
+ if (pending.size > 0) {
110
+ return props.fallback ?? null;
111
+ }
112
+
78
113
  try {
79
114
  const res = childFn();
80
115
  if (isPromiseLike(res)) {
81
- if (lastPromise !== res) {
82
- lastPromise = res;
83
- res.then(
84
- () => tick(tick.peek() + 1),
85
- () => tick(tick.peek() + 1)
86
- );
87
- }
116
+ register(res);
88
117
  return props.fallback ?? null;
89
118
  }
90
- lastPromise = null;
91
119
  return res ?? null;
92
120
  } catch (e) {
93
121
  if (isPromiseLike(e)) {
94
- if (lastPromise !== e) {
95
- lastPromise = e;
96
- e.then(
97
- () => tick(tick.peek() + 1),
98
- () => tick(tick.peek() + 1)
99
- );
100
- }
122
+ register(e);
101
123
  return props.fallback ?? null;
102
124
  }
103
125
  throw e;