round-core 0.1.0 → 0.1.2

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 CHANGED
@@ -20,7 +20,7 @@ Extension for [VSCode](https://marketplace.visualstudio.com/items?itemName=ZtaMD
20
20
 
21
21
  ---
22
22
 
23
- Instead of a Virtual DOM diff, Round updates the UI by subscribing DOM updates directly to reactive primitives (**signals**) and **bindables**. This keeps rendering predictable, small, and fast for interactive apps.
23
+ Instead of a Virtual DOM diff, Round updates the UI by subscribing DOM updates directly to reactive primitives **signals** and **bindables**. This keeps rendering predictable, small, and fast for interactive apps.
24
24
 
25
25
  The `round-core` package is the **foundation of RoundJS**.
26
26
 
@@ -163,6 +163,23 @@ effect(() => {
163
163
  name('Grace');
164
164
  ```
165
165
 
166
+ ### `untrack(fn)`
167
+
168
+ Run a function without tracking any signals it reads.
169
+
170
+ ```javascript
171
+ import { signal, untrack, effect } from 'round-core';
172
+
173
+ const count = signal(0);
174
+ effect(() => {
175
+ console.log('Count is:', count());
176
+ untrack(() => {
177
+ // This read won't trigger the effect if it changes elsewhere
178
+ console.log('Static value:', count());
179
+ });
180
+ });
181
+ ```
182
+
166
183
  ### `bindable(initialValue)`
167
184
 
168
185
  `bindable()` creates a signal intended for **two-way DOM bindings**.
@@ -206,14 +223,24 @@ export function TodoList() {
206
223
 
207
224
  return (
208
225
  <div>
209
- {todos().map(todo => <div>{todo.text}</div>)}
226
+ {for(todo in todos()){
227
+ <div>{todo.text}</div>
228
+ }}
210
229
  <button onClick={() => store.addTodo('Buy Milk')}>Add</button>
211
230
  </div>
212
231
  );
213
232
  }
214
233
 
215
234
  // 3. Persistence (Optional)
216
- store.persist('my-app-store');
235
+ store.persist('my-app-store', {
236
+ debounce: 100, // ms
237
+ exclude: ['someSecretKey']
238
+ });
239
+
240
+ // 4. Advanced Methods
241
+ store.patch({ filter: 'completed' }); // Update multiple keys at once
242
+ const data = store.snapshot({ reactive: false }); // Get static JSON of state
243
+ store.set('todos', []); // Direct set
217
244
  ```
218
245
 
219
246
  ### `bindable.object(initialObject)` and deep binding
@@ -241,6 +268,38 @@ export function Profile() {
241
268
  }
242
269
  ```
243
270
 
271
+ ## Lifecycle Hooks
272
+
273
+ Round provides hooks to tap into the lifecycle of components. These must be called during the synchronous execution of your component function.
274
+
275
+ ### `onMount(fn)`
276
+ Runs after the component is first created and its elements are added to the DOM. If `fn` returns a function, it's used as a cleanup (equivalent to `onUnmount`).
277
+
278
+ ### `onUnmount(fn)`
279
+ Runs when the component's elements are removed from the DOM.
280
+
281
+ ### `onUpdate(fn)`
282
+ Runs whenever any signal read during the component's *initial* render is updated.
283
+
284
+ ### `onCleanup(fn)`
285
+ Alias for `onUnmount`.
286
+
287
+ ```jsx
288
+ import { onMount, onUnmount } from 'round-core';
289
+
290
+ export function MyComponent() {
291
+ onMount(() => {
292
+ console.log('Mounted!');
293
+ const timer = setInterval(() => {}, 1000);
294
+ return () => clearInterval(timer); // Cleanup
295
+ });
296
+
297
+ onUnmount(() => console.log('Goodbye!'));
298
+
299
+ return <div>Hello</div>;
300
+ }
301
+ ```
302
+
244
303
  ### `.validate(validator, options)`
245
304
 
246
305
  Attach validation to a signal/bindable.
@@ -398,6 +457,22 @@ Round aims to provide strong developer feedback:
398
457
  - Runtime error reporting with safe boundaries.
399
458
  - `ErrorBoundary` to catch render-time errors and show a fallback.
400
459
 
460
+ ### `ErrorBoundary`
461
+
462
+ Wrap components to prevent the whole app from crashing if a child fails to render.
463
+
464
+ ```jsx
465
+ import { ErrorBoundary } from 'round-core';
466
+
467
+ function DangerousComponent() {
468
+ throw new Error('Boom!');
469
+ }
470
+
471
+ <ErrorBoundary fallback={(err) => <div className="error">Something went wrong: {err.message}</div>}>
472
+ <DangerousComponent />
473
+ </ErrorBoundary>
474
+ ```
475
+
401
476
  ## CLI
402
477
 
403
478
  The CLI is intended for day-to-day development:
package/dist/cli.js CHANGED
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
2
4
  import path from "node:path";
3
5
  import fs from "node:fs";
4
6
  import process from "node:process";
@@ -8,6 +10,7 @@ import RoundPlugin from "./vite-plugin.js";
8
10
  function onSignal() {
9
11
  process.exit(0);
10
12
  }
13
+ __name(onSignal, "onSignal");
11
14
  process.on("SIGINT", onSignal);
12
15
  process.on("SIGTERM", onSignal);
13
16
  process.on("exit", () => {
@@ -23,9 +26,11 @@ function cleanupTemporaryFiles() {
23
26
  }
24
27
  temporaryFiles.clear();
25
28
  }
29
+ __name(cleanupTemporaryFiles, "cleanupTemporaryFiles");
26
30
  function normalizePath(p) {
27
31
  return p.replaceAll("\\", "/");
28
32
  }
33
+ __name(normalizePath, "normalizePath");
29
34
  const colors = {
30
35
  reset: "\x1B[0m",
31
36
  dim: "\x1B[2m",
@@ -41,7 +46,11 @@ const colors = {
41
46
  function c(text, color) {
42
47
  return `${colors[color] ?? ""}${text}${colors.reset}`;
43
48
  }
49
+ __name(c, "c");
44
50
  class CliError extends Error {
51
+ static {
52
+ __name(this, "CliError");
53
+ }
45
54
  constructor(message, options = {}) {
46
55
  super(message);
47
56
  this.name = "CliError";
@@ -55,6 +64,7 @@ function printError(message) {
55
64
  process.stderr.write(`${c("Error:", "red")} ${msg}
56
65
  `);
57
66
  }
67
+ __name(printError, "printError");
58
68
  function getRoundVersion() {
59
69
  try {
60
70
  const here = path.dirname(fileURLToPath(import.meta.url));
@@ -66,6 +76,7 @@ function getRoundVersion() {
66
76
  return null;
67
77
  }
68
78
  }
79
+ __name(getRoundVersion, "getRoundVersion");
69
80
  function banner(title) {
70
81
  const v = getRoundVersion();
71
82
  const name = c("ROUND", "cyan");
@@ -76,12 +87,13 @@ function banner(title) {
76
87
  process.stdout.write(`
77
88
  `);
78
89
  }
90
+ __name(banner, "banner");
79
91
  function createViteLogger() {
80
92
  let hasError = false;
81
- const noop = () => {
82
- };
93
+ const noop = /* @__PURE__ */ __name(() => {
94
+ }, "noop");
83
95
  return {
84
- hasErrorLogged: () => hasError,
96
+ hasErrorLogged: /* @__PURE__ */ __name(() => hasError, "hasErrorLogged"),
85
97
  info(msg) {
86
98
  const s = String(msg ?? "");
87
99
  if (!s) return;
@@ -114,6 +126,7 @@ function createViteLogger() {
114
126
  }
115
127
  };
116
128
  }
129
+ __name(createViteLogger, "createViteLogger");
117
130
  function printUrls(resolvedUrls, base = "/", ms = null) {
118
131
  const local = resolvedUrls?.local?.[0];
119
132
  const network = resolvedUrls?.network?.[0];
@@ -130,11 +143,13 @@ function printUrls(resolvedUrls, base = "/", ms = null) {
130
143
  process.stdout.write(`
131
144
  `);
132
145
  }
146
+ __name(printUrls, "printUrls");
133
147
  function resolveFrom(baseDir, p) {
134
148
  if (!p) return null;
135
149
  if (path.isAbsolute(p)) return p;
136
150
  return path.resolve(baseDir, p);
137
151
  }
152
+ __name(resolveFrom, "resolveFrom");
138
153
  function parseArgs(argv) {
139
154
  const args = { _: [] };
140
155
  for (let i = 0; i < argv.length; i++) {
@@ -166,6 +181,7 @@ function parseArgs(argv) {
166
181
  }
167
182
  return args;
168
183
  }
184
+ __name(parseArgs, "parseArgs");
169
185
  function printHelp() {
170
186
  const header = `${c("round", "cyan")} ${c("(CLI)", "gray")}`;
171
187
  process.stdout.write(
@@ -186,15 +202,18 @@ function printHelp() {
186
202
  ].join("\n")
187
203
  );
188
204
  }
205
+ __name(printHelp, "printHelp");
189
206
  function ensureDir(dir) {
190
207
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
191
208
  }
209
+ __name(ensureDir, "ensureDir");
192
210
  function writeFileIfMissing(filePath, content) {
193
211
  if (fs.existsSync(filePath)) {
194
212
  throw new Error(`File already exists: ${filePath}`);
195
213
  }
196
214
  fs.writeFileSync(filePath, content, "utf8");
197
215
  }
216
+ __name(writeFileIfMissing, "writeFileIfMissing");
198
217
  async function runInit({ name }) {
199
218
  if (!name || typeof name !== "string") {
200
219
  throw new CliError(
@@ -327,15 +346,18 @@ ${c("Project created:", "green")} ${projectDir}
327
346
 
328
347
  `);
329
348
  }
349
+ __name(runInit, "runInit");
330
350
  function loadRoundConfig(configPathAbs) {
331
351
  const raw = fs.readFileSync(configPathAbs, "utf8");
332
352
  const json = JSON.parse(raw);
333
353
  return json && typeof json === "object" ? json : {};
334
354
  }
355
+ __name(loadRoundConfig, "loadRoundConfig");
335
356
  function coerceNumber(v, fallback) {
336
357
  const n = Number(v);
337
358
  return Number.isFinite(n) ? n : fallback;
338
359
  }
360
+ __name(coerceNumber, "coerceNumber");
339
361
  function generateIndexHtml(config) {
340
362
  const name = config?.name ?? "Round";
341
363
  const rawEntry = config?.entry ?? "./src/app.round";
@@ -362,6 +384,7 @@ function generateIndexHtml(config) {
362
384
  "</html>"
363
385
  ].join("\n");
364
386
  }
387
+ __name(generateIndexHtml, "generateIndexHtml");
365
388
  function writeTemporaryIndex(rootDir, config) {
366
389
  const indexPath = path.resolve(rootDir, "index.html");
367
390
  if (fs.existsSync(indexPath)) return false;
@@ -370,6 +393,7 @@ function writeTemporaryIndex(rootDir, config) {
370
393
  temporaryFiles.add(indexPath);
371
394
  return true;
372
395
  }
396
+ __name(writeTemporaryIndex, "writeTemporaryIndex");
373
397
  async function runDev({ rootDir, configPathAbs, config }) {
374
398
  const startedAt = Date.now();
375
399
  const configDir = path.dirname(configPathAbs);
@@ -381,7 +405,7 @@ async function runDev({ rootDir, configPathAbs, config }) {
381
405
  let viteServer = null;
382
406
  let restarting = false;
383
407
  let restartTimer = null;
384
- const startServer = async (nextConfig, { showBanner, showReady } = { showBanner: true, showReady: true }) => {
408
+ const startServer = /* @__PURE__ */ __name(async (nextConfig, { showBanner, showReady } = { showBanner: true, showReady: true }) => {
385
409
  const cfgDir = path.dirname(configPathAbs);
386
410
  const entryAbs2 = nextConfig?.entry ? resolveFrom(cfgDir, nextConfig.entry) : null;
387
411
  if (!entryAbs2 || !fs.existsSync(entryAbs2)) {
@@ -424,7 +448,7 @@ async function runDev({ rootDir, configPathAbs, config }) {
424
448
  printUrls(server.resolvedUrls, base2, ms);
425
449
  }
426
450
  return server;
427
- };
451
+ }, "startServer");
428
452
  viteServer = await startServer(config, { showBanner: true, showReady: true });
429
453
  if (typeof fs.watch === "function") {
430
454
  try {
@@ -451,6 +475,7 @@ ${c("[round]", "cyan")} ${c("config changed", "gray")} ${c("restarting dev serve
451
475
  }
452
476
  }
453
477
  }
478
+ __name(runDev, "runDev");
454
479
  async function runBuild({ rootDir, configPathAbs, config }) {
455
480
  const startedAt = Date.now();
456
481
  const configDir = path.dirname(configPathAbs);
@@ -491,6 +516,7 @@ async function runBuild({ rootDir, configPathAbs, config }) {
491
516
 
492
517
  `);
493
518
  }
519
+ __name(runBuild, "runBuild");
494
520
  async function runPreview({ rootDir, configPathAbs, config }) {
495
521
  const configDir = path.dirname(configPathAbs);
496
522
  const outDir = config?.output ? resolveFrom(configDir, config.output) : resolveFrom(rootDir, "./dist");
@@ -530,6 +556,7 @@ async function runPreview({ rootDir, configPathAbs, config }) {
530
556
  });
531
557
  printUrls(server.resolvedUrls, base);
532
558
  }
559
+ __name(runPreview, "runPreview");
533
560
  async function main() {
534
561
  const args = parseArgs(process.argv.slice(2));
535
562
  const cmd = args._[0];
@@ -567,6 +594,7 @@ ${String(e?.message ?? e)}`, { code: 1 });
567
594
  await runPreview({ rootDir, configPathAbs, config });
568
595
  }
569
596
  }
597
+ __name(main, "main");
570
598
  main().catch((e) => {
571
599
  if (e && e.name === "CliError") {
572
600
  printError(e.message);
package/dist/index.d.ts CHANGED
@@ -80,7 +80,8 @@ export const bindable: Bindable;
80
80
  export function untrack<T>(fn: () => T): T;
81
81
 
82
82
  /**
83
- * Create a reactive side-effect that runs whenever its signal dependencies change.
83
+ * Creates a reactive side-effect that runs whenever its signal dependencies change.
84
+ * Returns a function to manually stop the effect.
84
85
  */
85
86
  export function effect(fn: () => void | (() => void), options?: {
86
87
  /** If false, the effect won't run immediately on creation. Defaults to true. */
@@ -88,7 +89,7 @@ export function effect(fn: () => void | (() => void), options?: {
88
89
  }): () => void;
89
90
 
90
91
  /**
91
- * Create a reactive side-effect with explicit dependencies.
92
+ * Creates a reactive side-effect with explicit dependencies.
92
93
  */
93
94
  export function effect(deps: any[], fn: () => void | (() => void), options?: {
94
95
  /** If false, the effect won't run immediately on creation. Defaults to true. */
@@ -96,15 +97,40 @@ export function effect(deps: any[], fn: () => void | (() => void), options?: {
96
97
  }): () => void;
97
98
 
98
99
  /**
99
- * Create a read-only computed signal derived from other signals.
100
+ * Creates a read-only computed signal derived from other signals.
100
101
  */
101
102
  export function derive<T>(fn: () => T): () => T;
102
103
 
103
104
  /**
104
- * Create a read/write view of a specific path within a signal object.
105
+ * Creates a read/write view of a specific path within a signal object.
105
106
  */
106
107
  export function pick<T = any>(root: RoundSignal<any>, path: string | string[]): RoundSignal<T>;
107
108
 
109
+ /**
110
+ * Lifecycle Hooks
111
+ */
112
+
113
+ /**
114
+ * Runs a function when the component is mounted to the DOM.
115
+ * If the function returns another function, it will be used as an unmount cleanup.
116
+ */
117
+ export function onMount(fn: () => void | (() => void)): void;
118
+
119
+ /**
120
+ * Runs a function when the component is removed from the DOM.
121
+ */
122
+ export function onUnmount(fn: () => void): void;
123
+
124
+ /**
125
+ * Alias for onUnmount. Runs cleanup logic when the component is destroyed.
126
+ */
127
+ export function onCleanup(fn: () => void): void;
128
+
129
+ /**
130
+ * Runs a function after the component updates its DOM nodes.
131
+ */
132
+ export function onUpdate(fn: () => void): void;
133
+
108
134
  /**
109
135
  * Store API
110
136
  */
@@ -136,7 +162,7 @@ export interface RoundStore<T> {
136
162
  * Enable persistence for the store.
137
163
  */
138
164
  persist(storageKey: string, options?: {
139
- /** The storage implementation (defaults to localStorage). */
165
+ /** The storage provider (defaults to localStorage). */
140
166
  storage?: Storage;
141
167
  /** Debounce time in milliseconds for writes. */
142
168
  debounce?: number;
@@ -172,12 +198,12 @@ export interface RouteProps {
172
198
  description?: string;
173
199
  /** Advanced head configuration including links and meta tags. */
174
200
  head?: any;
175
- /** Fragment or elements to render when matched. */
201
+ /** Component or elements to render when matched. */
176
202
  children?: any;
177
203
  }
178
204
 
179
205
  /**
180
- * Define a route that renders its children when the path matches.
206
+ * Defines a route that renders its children when the path matches.
181
207
  */
182
208
  export function Route(props: RouteProps): any;
183
209
 
@@ -203,55 +229,55 @@ export interface LinkProps {
203
229
  }
204
230
 
205
231
  /**
206
- * A standard link component that performs SPA navigation.
232
+ * A link component that performs SPA navigation.
207
233
  */
208
234
  export function Link(props: LinkProps): any;
209
235
 
210
236
  /**
211
- * Define a fallback component or content for when no routes match.
237
+ * Defines a fallback component for when no routes match.
212
238
  */
213
239
  export function NotFound(props: {
214
- /** Optional component to render for the 404 state. */
240
+ /** Component to render for the 404 state. */
215
241
  component?: any;
216
- /** Fallback content. ignored if 'component' is provided. */
242
+ /** Fallback content. Ignored if 'component' is provided. */
217
243
  children?: any
218
244
  }): any;
219
245
 
220
246
  /**
221
- * Navigate to a different path programmatically.
247
+ * Navigates to a different path programmatically.
222
248
  */
223
249
  export function navigate(to: string, options?: {
224
- /** If true, replaces the current history entry instead of pushing. */
250
+ /** If true, replaces the current history entry. */
225
251
  replace?: boolean
226
252
  }): void;
227
253
 
228
254
  /**
229
- * Hook to get a reactive function returning the current normalized pathname.
255
+ * Returns a reactive function that returns the current normalized pathname.
230
256
  */
231
257
  export function usePathname(): () => string;
232
258
 
233
259
  /**
234
- * Get the current normalized pathname.
260
+ * Gets the current normalized pathname.
235
261
  */
236
262
  export function getPathname(): string;
237
263
 
238
264
  /**
239
- * Hook to get a reactive function returning the current location object.
265
+ * Returns a reactive function that returns the current location object.
240
266
  */
241
267
  export function useLocation(): () => { pathname: string; search: string; hash: string };
242
268
 
243
269
  /**
244
- * Get the current location object (pathname, search, hash).
270
+ * Gets the current location object (pathname, search, hash).
245
271
  */
246
272
  export function getLocation(): { pathname: string; search: string; hash: string };
247
273
 
248
274
  /**
249
- * Hook to get a reactive function returning whether the current path has no matches.
275
+ * Returns a reactive function that returns whether the current path has no matches.
250
276
  */
251
277
  export function useIsNotFound(): () => boolean;
252
278
 
253
279
  /**
254
- * Get whether the current path is NOT matched by any defined route.
280
+ * Gets whether the current path is NOT matched by any defined route.
255
281
  */
256
282
  export function getIsNotFound(): boolean;
257
283
 
@@ -260,7 +286,7 @@ export function getIsNotFound(): boolean;
260
286
  */
261
287
 
262
288
  /**
263
- * Create a DOM element or instance a component.
289
+ * Creates a DOM element or instances a component.
264
290
  */
265
291
  export function createElement(tag: any, props?: any, ...children: any[]): any;
266
292
 
@@ -272,19 +298,19 @@ export function Fragment(props: { children?: any }): any;
272
298
  export interface Context<T> {
273
299
  /** Internal identifier for the context. */
274
300
  id: number;
275
- /** Default value used when no Provider is found in the tree. */
301
+ /** Default value if no Provider is found. */
276
302
  defaultValue: T;
277
- /** Component that provides a value to all its descendants. */
303
+ /** Component that provides a value to descendants. */
278
304
  Provider: (props: { value: T; children?: any }) => any;
279
305
  }
280
306
 
281
307
  /**
282
- * Create a new Context object for sharing state between components.
308
+ * Creates a new Context for sharing state between components.
283
309
  */
284
310
  export function createContext<T>(defaultValue?: T): Context<T>;
285
311
 
286
312
  /**
287
- * Read the current value of a context from the component tree.
313
+ * Reads the current context value from the component tree.
288
314
  */
289
315
  export function readContext<T>(ctx: Context<T>): T;
290
316
 
@@ -293,27 +319,46 @@ export function readContext<T>(ctx: Context<T>): T;
293
319
  */
294
320
  export function bindContext<T>(ctx: Context<T>): () => T;
295
321
 
322
+ /**
323
+ * Error Handling
324
+ */
325
+
326
+ export interface ErrorBoundaryProps {
327
+ /** Content to render if an error occurs. Can be a signal or function. */
328
+ fallback?: any | ((props: { error: any }) => any);
329
+ /** Optional identifier for the boundary. */
330
+ name?: string;
331
+ /** Optional key that, when changed, resets the boundary error state. */
332
+ resetKey?: any;
333
+ /** Content that might throw an error. */
334
+ children?: any;
335
+ }
336
+
337
+ /**
338
+ * Component that catches runtime errors in its child tree and displays a fallback UI.
339
+ */
340
+ export function ErrorBoundary(props: ErrorBoundaryProps): any;
341
+
296
342
  /**
297
343
  * Async & Code Splitting
298
344
  */
299
345
 
300
346
  /**
301
- * Mark a component for lazy loading (code-splitting).
302
- * Expects a function returning a dynamic import promise.
347
+ * Marks a component for lazy loading (code-splitting).
303
348
  */
304
349
  export function lazy<T>(fn: () => Promise<{ default: T } | T>): any;
305
350
 
306
351
  declare module "*.round";
307
352
 
308
353
  export interface SuspenseProps {
309
- /** Content to show while children (e.g. lazy components) are loading. */
354
+ /** Content to show while children are loading. */
310
355
  fallback: any;
311
356
  /** Content that might trigger a loading state. */
312
357
  children?: any;
313
358
  }
314
359
 
315
360
  /**
316
- * Component that boundaries async operations and renders a fallback while loading.
361
+ * Component that renders a fallback UI while its children are loading.
317
362
  */
318
363
  export function Suspense(props: SuspenseProps): any;
319
364
 
@@ -322,20 +367,6 @@ export function Suspense(props: SuspenseProps): any;
322
367
  */
323
368
 
324
369
  /**
325
- * Define static head metadata (titles, meta tags, favicons, etc.).
326
- */
327
- export function startHead(head: any): any;
328
-
329
- /**
330
- * Markdown
331
- */
332
-
333
- /**
334
- * Component that renders Markdown content into HTML.
370
+ * Defines static head metadata (titles, meta tags, etc.).
335
371
  */
336
- export function Markdown(props: {
337
- /** The markdown string or a function returning it. */
338
- content: string | (() => string);
339
- /** Remark/Rehype configuration options. */
340
- options?: any
341
- }): any;
372
+ export function startHead(head: any): any;