@cajoa/core 0.10.1 → 0.12.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/dist/index.cjs CHANGED
@@ -20,10 +20,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ ErrorBoundary: () => ErrorBoundary,
23
24
  For: () => For,
24
25
  Fragment: () => Fragment,
25
26
  Show: () => Show,
26
27
  batch: () => batch,
28
+ catchError: () => catchError,
27
29
  createContext: () => createContext,
28
30
  createEffect: () => createEffect,
29
31
  createMemo: () => createMemo,
@@ -44,6 +46,19 @@ __export(index_exports, {
44
46
  });
45
47
  module.exports = __toCommonJS(index_exports);
46
48
 
49
+ // src/dev.ts
50
+ function isDev() {
51
+ return globalThis.__CAJOA_DEV__ === true;
52
+ }
53
+ function warn(message) {
54
+ if (isDev() && typeof console !== "undefined")
55
+ console.warn(`[cajoa] ${message}`);
56
+ }
57
+ function logError(message, error) {
58
+ if (isDev() && typeof console !== "undefined")
59
+ console.error(`[cajoa] ${message}`, error);
60
+ }
61
+
47
62
  // src/reactivity/signal.ts
48
63
  var currentOwner = null;
49
64
  var currentEffect = null;
@@ -57,6 +72,18 @@ function cleanComputation(c) {
57
72
  c.deps.clear();
58
73
  clean(c);
59
74
  }
75
+ function addCleanup(fn) {
76
+ currentOwner?.cleanups.push(fn);
77
+ }
78
+ function handleError(error, owner) {
79
+ for (let o = owner; o; o = o.parent) {
80
+ if (o.errorHandler) {
81
+ o.errorHandler(error);
82
+ return;
83
+ }
84
+ }
85
+ throw error;
86
+ }
60
87
  var batchDepth = 0;
61
88
  var pending = /* @__PURE__ */ new Set();
62
89
  function flush() {
@@ -109,20 +136,27 @@ function createEffect(fn) {
109
136
  currentEffect = effect;
110
137
  try {
111
138
  fn();
139
+ } catch (error) {
140
+ handleError(error, effect);
112
141
  } finally {
113
142
  currentOwner = prevOwner;
114
143
  currentEffect = prevEffect;
115
144
  }
116
145
  }
117
146
  };
118
- onCleanup(() => {
147
+ addCleanup(() => {
119
148
  effect.disposed = true;
120
149
  cleanComputation(effect);
121
150
  });
122
151
  effect.run();
123
152
  }
124
153
  function onCleanup(fn) {
125
- currentOwner?.cleanups.push(fn);
154
+ if (!currentOwner) {
155
+ warn(
156
+ "onCleanup() was called outside of a component or effect, so this cleanup will never run."
157
+ );
158
+ }
159
+ addCleanup(fn);
126
160
  }
127
161
  function createRoot(fn) {
128
162
  const root = { cleanups: [], parent: currentOwner };
@@ -146,12 +180,48 @@ function onMount(fn) {
146
180
  currentEffect = null;
147
181
  try {
148
182
  fn();
183
+ } catch (error) {
184
+ handleError(error, owner);
149
185
  } finally {
150
186
  currentOwner = prevOwner;
151
187
  currentEffect = prevEffect;
152
188
  }
153
189
  });
154
190
  }
191
+ function untrack(fn) {
192
+ const prev = currentEffect;
193
+ currentEffect = null;
194
+ try {
195
+ return fn();
196
+ } finally {
197
+ currentEffect = prev;
198
+ }
199
+ }
200
+ function catchError(fn, handler) {
201
+ const owner = {
202
+ cleanups: [],
203
+ parent: currentOwner,
204
+ errorHandler: handler
205
+ };
206
+ addCleanup(() => clean(owner));
207
+ const prevOwner = currentOwner;
208
+ currentOwner = owner;
209
+ let result;
210
+ let thrown = null;
211
+ try {
212
+ result = fn();
213
+ } catch (error) {
214
+ clean(owner);
215
+ thrown = { error };
216
+ } finally {
217
+ currentOwner = prevOwner;
218
+ }
219
+ if (thrown) {
220
+ handler(thrown.error);
221
+ return void 0;
222
+ }
223
+ return result;
224
+ }
155
225
  function createContext(defaultValue) {
156
226
  return { id: /* @__PURE__ */ Symbol("cajoa-context"), defaultValue };
157
227
  }
@@ -161,7 +231,7 @@ function provide(ctx, value, fn) {
161
231
  parent: currentOwner,
162
232
  context: /* @__PURE__ */ new Map([[ctx.id, value]])
163
233
  };
164
- onCleanup(() => clean(owner));
234
+ addCleanup(() => clean(owner));
165
235
  const prev = currentOwner;
166
236
  currentOwner = owner;
167
237
  try {
@@ -171,20 +241,16 @@ function provide(ctx, value, fn) {
171
241
  }
172
242
  }
173
243
  function useContext(ctx) {
244
+ if (!currentOwner) {
245
+ warn(
246
+ "useContext() was called outside of a component or effect, so it can only return the default value. Call it at the top of your component and use the value later."
247
+ );
248
+ }
174
249
  for (let o = currentOwner; o; o = o.parent) {
175
250
  if (o.context?.has(ctx.id)) return o.context.get(ctx.id);
176
251
  }
177
252
  return ctx.defaultValue;
178
253
  }
179
- function untrack(fn) {
180
- const prev = currentEffect;
181
- currentEffect = null;
182
- try {
183
- return fn();
184
- } finally {
185
- currentEffect = prev;
186
- }
187
- }
188
254
 
189
255
  // src/reactivity/memo.ts
190
256
  function createMemo(fn) {
@@ -273,6 +339,11 @@ function render(component, container) {
273
339
  return createRoot((dispose) => {
274
340
  const r = getRenderer();
275
341
  const node = component();
342
+ if (typeof node === "function") {
343
+ throw new Error(
344
+ "render(): the component returned a function (for example <Show>, <For>, <Router> or <ErrorBoundary>). Wrap it in an element, such as <div>\u2026</div>."
345
+ );
346
+ }
276
347
  r.clear(container);
277
348
  r.insert(container, node);
278
349
  return () => {
@@ -304,6 +375,11 @@ function For(props) {
304
375
  const nodes = [];
305
376
  items.forEach((item, i) => {
306
377
  const k = props.key ? props.key(item, i) : item;
378
+ if (next.has(k)) {
379
+ warn(
380
+ `<For> found a duplicate key (${String(k)}). Keys must be unique, otherwise one item replaces another.`
381
+ );
382
+ }
307
383
  let entry = cache.get(k);
308
384
  if (!entry) {
309
385
  entry = createRoot((dispose) => ({
@@ -322,6 +398,60 @@ function For(props) {
322
398
  };
323
399
  }
324
400
 
401
+ // src/components/error-boundary.ts
402
+ function resolve2(value) {
403
+ let current2 = value;
404
+ while (typeof current2 === "function") {
405
+ current2 = current2();
406
+ }
407
+ return current2;
408
+ }
409
+ function ErrorBoundary(props) {
410
+ const [version, setVersion] = createSignal(0);
411
+ let failure = null;
412
+ const caught = (error) => {
413
+ logError("ErrorBoundary caught an error:", error);
414
+ props.onError?.(error);
415
+ };
416
+ const reset = () => {
417
+ failure = null;
418
+ setVersion((v) => v + 1);
419
+ };
420
+ const fail = (error) => {
421
+ caught(error);
422
+ failure = { error };
423
+ setVersion((v) => v + 1);
424
+ };
425
+ const showFallback = (error) => typeof props.fallback === "function" ? props.fallback(
426
+ error,
427
+ reset
428
+ ) : props.fallback;
429
+ return () => {
430
+ version();
431
+ const current2 = failure;
432
+ if (current2) return untrack(() => showFallback(current2.error));
433
+ let creating = true;
434
+ let early = null;
435
+ const content = untrack(
436
+ () => catchError(
437
+ () => resolve2(props.children ?? null),
438
+ (error) => {
439
+ if (creating) early = { error };
440
+ else fail(error);
441
+ }
442
+ )
443
+ );
444
+ creating = false;
445
+ const first = early;
446
+ if (first) {
447
+ caught(first.error);
448
+ failure = first;
449
+ return untrack(() => showFallback(first.error));
450
+ }
451
+ return content;
452
+ };
453
+ }
454
+
325
455
  // src/jsx-runtime.ts
326
456
  function jsx(type, props) {
327
457
  if (typeof type === "function") return type(props ?? {});
@@ -338,10 +468,12 @@ function Fragment(props) {
338
468
  }
339
469
  // Annotate the CommonJS export names for ESM import in node:
340
470
  0 && (module.exports = {
471
+ ErrorBoundary,
341
472
  For,
342
473
  Fragment,
343
474
  Show,
344
475
  batch,
476
+ catchError,
345
477
  createContext,
346
478
  createEffect,
347
479
  createMemo,
package/dist/index.d.cts CHANGED
@@ -5,6 +5,8 @@ declare function createEffect(fn: () => void): void;
5
5
  declare function onCleanup(fn: Cleanup): void;
6
6
  declare function createRoot<T>(fn: (dispose: () => void) => T): T;
7
7
  declare function onMount(fn: () => void): void;
8
+ declare function untrack<T>(fn: () => T): T;
9
+ declare function catchError<T>(fn: () => T, handler: (error: unknown) => void): T | undefined;
8
10
  interface Context<T> {
9
11
  readonly id: symbol;
10
12
  readonly defaultValue: T;
@@ -12,7 +14,6 @@ interface Context<T> {
12
14
  declare function createContext<T>(defaultValue: T): Context<T>;
13
15
  declare function provide<T, R>(ctx: Context<T>, value: T, fn: () => R): R;
14
16
  declare function useContext<T>(ctx: Context<T>): T;
15
- declare function untrack<T>(fn: () => T): T;
16
17
 
17
18
  declare function createMemo<T>(fn: () => T): () => T;
18
19
 
@@ -36,11 +37,11 @@ declare function h(tag: string, props: Record<string, any> | null, ...children:
36
37
 
37
38
  declare function render(component: () => HostNode, container: HostNode): () => void;
38
39
 
39
- type Lazy<T> = T | (() => T);
40
+ type Lazy$1<T> = T | (() => T);
40
41
  declare function Show<T>(props: {
41
- when: Lazy<T>;
42
- children?: Lazy<Child>;
43
- fallback?: Lazy<Child>;
42
+ when: Lazy$1<T>;
43
+ children?: Lazy$1<Child>;
44
+ fallback?: Lazy$1<Child>;
44
45
  }): () => string | number | boolean | object | null;
45
46
 
46
47
  declare function For<T>(props: {
@@ -49,6 +50,13 @@ declare function For<T>(props: {
49
50
  key?: (item: T, index: number) => unknown;
50
51
  }): () => object[];
51
52
 
53
+ type Lazy<T> = T | (() => T);
54
+ declare function ErrorBoundary(props: {
55
+ fallback: Child | ((error: unknown, reset: () => void) => Child);
56
+ children?: Lazy<Child>;
57
+ onError?: (error: unknown) => void;
58
+ }): () => Child;
59
+
52
60
  declare function jsx(type: any, props: Record<string, any> | null): any;
53
61
  declare const jsxs: typeof jsx;
54
62
  declare const jsxDEV: typeof jsx;
@@ -68,5 +76,5 @@ declare function Fragment(props: {
68
76
  declare function setRenderer(renderer: Renderer): void;
69
77
  declare function getRenderer(): Renderer;
70
78
 
71
- export { For, Fragment, JSX, Show, batch, createContext, createEffect, createMemo, createRoot, createSignal, getRenderer, h, jsx, jsxDEV, jsxs, onCleanup, onMount, provide, render, setRenderer, untrack, useContext };
79
+ export { ErrorBoundary, For, Fragment, JSX, Show, batch, catchError, createContext, createEffect, createMemo, createRoot, createSignal, getRenderer, h, jsx, jsxDEV, jsxs, onCleanup, onMount, provide, render, setRenderer, untrack, useContext };
72
80
  export type { Child, Context, HostNode, Renderer };
package/dist/index.d.ts CHANGED
@@ -5,6 +5,8 @@ declare function createEffect(fn: () => void): void;
5
5
  declare function onCleanup(fn: Cleanup): void;
6
6
  declare function createRoot<T>(fn: (dispose: () => void) => T): T;
7
7
  declare function onMount(fn: () => void): void;
8
+ declare function untrack<T>(fn: () => T): T;
9
+ declare function catchError<T>(fn: () => T, handler: (error: unknown) => void): T | undefined;
8
10
  interface Context<T> {
9
11
  readonly id: symbol;
10
12
  readonly defaultValue: T;
@@ -12,7 +14,6 @@ interface Context<T> {
12
14
  declare function createContext<T>(defaultValue: T): Context<T>;
13
15
  declare function provide<T, R>(ctx: Context<T>, value: T, fn: () => R): R;
14
16
  declare function useContext<T>(ctx: Context<T>): T;
15
- declare function untrack<T>(fn: () => T): T;
16
17
 
17
18
  declare function createMemo<T>(fn: () => T): () => T;
18
19
 
@@ -36,11 +37,11 @@ declare function h(tag: string, props: Record<string, any> | null, ...children:
36
37
 
37
38
  declare function render(component: () => HostNode, container: HostNode): () => void;
38
39
 
39
- type Lazy<T> = T | (() => T);
40
+ type Lazy$1<T> = T | (() => T);
40
41
  declare function Show<T>(props: {
41
- when: Lazy<T>;
42
- children?: Lazy<Child>;
43
- fallback?: Lazy<Child>;
42
+ when: Lazy$1<T>;
43
+ children?: Lazy$1<Child>;
44
+ fallback?: Lazy$1<Child>;
44
45
  }): () => string | number | boolean | object | null;
45
46
 
46
47
  declare function For<T>(props: {
@@ -49,6 +50,13 @@ declare function For<T>(props: {
49
50
  key?: (item: T, index: number) => unknown;
50
51
  }): () => object[];
51
52
 
53
+ type Lazy<T> = T | (() => T);
54
+ declare function ErrorBoundary(props: {
55
+ fallback: Child | ((error: unknown, reset: () => void) => Child);
56
+ children?: Lazy<Child>;
57
+ onError?: (error: unknown) => void;
58
+ }): () => Child;
59
+
52
60
  declare function jsx(type: any, props: Record<string, any> | null): any;
53
61
  declare const jsxs: typeof jsx;
54
62
  declare const jsxDEV: typeof jsx;
@@ -68,5 +76,5 @@ declare function Fragment(props: {
68
76
  declare function setRenderer(renderer: Renderer): void;
69
77
  declare function getRenderer(): Renderer;
70
78
 
71
- export { For, Fragment, JSX, Show, batch, createContext, createEffect, createMemo, createRoot, createSignal, getRenderer, h, jsx, jsxDEV, jsxs, onCleanup, onMount, provide, render, setRenderer, untrack, useContext };
79
+ export { ErrorBoundary, For, Fragment, JSX, Show, batch, catchError, createContext, createEffect, createMemo, createRoot, createSignal, getRenderer, h, jsx, jsxDEV, jsxs, onCleanup, onMount, provide, render, setRenderer, untrack, useContext };
72
80
  export type { Child, Context, HostNode, Renderer };
package/dist/index.js CHANGED
@@ -1,3 +1,16 @@
1
+ // src/dev.ts
2
+ function isDev() {
3
+ return globalThis.__CAJOA_DEV__ === true;
4
+ }
5
+ function warn(message) {
6
+ if (isDev() && typeof console !== "undefined")
7
+ console.warn(`[cajoa] ${message}`);
8
+ }
9
+ function logError(message, error) {
10
+ if (isDev() && typeof console !== "undefined")
11
+ console.error(`[cajoa] ${message}`, error);
12
+ }
13
+
1
14
  // src/reactivity/signal.ts
2
15
  var currentOwner = null;
3
16
  var currentEffect = null;
@@ -11,6 +24,18 @@ function cleanComputation(c) {
11
24
  c.deps.clear();
12
25
  clean(c);
13
26
  }
27
+ function addCleanup(fn) {
28
+ currentOwner?.cleanups.push(fn);
29
+ }
30
+ function handleError(error, owner) {
31
+ for (let o = owner; o; o = o.parent) {
32
+ if (o.errorHandler) {
33
+ o.errorHandler(error);
34
+ return;
35
+ }
36
+ }
37
+ throw error;
38
+ }
14
39
  var batchDepth = 0;
15
40
  var pending = /* @__PURE__ */ new Set();
16
41
  function flush() {
@@ -63,20 +88,27 @@ function createEffect(fn) {
63
88
  currentEffect = effect;
64
89
  try {
65
90
  fn();
91
+ } catch (error) {
92
+ handleError(error, effect);
66
93
  } finally {
67
94
  currentOwner = prevOwner;
68
95
  currentEffect = prevEffect;
69
96
  }
70
97
  }
71
98
  };
72
- onCleanup(() => {
99
+ addCleanup(() => {
73
100
  effect.disposed = true;
74
101
  cleanComputation(effect);
75
102
  });
76
103
  effect.run();
77
104
  }
78
105
  function onCleanup(fn) {
79
- currentOwner?.cleanups.push(fn);
106
+ if (!currentOwner) {
107
+ warn(
108
+ "onCleanup() was called outside of a component or effect, so this cleanup will never run."
109
+ );
110
+ }
111
+ addCleanup(fn);
80
112
  }
81
113
  function createRoot(fn) {
82
114
  const root = { cleanups: [], parent: currentOwner };
@@ -100,12 +132,48 @@ function onMount(fn) {
100
132
  currentEffect = null;
101
133
  try {
102
134
  fn();
135
+ } catch (error) {
136
+ handleError(error, owner);
103
137
  } finally {
104
138
  currentOwner = prevOwner;
105
139
  currentEffect = prevEffect;
106
140
  }
107
141
  });
108
142
  }
143
+ function untrack(fn) {
144
+ const prev = currentEffect;
145
+ currentEffect = null;
146
+ try {
147
+ return fn();
148
+ } finally {
149
+ currentEffect = prev;
150
+ }
151
+ }
152
+ function catchError(fn, handler) {
153
+ const owner = {
154
+ cleanups: [],
155
+ parent: currentOwner,
156
+ errorHandler: handler
157
+ };
158
+ addCleanup(() => clean(owner));
159
+ const prevOwner = currentOwner;
160
+ currentOwner = owner;
161
+ let result;
162
+ let thrown = null;
163
+ try {
164
+ result = fn();
165
+ } catch (error) {
166
+ clean(owner);
167
+ thrown = { error };
168
+ } finally {
169
+ currentOwner = prevOwner;
170
+ }
171
+ if (thrown) {
172
+ handler(thrown.error);
173
+ return void 0;
174
+ }
175
+ return result;
176
+ }
109
177
  function createContext(defaultValue) {
110
178
  return { id: /* @__PURE__ */ Symbol("cajoa-context"), defaultValue };
111
179
  }
@@ -115,7 +183,7 @@ function provide(ctx, value, fn) {
115
183
  parent: currentOwner,
116
184
  context: /* @__PURE__ */ new Map([[ctx.id, value]])
117
185
  };
118
- onCleanup(() => clean(owner));
186
+ addCleanup(() => clean(owner));
119
187
  const prev = currentOwner;
120
188
  currentOwner = owner;
121
189
  try {
@@ -125,20 +193,16 @@ function provide(ctx, value, fn) {
125
193
  }
126
194
  }
127
195
  function useContext(ctx) {
196
+ if (!currentOwner) {
197
+ warn(
198
+ "useContext() was called outside of a component or effect, so it can only return the default value. Call it at the top of your component and use the value later."
199
+ );
200
+ }
128
201
  for (let o = currentOwner; o; o = o.parent) {
129
202
  if (o.context?.has(ctx.id)) return o.context.get(ctx.id);
130
203
  }
131
204
  return ctx.defaultValue;
132
205
  }
133
- function untrack(fn) {
134
- const prev = currentEffect;
135
- currentEffect = null;
136
- try {
137
- return fn();
138
- } finally {
139
- currentEffect = prev;
140
- }
141
- }
142
206
 
143
207
  // src/reactivity/memo.ts
144
208
  function createMemo(fn) {
@@ -227,6 +291,11 @@ function render(component, container) {
227
291
  return createRoot((dispose) => {
228
292
  const r = getRenderer();
229
293
  const node = component();
294
+ if (typeof node === "function") {
295
+ throw new Error(
296
+ "render(): the component returned a function (for example <Show>, <For>, <Router> or <ErrorBoundary>). Wrap it in an element, such as <div>\u2026</div>."
297
+ );
298
+ }
230
299
  r.clear(container);
231
300
  r.insert(container, node);
232
301
  return () => {
@@ -258,6 +327,11 @@ function For(props) {
258
327
  const nodes = [];
259
328
  items.forEach((item, i) => {
260
329
  const k = props.key ? props.key(item, i) : item;
330
+ if (next.has(k)) {
331
+ warn(
332
+ `<For> found a duplicate key (${String(k)}). Keys must be unique, otherwise one item replaces another.`
333
+ );
334
+ }
261
335
  let entry = cache.get(k);
262
336
  if (!entry) {
263
337
  entry = createRoot((dispose) => ({
@@ -276,6 +350,60 @@ function For(props) {
276
350
  };
277
351
  }
278
352
 
353
+ // src/components/error-boundary.ts
354
+ function resolve2(value) {
355
+ let current2 = value;
356
+ while (typeof current2 === "function") {
357
+ current2 = current2();
358
+ }
359
+ return current2;
360
+ }
361
+ function ErrorBoundary(props) {
362
+ const [version, setVersion] = createSignal(0);
363
+ let failure = null;
364
+ const caught = (error) => {
365
+ logError("ErrorBoundary caught an error:", error);
366
+ props.onError?.(error);
367
+ };
368
+ const reset = () => {
369
+ failure = null;
370
+ setVersion((v) => v + 1);
371
+ };
372
+ const fail = (error) => {
373
+ caught(error);
374
+ failure = { error };
375
+ setVersion((v) => v + 1);
376
+ };
377
+ const showFallback = (error) => typeof props.fallback === "function" ? props.fallback(
378
+ error,
379
+ reset
380
+ ) : props.fallback;
381
+ return () => {
382
+ version();
383
+ const current2 = failure;
384
+ if (current2) return untrack(() => showFallback(current2.error));
385
+ let creating = true;
386
+ let early = null;
387
+ const content = untrack(
388
+ () => catchError(
389
+ () => resolve2(props.children ?? null),
390
+ (error) => {
391
+ if (creating) early = { error };
392
+ else fail(error);
393
+ }
394
+ )
395
+ );
396
+ creating = false;
397
+ const first = early;
398
+ if (first) {
399
+ caught(first.error);
400
+ failure = first;
401
+ return untrack(() => showFallback(first.error));
402
+ }
403
+ return content;
404
+ };
405
+ }
406
+
279
407
  // src/jsx-runtime.ts
280
408
  function jsx(type, props) {
281
409
  if (typeof type === "function") return type(props ?? {});
@@ -291,10 +419,12 @@ function Fragment(props) {
291
419
  return frag;
292
420
  }
293
421
  export {
422
+ ErrorBoundary,
294
423
  For,
295
424
  Fragment,
296
425
  Show,
297
426
  batch,
427
+ catchError,
298
428
  createContext,
299
429
  createEffect,
300
430
  createMemo,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cajoa/core",
3
- "version": "0.10.1",
3
+ "version": "0.12.0",
4
4
  "description": "Cajoa core: signals, JSX runtime and the renderer interface.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",