cajoa 0.1.0 → 0.3.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/README.md CHANGED
@@ -1,14 +1,15 @@
1
1
  # Cajoa
2
2
 
3
- A small, beginner-friendly frontend framework built on **signals**.
3
+ A small, beginner-friendly frontend framework built on **signals** and **JSX**.
4
4
  No virtual DOM, no hooks rules, no re-rendering components.
5
5
 
6
- > ⚠️ **v0.1 (early stage).** The API will change. Not production-ready yet.
6
+ > ⚠️ **v0.3 (early stage).** The API may still change. Not production-ready yet.
7
7
 
8
8
  ## Why Cajoa?
9
9
 
10
10
  - **Components run once.** A component is just a function that builds DOM. Only the parts that depend on a signal update.
11
- - **Tiny and readable.** The whole core is a few hundred lines you can read in one sitting.
11
+ - **Plain JSX.** Write `<button onClick={...}>`, no special syntax to learn.
12
+ - **Tiny and readable.** The whole core is small enough to read in one sitting.
12
13
  - **TypeScript-first**, ESM + CommonJS, zero runtime dependencies.
13
14
 
14
15
  ## Install
@@ -17,60 +18,135 @@ No virtual DOM, no hooks rules, no re-rendering components.
17
18
  npm install cajoa
18
19
  ```
19
20
 
21
+ ## Setup (JSX)
22
+
23
+ In your `tsconfig.json`:
24
+
25
+ ```json
26
+ {
27
+ "compilerOptions": {
28
+ "jsx": "react-jsx",
29
+ "jsxImportSource": "cajoa"
30
+ }
31
+ }
32
+ ```
33
+
34
+ If you use Vite and your JSX is not transformed, also set it in `vite.config.ts`
35
+ (`oxc: { jsx: { runtime: "automatic", importSource: "cajoa" } }` on newer Vite,
36
+ or `esbuild: { jsx: "automatic", jsxImportSource: "cajoa" }` on older ones).
37
+
20
38
  ## Quick start
21
39
 
22
- ```ts
23
- import { createSignal, h, render } from "cajoa";
40
+ ```tsx
41
+ import { createSignal, render } from "cajoa";
24
42
 
25
43
  function Counter() {
26
44
  const [count, setCount] = createSignal(0);
27
45
 
28
- return h(
29
- "button",
30
- { onClick: () => setCount((c) => c + 1) },
31
- "Clicked ",
32
- count,
33
- " times",
46
+ return (
47
+ <button onClick={() => setCount((c) => c + 1)}>
48
+ Clicked {count} times
49
+ </button>
34
50
  );
35
51
  }
36
52
 
37
53
  render(Counter, document.getElementById("app")!);
38
54
  ```
39
55
 
40
- Passing `count` (the function itself, not `count()`) is what makes that part of the DOM reactive.
56
+ ## Two rules to remember
57
+
58
+ Cajoa has no compiler, so JSX expressions run exactly once. That gives two rules:
59
+
60
+ 1. **Pass the signal, don't call it.**
61
+ `{count}` is reactive. `{count()}` is read once and never updates.
62
+ For computed values, pass a function: `{() => count() * 2}`.
63
+ 2. **`Show` children are a function.**
64
+ `<Show when={on}>{() => <p>Hello</p>}</Show>`
65
+ Otherwise the element is created even when it is hidden.
66
+
67
+ ## Lists and conditions
68
+
69
+ ```tsx
70
+ import { createSignal, Show, For } from "cajoa";
71
+
72
+ const [todos, setTodos] = createSignal([{ id: 1, text: "Learn Cajoa" }]);
73
+
74
+ <Show when={() => todos().length > 0} fallback={() => <p>Nothing to do.</p>}>
75
+ {() => (
76
+ <ul>
77
+ <For each={todos}>{(todo) => <li>{todo.text}</li>}</For>
78
+ </ul>
79
+ )}
80
+ </Show>;
81
+ ```
41
82
 
42
83
  ## API
43
84
 
44
- | Function | What it does |
45
- | ------------------------------------ | ------------------------------------------------------------------------ |
46
- | `createSignal(value)` | Returns `[get, set]`. Reading `get()` inside an effect subscribes to it. |
47
- | `createEffect(fn)` | Runs `fn` now and again whenever a signal it read changes. |
48
- | `h(tag, props, ...children)` | Creates a real DOM element. Function props/children are reactive. |
49
- | `render(component, container)` | Mounts a component. Returns a `dispose` function. |
50
- | `Show({ when, children, fallback })` | Conditional rendering. |
51
- | `For({ each, children })` | List rendering. |
52
- | `onMount(fn)` | Runs `fn` after the component is rendered. |
53
- | `onCleanup(fn)` | Runs `fn` before an effect re-runs or when its owner is disposed. |
54
- | `createRoot(fn)` | Creates a top-level owner you can dispose manually. |
85
+ ### Reactivity
86
+
87
+ | Function | What it does |
88
+ | --------------------- | ---------------------------------------------------------------------------------------------------------------------- |
89
+ | `createSignal(value)` | Returns `[get, set]`. Reading `get()` inside an effect subscribes to it. `set` accepts a value or an updater function. |
90
+ | `createEffect(fn)` | Runs `fn` now and again whenever a signal it read changes. |
91
+ | `createMemo(fn)` | Cached computed value. Only notifies when the result changes. |
92
+ | `batch(fn)` | Groups several `set` calls into one update. |
93
+
94
+ ### Components
95
+
96
+ | Component | What it does |
97
+ | ---------------------- | ---------------------------------------------------------------------------------------------------- |
98
+ | `<Show when fallback>` | Conditional rendering. `children` and `fallback` are functions. |
99
+ | `<For each key?>` | List rendering. Reuses DOM nodes for unchanged items. The child is a function that returns a `Node`. |
100
+
101
+ ### Rendering
102
+
103
+ | Function | What it does |
104
+ | ------------------------------ | ------------------------------------------------- |
105
+ | `render(component, container)` | Mounts a component. Returns a `dispose` function. |
106
+ | `h(tag, props, ...children)` | Low-level element creator. JSX compiles to this. |
107
+
108
+ ### Lifecycle
109
+
110
+ | Function | What it does |
111
+ | ---------------- | ----------------------------------------------------------------- |
112
+ | `onMount(fn)` | Runs `fn` after the component is rendered. |
113
+ | `onCleanup(fn)` | Runs `fn` before an effect re-runs or when its owner is disposed. |
114
+ | `createRoot(fn)` | Creates a top-level owner you can dispose manually. |
115
+
116
+ ### Context
117
+
118
+ | Function | What it does |
119
+ | ------------------------- | ------------------------------------------------------ |
120
+ | `createContext(default)` | Creates a context with a default value. |
121
+ | `provide(ctx, value, fn)` | Runs `fn`; everything created inside can read `value`. |
122
+ | `useContext(ctx)` | Reads the nearest provided value (or the default). |
123
+
124
+ ## Good to know
125
+
126
+ - `For` uses the item itself as the key by default. If you pass `key`, items with the same key are **not** re-rendered when their data changes, so keys must be unique and the item should hold its own signals if it needs to update.
127
+ - `useContext` only works while a component is being created (or inside `onMount` / effects). Read it at the top of your component and use the value later in handlers.
128
+ - Updating an object in place will not trigger effects. Set a new object instead.
55
129
 
56
130
  ## Known limitations
57
131
 
58
- - `For` rebuilds the whole list on every change (keyed diffing is planned).
59
- - No `createMemo`, `batch`, or context yet.
60
- - No JSX support yet (`h()` only).
132
+ - No Fragments (`<>...</>`) yet. Wrap siblings in an element.
61
133
  - No SSR.
134
+ - `createMemo` can show a brief inconsistent state when memos depend on each other.
135
+ - `set` calls are not batched automatically inside event handlers. Wrap them in `batch()`.
62
136
 
63
137
  ## Roadmap
64
138
 
65
139
  - [x] Signals and effects
66
- - [x] DOM bindings (`h`, `render`)
67
- - [x] `Show`, `For`
68
- - [x] Lifecycle (`onMount`, `onCleanup`) and disposal
69
- - [ ] `createMemo`, `batch`
70
- - [ ] Keyed `For`
71
- - [ ] Context
72
- - [ ] JSX support
140
+ - [x] DOM bindings and `render`
141
+ - [x] `Show`, `For` (keyed)
142
+ - [x] Lifecycle and disposal
143
+ - [x] `createMemo`, `batch`
144
+ - [x] Context
145
+ - [x] JSX support
146
+ - [ ] Fragments
147
+ - [ ] Vite plugin (removes the two rules above)
73
148
  - [ ] Router
149
+ - [ ] SSR
74
150
 
75
151
  ## Development
76
152
 
package/dist/index.cjs CHANGED
@@ -22,13 +22,21 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  For: () => For,
24
24
  Show: () => Show,
25
+ batch: () => batch,
26
+ createContext: () => createContext,
25
27
  createEffect: () => createEffect,
28
+ createMemo: () => createMemo,
26
29
  createRoot: () => createRoot,
27
30
  createSignal: () => createSignal,
28
31
  h: () => h,
32
+ jsx: () => jsx,
33
+ jsxDEV: () => jsxDEV,
34
+ jsxs: () => jsxs,
29
35
  onCleanup: () => onCleanup,
30
36
  onMount: () => onMount,
31
- render: () => render
37
+ provide: () => provide,
38
+ render: () => render,
39
+ useContext: () => useContext
32
40
  });
33
41
  module.exports = __toCommonJS(index_exports);
34
42
 
@@ -45,6 +53,22 @@ function cleanComputation(c) {
45
53
  c.deps.clear();
46
54
  clean(c);
47
55
  }
56
+ var batchDepth = 0;
57
+ var pending = /* @__PURE__ */ new Set();
58
+ function flush() {
59
+ const effects = [...pending];
60
+ pending.clear();
61
+ effects.forEach((effect) => effect.run());
62
+ }
63
+ function batch(fn) {
64
+ batchDepth++;
65
+ try {
66
+ return fn();
67
+ } finally {
68
+ batchDepth--;
69
+ if (batchDepth === 0) flush();
70
+ }
71
+ }
48
72
  function createSignal(value) {
49
73
  const subs = /* @__PURE__ */ new Set();
50
74
  const get = () => {
@@ -58,13 +82,18 @@ function createSignal(value) {
58
82
  const v = typeof next === "function" ? next(value) : next;
59
83
  if (Object.is(v, value)) return;
60
84
  value = v;
61
- [...subs].forEach((effect) => effect.run());
85
+ for (const effect of [...subs]) {
86
+ if (batchDepth > 0)
87
+ pending.add(effect);
88
+ else effect.run();
89
+ }
62
90
  };
63
91
  return [get, set];
64
92
  }
65
93
  function createEffect(fn) {
66
94
  const effect = {
67
95
  cleanups: [],
96
+ parent: currentOwner,
68
97
  deps: /* @__PURE__ */ new Set(),
69
98
  disposed: false,
70
99
  run() {
@@ -92,7 +121,7 @@ function onCleanup(fn) {
92
121
  currentOwner?.cleanups.push(fn);
93
122
  }
94
123
  function createRoot(fn) {
95
- const root = { cleanups: [] };
124
+ const root = { cleanups: [], parent: currentOwner };
96
125
  const prevOwner = currentOwner;
97
126
  const prevEffect = currentEffect;
98
127
  currentOwner = root;
@@ -119,6 +148,30 @@ function onMount(fn) {
119
148
  }
120
149
  });
121
150
  }
151
+ function createContext(defaultValue) {
152
+ return { id: /* @__PURE__ */ Symbol("cajoa-context"), defaultValue };
153
+ }
154
+ function provide(ctx, value, fn) {
155
+ const owner = {
156
+ cleanups: [],
157
+ parent: currentOwner,
158
+ context: /* @__PURE__ */ new Map([[ctx.id, value]])
159
+ };
160
+ onCleanup(() => clean(owner));
161
+ const prev = currentOwner;
162
+ currentOwner = owner;
163
+ try {
164
+ return fn();
165
+ } finally {
166
+ currentOwner = prev;
167
+ }
168
+ }
169
+ function useContext(ctx) {
170
+ for (let o = currentOwner; o; o = o.parent) {
171
+ if (o.context?.has(ctx.id)) return o.context.get(ctx.id);
172
+ }
173
+ return ctx.defaultValue;
174
+ }
122
175
 
123
176
  // src/dom/h.ts
124
177
  function h(tag, props, ...children) {
@@ -152,12 +205,12 @@ function toNodes(value) {
152
205
  return out;
153
206
  }
154
207
  function appendChildren(parent, children) {
155
- const pending = [...children].reverse();
156
- while (pending.length > 0) {
157
- const child = pending.pop();
208
+ const pending2 = [...children].reverse();
209
+ while (pending2.length > 0) {
210
+ const child = pending2.pop();
158
211
  if (Array.isArray(child)) {
159
212
  for (let i = child.length - 1; i >= 0; i--) {
160
- pending.push(child[i]);
213
+ pending2.push(child[i]);
161
214
  }
162
215
  continue;
163
216
  }
@@ -166,17 +219,29 @@ function appendChildren(parent, children) {
166
219
  const end = document.createComment("");
167
220
  parent.append(start, end);
168
221
  createEffect(() => {
169
- const nodes = toNodes(child());
170
- while (start.nextSibling && start.nextSibling !== end) {
171
- start.nextSibling.remove();
172
- }
173
- end.before(...nodes);
222
+ reconcile(start, end, toNodes(child()));
174
223
  });
175
224
  } else {
176
225
  parent.append(...toNodes(child));
177
226
  }
178
227
  }
179
228
  }
229
+ function reconcile(start, end, next) {
230
+ const keep = new Set(next);
231
+ let cur = start.nextSibling;
232
+ while (cur && cur !== end) {
233
+ const after = cur.nextSibling;
234
+ if (!keep.has(cur)) cur.remove();
235
+ cur = after;
236
+ }
237
+ const parent = end.parentNode;
238
+ let ref = start.nextSibling;
239
+ for (const node of next) {
240
+ if (node === ref)
241
+ ref = ref.nextSibling;
242
+ else parent.insertBefore(node, ref);
243
+ }
244
+ }
180
245
 
181
246
  // src/dom/render.ts
182
247
  function render(component, container) {
@@ -196,17 +261,68 @@ function Show(props) {
196
261
 
197
262
  // src/components/for.ts
198
263
  function For(props) {
199
- return () => props.each().map((item, i) => props.children(item, i));
264
+ let cache = /* @__PURE__ */ new Map();
265
+ onCleanup(() => {
266
+ cache.forEach((e) => e.dispose());
267
+ cache.clear();
268
+ });
269
+ return () => {
270
+ const items = props.each();
271
+ const next = /* @__PURE__ */ new Map();
272
+ const nodes = [];
273
+ items.forEach((item, i) => {
274
+ const k = props.key ? props.key(item, i) : item;
275
+ let entry = cache.get(k);
276
+ if (!entry) {
277
+ entry = createRoot((dispose) => ({
278
+ node: props.children(item, i),
279
+ dispose
280
+ }));
281
+ }
282
+ next.set(k, entry);
283
+ nodes.push(entry.node);
284
+ });
285
+ cache.forEach((entry, k) => {
286
+ if (!next.has(k)) entry.dispose();
287
+ });
288
+ cache = next;
289
+ return nodes;
290
+ };
291
+ }
292
+
293
+ // src/reactivity/memo.ts
294
+ function createMemo(fn) {
295
+ const [value, setValue] = createSignal(void 0);
296
+ createEffect(() => setValue(() => fn()));
297
+ return value;
298
+ }
299
+
300
+ // src/jsx-runtime.ts
301
+ function jsx(type, props) {
302
+ if (typeof type === "function") return type(props ?? {});
303
+ const { children, ...rest } = props ?? {};
304
+ const kids = children === void 0 ? [] : [children];
305
+ return h(type, rest, ...kids);
200
306
  }
307
+ var jsxs = jsx;
308
+ var jsxDEV = jsx;
201
309
  // Annotate the CommonJS export names for ESM import in node:
202
310
  0 && (module.exports = {
203
311
  For,
204
312
  Show,
313
+ batch,
314
+ createContext,
205
315
  createEffect,
316
+ createMemo,
206
317
  createRoot,
207
318
  createSignal,
208
319
  h,
320
+ jsx,
321
+ jsxDEV,
322
+ jsxs,
209
323
  onCleanup,
210
324
  onMount,
211
- render
325
+ provide,
326
+ render,
327
+ useContext
212
328
  });
package/dist/index.d.cts CHANGED
@@ -1,9 +1,17 @@
1
1
  type Cleanup = () => void;
2
+ declare function batch<T>(fn: () => T): T;
2
3
  declare function createSignal<T>(value: T): readonly [() => T, (next: T | ((prev: T) => T)) => void];
3
4
  declare function createEffect(fn: () => void): void;
4
5
  declare function onCleanup(fn: Cleanup): void;
5
6
  declare function createRoot<T>(fn: (dispose: () => void) => T): T;
6
7
  declare function onMount(fn: () => void): void;
8
+ interface Context<T> {
9
+ readonly id: symbol;
10
+ readonly defaultValue: T;
11
+ }
12
+ declare function createContext<T>(defaultValue: T): Context<T>;
13
+ declare function provide<T, R>(ctx: Context<T>, value: T, fn: () => R): R;
14
+ declare function useContext<T>(ctx: Context<T>): T;
7
15
 
8
16
  type Child = string | number | boolean | null | undefined | Node | (() => unknown) | Child[];
9
17
  declare function h(tag: string, props: Record<string, any> | null, ...children: Child[]): HTMLElement;
@@ -18,8 +26,24 @@ declare function Show<T>(props: {
18
26
 
19
27
  declare function For<T>(props: {
20
28
  each: () => T[];
21
- children: (item: T, index: number) => Child;
22
- }): () => Child[];
29
+ children: (item: T, index: number) => Node;
30
+ key?: (item: T, index: number) => unknown;
31
+ }): () => Node[];
23
32
 
24
- export { For, Show, createEffect, createRoot, createSignal, h, onCleanup, onMount, render };
25
- export type { Child };
33
+ declare function createMemo<T>(fn: () => T): () => T;
34
+
35
+ declare function jsx(type: any, props: Record<string, any> | null): any;
36
+ declare const jsxs: typeof jsx;
37
+ declare const jsxDEV: typeof jsx;
38
+ declare namespace JSX {
39
+ type Element = any;
40
+ interface IntrinsicElements {
41
+ [tag: string]: any;
42
+ }
43
+ interface ElementChildrenAttribute {
44
+ children: {};
45
+ }
46
+ }
47
+
48
+ export { For, JSX, Show, batch, createContext, createEffect, createMemo, createRoot, createSignal, h, jsx, jsxDEV, jsxs, onCleanup, onMount, provide, render, useContext };
49
+ export type { Child, Context };
package/dist/index.d.ts CHANGED
@@ -1,9 +1,17 @@
1
1
  type Cleanup = () => void;
2
+ declare function batch<T>(fn: () => T): T;
2
3
  declare function createSignal<T>(value: T): readonly [() => T, (next: T | ((prev: T) => T)) => void];
3
4
  declare function createEffect(fn: () => void): void;
4
5
  declare function onCleanup(fn: Cleanup): void;
5
6
  declare function createRoot<T>(fn: (dispose: () => void) => T): T;
6
7
  declare function onMount(fn: () => void): void;
8
+ interface Context<T> {
9
+ readonly id: symbol;
10
+ readonly defaultValue: T;
11
+ }
12
+ declare function createContext<T>(defaultValue: T): Context<T>;
13
+ declare function provide<T, R>(ctx: Context<T>, value: T, fn: () => R): R;
14
+ declare function useContext<T>(ctx: Context<T>): T;
7
15
 
8
16
  type Child = string | number | boolean | null | undefined | Node | (() => unknown) | Child[];
9
17
  declare function h(tag: string, props: Record<string, any> | null, ...children: Child[]): HTMLElement;
@@ -18,8 +26,24 @@ declare function Show<T>(props: {
18
26
 
19
27
  declare function For<T>(props: {
20
28
  each: () => T[];
21
- children: (item: T, index: number) => Child;
22
- }): () => Child[];
29
+ children: (item: T, index: number) => Node;
30
+ key?: (item: T, index: number) => unknown;
31
+ }): () => Node[];
23
32
 
24
- export { For, Show, createEffect, createRoot, createSignal, h, onCleanup, onMount, render };
25
- export type { Child };
33
+ declare function createMemo<T>(fn: () => T): () => T;
34
+
35
+ declare function jsx(type: any, props: Record<string, any> | null): any;
36
+ declare const jsxs: typeof jsx;
37
+ declare const jsxDEV: typeof jsx;
38
+ declare namespace JSX {
39
+ type Element = any;
40
+ interface IntrinsicElements {
41
+ [tag: string]: any;
42
+ }
43
+ interface ElementChildrenAttribute {
44
+ children: {};
45
+ }
46
+ }
47
+
48
+ export { For, JSX, Show, batch, createContext, createEffect, createMemo, createRoot, createSignal, h, jsx, jsxDEV, jsxs, onCleanup, onMount, provide, render, useContext };
49
+ export type { Child, Context };
package/dist/index.js CHANGED
@@ -11,6 +11,22 @@ function cleanComputation(c) {
11
11
  c.deps.clear();
12
12
  clean(c);
13
13
  }
14
+ var batchDepth = 0;
15
+ var pending = /* @__PURE__ */ new Set();
16
+ function flush() {
17
+ const effects = [...pending];
18
+ pending.clear();
19
+ effects.forEach((effect) => effect.run());
20
+ }
21
+ function batch(fn) {
22
+ batchDepth++;
23
+ try {
24
+ return fn();
25
+ } finally {
26
+ batchDepth--;
27
+ if (batchDepth === 0) flush();
28
+ }
29
+ }
14
30
  function createSignal(value) {
15
31
  const subs = /* @__PURE__ */ new Set();
16
32
  const get = () => {
@@ -24,13 +40,18 @@ function createSignal(value) {
24
40
  const v = typeof next === "function" ? next(value) : next;
25
41
  if (Object.is(v, value)) return;
26
42
  value = v;
27
- [...subs].forEach((effect) => effect.run());
43
+ for (const effect of [...subs]) {
44
+ if (batchDepth > 0)
45
+ pending.add(effect);
46
+ else effect.run();
47
+ }
28
48
  };
29
49
  return [get, set];
30
50
  }
31
51
  function createEffect(fn) {
32
52
  const effect = {
33
53
  cleanups: [],
54
+ parent: currentOwner,
34
55
  deps: /* @__PURE__ */ new Set(),
35
56
  disposed: false,
36
57
  run() {
@@ -58,7 +79,7 @@ function onCleanup(fn) {
58
79
  currentOwner?.cleanups.push(fn);
59
80
  }
60
81
  function createRoot(fn) {
61
- const root = { cleanups: [] };
82
+ const root = { cleanups: [], parent: currentOwner };
62
83
  const prevOwner = currentOwner;
63
84
  const prevEffect = currentEffect;
64
85
  currentOwner = root;
@@ -85,6 +106,30 @@ function onMount(fn) {
85
106
  }
86
107
  });
87
108
  }
109
+ function createContext(defaultValue) {
110
+ return { id: /* @__PURE__ */ Symbol("cajoa-context"), defaultValue };
111
+ }
112
+ function provide(ctx, value, fn) {
113
+ const owner = {
114
+ cleanups: [],
115
+ parent: currentOwner,
116
+ context: /* @__PURE__ */ new Map([[ctx.id, value]])
117
+ };
118
+ onCleanup(() => clean(owner));
119
+ const prev = currentOwner;
120
+ currentOwner = owner;
121
+ try {
122
+ return fn();
123
+ } finally {
124
+ currentOwner = prev;
125
+ }
126
+ }
127
+ function useContext(ctx) {
128
+ for (let o = currentOwner; o; o = o.parent) {
129
+ if (o.context?.has(ctx.id)) return o.context.get(ctx.id);
130
+ }
131
+ return ctx.defaultValue;
132
+ }
88
133
 
89
134
  // src/dom/h.ts
90
135
  function h(tag, props, ...children) {
@@ -118,12 +163,12 @@ function toNodes(value) {
118
163
  return out;
119
164
  }
120
165
  function appendChildren(parent, children) {
121
- const pending = [...children].reverse();
122
- while (pending.length > 0) {
123
- const child = pending.pop();
166
+ const pending2 = [...children].reverse();
167
+ while (pending2.length > 0) {
168
+ const child = pending2.pop();
124
169
  if (Array.isArray(child)) {
125
170
  for (let i = child.length - 1; i >= 0; i--) {
126
- pending.push(child[i]);
171
+ pending2.push(child[i]);
127
172
  }
128
173
  continue;
129
174
  }
@@ -132,17 +177,29 @@ function appendChildren(parent, children) {
132
177
  const end = document.createComment("");
133
178
  parent.append(start, end);
134
179
  createEffect(() => {
135
- const nodes = toNodes(child());
136
- while (start.nextSibling && start.nextSibling !== end) {
137
- start.nextSibling.remove();
138
- }
139
- end.before(...nodes);
180
+ reconcile(start, end, toNodes(child()));
140
181
  });
141
182
  } else {
142
183
  parent.append(...toNodes(child));
143
184
  }
144
185
  }
145
186
  }
187
+ function reconcile(start, end, next) {
188
+ const keep = new Set(next);
189
+ let cur = start.nextSibling;
190
+ while (cur && cur !== end) {
191
+ const after = cur.nextSibling;
192
+ if (!keep.has(cur)) cur.remove();
193
+ cur = after;
194
+ }
195
+ const parent = end.parentNode;
196
+ let ref = start.nextSibling;
197
+ for (const node of next) {
198
+ if (node === ref)
199
+ ref = ref.nextSibling;
200
+ else parent.insertBefore(node, ref);
201
+ }
202
+ }
146
203
 
147
204
  // src/dom/render.ts
148
205
  function render(component, container) {
@@ -162,16 +219,67 @@ function Show(props) {
162
219
 
163
220
  // src/components/for.ts
164
221
  function For(props) {
165
- return () => props.each().map((item, i) => props.children(item, i));
222
+ let cache = /* @__PURE__ */ new Map();
223
+ onCleanup(() => {
224
+ cache.forEach((e) => e.dispose());
225
+ cache.clear();
226
+ });
227
+ return () => {
228
+ const items = props.each();
229
+ const next = /* @__PURE__ */ new Map();
230
+ const nodes = [];
231
+ items.forEach((item, i) => {
232
+ const k = props.key ? props.key(item, i) : item;
233
+ let entry = cache.get(k);
234
+ if (!entry) {
235
+ entry = createRoot((dispose) => ({
236
+ node: props.children(item, i),
237
+ dispose
238
+ }));
239
+ }
240
+ next.set(k, entry);
241
+ nodes.push(entry.node);
242
+ });
243
+ cache.forEach((entry, k) => {
244
+ if (!next.has(k)) entry.dispose();
245
+ });
246
+ cache = next;
247
+ return nodes;
248
+ };
249
+ }
250
+
251
+ // src/reactivity/memo.ts
252
+ function createMemo(fn) {
253
+ const [value, setValue] = createSignal(void 0);
254
+ createEffect(() => setValue(() => fn()));
255
+ return value;
256
+ }
257
+
258
+ // src/jsx-runtime.ts
259
+ function jsx(type, props) {
260
+ if (typeof type === "function") return type(props ?? {});
261
+ const { children, ...rest } = props ?? {};
262
+ const kids = children === void 0 ? [] : [children];
263
+ return h(type, rest, ...kids);
166
264
  }
265
+ var jsxs = jsx;
266
+ var jsxDEV = jsx;
167
267
  export {
168
268
  For,
169
269
  Show,
270
+ batch,
271
+ createContext,
170
272
  createEffect,
273
+ createMemo,
171
274
  createRoot,
172
275
  createSignal,
173
276
  h,
277
+ jsx,
278
+ jsxDEV,
279
+ jsxs,
174
280
  onCleanup,
175
281
  onMount,
176
- render
282
+ provide,
283
+ render,
284
+ useContext
177
285
  };
package/package.json CHANGED
@@ -1,57 +1,78 @@
1
- {
2
- "name": "cajoa",
3
- "version": "0.1.0",
4
- "description": "A beginner-friendly, signals-based frontend framework.",
5
- "type": "module",
6
- "main": "./dist/index.cjs",
7
- "module": "./dist/index.js",
8
- "types": "./dist/index.d.ts",
9
- "exports": {
10
- ".": {
11
- "import": {
12
- "types": "./dist/index.d.ts",
13
- "default": "./dist/index.js"
14
- },
15
- "require": {
16
- "types": "./dist/index.d.cts",
17
- "default": "./dist/index.cjs"
18
- }
19
- }
20
- },
21
- "files": [
22
- "dist"
23
- ],
24
- "sideEffects": false,
25
- "scripts": {
26
- "typecheck": "tsc --noEmit",
27
- "dev": "vite --open /playground/",
28
- "build": "tsup src/index.ts --format esm,cjs --dts --clean",
29
- "test": "vitest",
30
- "prepublishOnly": "vitest run && npm run build"
31
- },
32
- "repository": {
33
- "type": "git",
34
- "url": "git+https://github.com/cjmgrc/Cajoa.git"
35
- },
36
- "keywords": [
37
- "frontend",
38
- "framework",
39
- "signals",
40
- "reactive",
41
- "ui",
42
- "cajoa"
43
- ],
44
- "author": "cjmgrc",
45
- "license": "Apache-2.0",
46
- "bugs": {
47
- "url": "https://github.com/cjmgrc/Cajoa/issues"
48
- },
49
- "homepage": "https://github.com/cjmgrc/Cajoa#readme",
50
- "devDependencies": {
51
- "jsdom": "^29.1.1",
52
- "tsup": "^9.0.0",
53
- "typescript": "^5.9.3",
54
- "vite": "^8.3.4",
55
- "vitest": "^5.0.5"
56
- }
57
- }
1
+ {
2
+ "name": "cajoa",
3
+ "version": "0.3.0",
4
+ "description": "A beginner-friendly, signals-based frontend framework.",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ },
20
+ "./jsx-runtime": {
21
+ "import": {
22
+ "types": "./dist/index.d.ts",
23
+ "default": "./dist/index.js"
24
+ },
25
+ "require": {
26
+ "types": "./dist/index.d.cts",
27
+ "default": "./dist/index.cjs"
28
+ }
29
+ },
30
+ "./jsx-dev-runtime": {
31
+ "import": {
32
+ "types": "./dist/index.d.ts",
33
+ "default": "./dist/index.js"
34
+ },
35
+ "require": {
36
+ "types": "./dist/index.d.cts",
37
+ "default": "./dist/index.cjs"
38
+ }
39
+ }
40
+ },
41
+ "files": [
42
+ "dist"
43
+ ],
44
+ "sideEffects": false,
45
+ "scripts": {
46
+ "typecheck": "tsc --noEmit",
47
+ "dev": "vite --open /playground/",
48
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean",
49
+ "test": "vitest",
50
+ "prepublishOnly": "vitest run && npm run build"
51
+ },
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "git+https://github.com/CajoaTS/Cajoa.git"
55
+ },
56
+ "keywords": [
57
+ "frontend",
58
+ "framework",
59
+ "signals",
60
+ "reactive",
61
+ "ui",
62
+ "cajoa"
63
+ ],
64
+ "author": "cjmgrc",
65
+ "license": "Apache-2.0",
66
+ "bugs": {
67
+ "url": "https://github.com/CajoaTS/Cajoa/issues"
68
+ },
69
+ "homepage": "https://github.com/CajoaTS/Cajoa#readme",
70
+ "devDependencies": {
71
+ "@types/node": "^26.6.5",
72
+ "jsdom": "^29.1.1",
73
+ "tsup": "^9.0.0",
74
+ "typescript": "^5.9.3",
75
+ "vite": "^8.3.4",
76
+ "vitest": "^5.0.5"
77
+ }
78
+ }