decantr 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 David Aimi
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/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # decantr
2
+
3
+ AI-first web framework. Zero dependencies. Native JS/CSS/HTML.
4
+
5
+ Decantr is designed for LLMs to generate, read, and maintain — not for human readability. Every project follows predictable patterns with machine-readable manifests so AI agents can understand and modify code without parsing source files.
6
+
7
+ ## Quick Start
8
+
9
+ ```bash
10
+ npx decantr init
11
+ npm install
12
+ npx decantr dev
13
+ ```
14
+
15
+ ## Features
16
+
17
+ - **Zero dependencies** — Pure JavaScript, CSS, HTML
18
+ - **Signal-based reactivity** — Fine-grained DOM updates, no virtual DOM
19
+ - **Direct DOM rendering** — `h()` creates real elements, no diffing overhead
20
+ - **Atomic CSS** — Terse, auto-generated utility classes (`p4`, `bg1`, `flex`)
21
+ - **Dual router** — Hash or History API, user chooses at init
22
+ - **Built-in test runner** — Wraps `node:test` with DOM testing helpers
23
+ - **AI manifests** — `.decantr/` directory with JSON schemas for LLM consumption
24
+ - **< 2KB gzipped** — Hello world JS runtime under 2KB gzipped
25
+
26
+ ## Architecture
27
+
28
+ ```
29
+ decantr/core — h(), text(), cond(), list(), mount()
30
+ decantr/state — createSignal, createEffect, createMemo, createStore, batch
31
+ decantr/router — createRouter, link, navigate, useRoute
32
+ decantr/css — css(), define()
33
+ decantr/test — render, fire, flush + node:test re-exports
34
+ ```
35
+
36
+ ## CLI Commands
37
+
38
+ ```bash
39
+ decantr init # Scaffold a new project
40
+ decantr dev # Start dev server with hot reload
41
+ decantr build # Production build
42
+ decantr test # Run tests
43
+ decantr test --watch # Run tests in watch mode
44
+ ```
45
+
46
+ ## Component Pattern
47
+
48
+ Every component is a function that returns an HTMLElement:
49
+
50
+ ```javascript
51
+ import { h, text } from 'decantr/core';
52
+ import { createSignal } from 'decantr/state';
53
+
54
+ export function Counter({ initial = 0 } = {}) {
55
+ const [count, setCount] = createSignal(initial);
56
+ return h('div', { class: 'flex gap2 p4' },
57
+ h('button', { onclick: () => setCount(c => c - 1) }, '-'),
58
+ h('span', null, text(() => String(count()))),
59
+ h('button', { onclick: () => setCount(c => c + 1) }, '+')
60
+ );
61
+ }
62
+ ```
63
+
64
+ ## Requirements
65
+
66
+ - Node.js >= 22.0.0
67
+
68
+ ## License
69
+
70
+ MIT
@@ -0,0 +1,17 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+
4
+ export async function run() {
5
+ const cwd = process.cwd();
6
+ let config = { build: { outDir: 'dist', inline: false, sourcemap: false } };
7
+
8
+ try {
9
+ const raw = await readFile(join(cwd, 'decantr.config.json'), 'utf-8');
10
+ config = JSON.parse(raw);
11
+ } catch (e) {
12
+ // Use defaults
13
+ }
14
+
15
+ const { build } = await import('../../tools/builder.js');
16
+ await build(cwd, config.build || {});
17
+ }
@@ -0,0 +1,18 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+
4
+ export async function run() {
5
+ const cwd = process.cwd();
6
+ let config = { dev: { port: 3000 } };
7
+
8
+ try {
9
+ const raw = await readFile(join(cwd, 'decantr.config.json'), 'utf-8');
10
+ config = JSON.parse(raw);
11
+ } catch (e) {
12
+ // Use defaults
13
+ }
14
+
15
+ const port = config.dev?.port || 3000;
16
+ const { startDevServer } = await import('../../tools/dev-server.js');
17
+ startDevServer(cwd, port);
18
+ }
@@ -0,0 +1,247 @@
1
+ import { createInterface } from 'node:readline/promises';
2
+ import { stdin, stdout } from 'node:process';
3
+ import { mkdir, writeFile } from 'node:fs/promises';
4
+ import { join, basename } from 'node:path';
5
+
6
+ const templates = {
7
+ packageJson: (name) => JSON.stringify({
8
+ name,
9
+ version: '0.1.0',
10
+ type: 'module',
11
+ scripts: {
12
+ dev: 'decantr dev',
13
+ build: 'decantr build',
14
+ test: 'decantr test'
15
+ },
16
+ dependencies: {
17
+ decantr: '^0.1.0'
18
+ }
19
+ }, null, 2),
20
+
21
+ config: (name, routerMode, port) => JSON.stringify({
22
+ $schema: 'https://decantr.ai/schemas/config.v1.json',
23
+ name,
24
+ router: routerMode,
25
+ dev: { port },
26
+ build: { outDir: 'dist', inline: false, sourcemap: false }
27
+ }, null, 2),
28
+
29
+ indexHtml: (name) => `<!DOCTYPE html>
30
+ <html lang="en">
31
+ <head>
32
+ <meta charset="UTF-8">
33
+ <meta name="viewport" content="width=device-width,initial-scale=1">
34
+ <title>${name}</title>
35
+ <style>:root{--c0:#111;--c1:#2563eb;--c2:#f8fafc;--c3:#e2e8f0;--c4:#94a3b8;--c5:#475569;--c6:#1e293b;--c7:#f59e0b;--c8:#10b981;--c9:#ef4444}*{margin:0;box-sizing:border-box}body{font-family:system-ui,-apple-system,sans-serif;color:var(--c0);background:var(--c2)}</style>
36
+ </head>
37
+ <body>
38
+ <div id="app"></div>
39
+ <script type="module" src="/src/app.js"></script>
40
+ </body>
41
+ </html>`,
42
+
43
+ appJs: (routerMode, includeCounter) => `import { h, mount } from 'decantr/core';
44
+ import { createRouter, link } from 'decantr/router';
45
+ import { Home } from './pages/home.js';
46
+ import { About } from './pages/about.js';
47
+
48
+ const router = createRouter({
49
+ mode: '${routerMode}',
50
+ routes: [
51
+ { path: '/', component: Home },
52
+ { path: '/about', component: About }
53
+ ]
54
+ });
55
+
56
+ function App() {
57
+ return h('div', null,
58
+ h('nav', { class: 'flex gap4 p4 bg1' },
59
+ link({ href: '/', class: 'fg2 nounder' }, 'Home'),
60
+ link({ href: '/about', class: 'fg2 nounder' }, 'About')
61
+ ),
62
+ router.outlet()
63
+ );
64
+ }
65
+
66
+ mount(document.getElementById('app'), App);
67
+ `,
68
+
69
+ homeJs: (includeCounter) => `import { h } from 'decantr/core';
70
+ ${includeCounter ? "import { Counter } from '../components/counter.js';\n" : ''}
71
+ /** @returns {HTMLElement} */
72
+ export function Home() {
73
+ return h('main', { class: 'p8' },
74
+ h('h1', { class: 't32 bold' }, 'Welcome to Decantr'),
75
+ h('p', { class: 'py4 fg5' }, 'AI-first web framework. Zero dependencies.')${includeCounter ? ',\n h(\'div\', { class: \'py4\' }, Counter({ initial: 0 }))' : ''}
76
+ );
77
+ }
78
+ `,
79
+
80
+ aboutJs: () => `import { h } from 'decantr/core';
81
+
82
+ /** @returns {HTMLElement} */
83
+ export function About() {
84
+ return h('main', { class: 'p8' },
85
+ h('h1', { class: 't32 bold' }, 'About'),
86
+ h('p', { class: 'py4 fg5' }, 'Built with Decantr — the AI-first framework.')
87
+ );
88
+ }
89
+ `,
90
+
91
+ counterJs: () => `import { h, text } from 'decantr/core';
92
+ import { createSignal } from 'decantr/state';
93
+
94
+ /**
95
+ * @param {{ initial?: number }} props
96
+ * @returns {HTMLElement}
97
+ */
98
+ export function Counter({ initial = 0 } = {}) {
99
+ const [count, setCount] = createSignal(initial);
100
+
101
+ return h('div', { class: 'flex gap2 p4 aic' },
102
+ h('button', { onclick: () => setCount(c => c - 1), class: 'p2 px4 r2 bg1 fg2 pointer b0 t16' }, '-'),
103
+ h('span', { class: 'p2 t20 bold' }, text(() => String(count()))),
104
+ h('button', { onclick: () => setCount(c => c + 1), class: 'p2 px4 r2 bg1 fg2 pointer b0 t16' }, '+')
105
+ );
106
+ }
107
+ `,
108
+
109
+ counterTestJs: () => `import { describe, it, assert, render, fire, flush } from 'decantr/test';
110
+ import { Counter } from '../src/components/counter.js';
111
+
112
+ describe('Counter', () => {
113
+ it('renders with initial value', () => {
114
+ const { container } = render(() => Counter({ initial: 5 }));
115
+ assert.ok(container.textContent.includes('5'));
116
+ });
117
+
118
+ it('increments on + click', async () => {
119
+ const { container, getByText } = render(() => Counter({ initial: 0 }));
120
+ fire(getByText('+'), 'click');
121
+ await flush();
122
+ assert.ok(container.textContent.includes('1'));
123
+ });
124
+
125
+ it('decrements on - click', async () => {
126
+ const { container, getByText } = render(() => Counter({ initial: 5 }));
127
+ fire(getByText('-'), 'click');
128
+ await flush();
129
+ assert.ok(container.textContent.includes('4'));
130
+ });
131
+ });
132
+ `,
133
+
134
+ manifest: (name, routerMode) => JSON.stringify({
135
+ $schema: 'https://decantr.ai/schemas/manifest.v1.json',
136
+ version: '0.1.0',
137
+ name,
138
+ router: routerMode,
139
+ entrypoint: 'src/app.js',
140
+ shell: 'public/index.html',
141
+ mountTarget: '#app',
142
+ components: '.decantr/components.json',
143
+ routes: '.decantr/routes.json',
144
+ state: '.decantr/state.json'
145
+ }, null, 2),
146
+
147
+ components: (includeCounter) => JSON.stringify({
148
+ $schema: 'https://decantr.ai/schemas/components.v1.json',
149
+ components: includeCounter ? [{
150
+ id: 'counter',
151
+ file: 'src/components/counter.js',
152
+ exportName: 'Counter',
153
+ description: 'Increment/decrement counter with display',
154
+ props: [{ name: 'initial', type: 'number', default: 0, required: false }],
155
+ signals: ['count'],
156
+ effects: [],
157
+ children: false
158
+ }] : []
159
+ }, null, 2),
160
+
161
+ routes: (routerMode) => JSON.stringify({
162
+ $schema: 'https://decantr.ai/schemas/routes.v1.json',
163
+ mode: routerMode,
164
+ routes: [
165
+ { path: '/', component: 'home', file: 'src/pages/home.js', exportName: 'Home', title: 'Home' },
166
+ { path: '/about', component: 'about', file: 'src/pages/about.js', exportName: 'About', title: 'About' }
167
+ ]
168
+ }, null, 2),
169
+
170
+ state: (includeCounter) => JSON.stringify({
171
+ $schema: 'https://decantr.ai/schemas/state.v1.json',
172
+ signals: includeCounter ? [{
173
+ id: 'count',
174
+ file: 'src/components/counter.js',
175
+ type: 'number',
176
+ initial: 0,
177
+ usedBy: ['Counter']
178
+ }] : [],
179
+ effects: [],
180
+ memos: []
181
+ }, null, 2)
182
+ };
183
+
184
+ async function ask(rl, question, defaultVal) {
185
+ const answer = await rl.question(`${question} (${defaultVal}): `);
186
+ return answer.trim() || defaultVal;
187
+ }
188
+
189
+ async function askChoice(rl, question, options, defaultVal) {
190
+ console.log(`\n${question}`);
191
+ options.forEach((opt, i) => console.log(` ${i + 1}) ${opt}`));
192
+ const answer = await rl.question(`Choose [${defaultVal}]: `);
193
+ const idx = parseInt(answer.trim()) - 1;
194
+ return (idx >= 0 && idx < options.length) ? options[idx] : defaultVal;
195
+ }
196
+
197
+ export async function run() {
198
+ const rl = createInterface({ input: stdin, output: stdout });
199
+ const cwd = process.cwd();
200
+
201
+ console.log('\n decantr init — Create a new project\n');
202
+
203
+ try {
204
+ const name = await ask(rl, 'Project name?', basename(cwd));
205
+ const routerMode = await askChoice(rl, 'Router mode?', ['history', 'hash'], 'history');
206
+ const includeCounterAnswer = await askChoice(rl, 'Include example counter component?', ['yes', 'no'], 'yes');
207
+ const includeCounter = includeCounterAnswer === 'yes';
208
+ const port = parseInt(await ask(rl, 'Dev server port?', '3000'));
209
+
210
+ // Create directories
211
+ const dirs = ['src/pages', 'src/components', 'public', '.decantr', 'test'];
212
+ for (const dir of dirs) {
213
+ await mkdir(join(cwd, dir), { recursive: true });
214
+ }
215
+
216
+ // Write files
217
+ const files = [
218
+ ['package.json', templates.packageJson(name)],
219
+ ['decantr.config.json', templates.config(name, routerMode, port)],
220
+ ['public/index.html', templates.indexHtml(name)],
221
+ ['src/app.js', templates.appJs(routerMode, includeCounter)],
222
+ ['src/pages/home.js', templates.homeJs(includeCounter)],
223
+ ['src/pages/about.js', templates.aboutJs()],
224
+ ['.decantr/manifest.json', templates.manifest(name, routerMode)],
225
+ ['.decantr/components.json', templates.components(includeCounter)],
226
+ ['.decantr/routes.json', templates.routes(routerMode)],
227
+ ['.decantr/state.json', templates.state(includeCounter)]
228
+ ];
229
+
230
+ if (includeCounter) {
231
+ files.push(['src/components/counter.js', templates.counterJs()]);
232
+ files.push(['test/counter.test.js', templates.counterTestJs()]);
233
+ }
234
+
235
+ for (const [path, content] of files) {
236
+ await writeFile(join(cwd, path), content + '\n');
237
+ }
238
+
239
+ console.log(`\n Project "${name}" created successfully!\n`);
240
+ console.log(' Next steps:');
241
+ console.log(' npm install');
242
+ console.log(' npx decantr dev\n');
243
+
244
+ } finally {
245
+ rl.close();
246
+ }
247
+ }
@@ -0,0 +1,38 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { glob } from 'node:fs';
3
+ import { join } from 'node:path';
4
+
5
+ export async function run() {
6
+ const cwd = process.cwd();
7
+ const args = process.argv.slice(3);
8
+ const watch = args.includes('--watch');
9
+
10
+ const testArgs = ['--test'];
11
+ if (watch) testArgs.push('--watch');
12
+
13
+ // Find test files
14
+ const testFiles = [];
15
+ const pattern = join(cwd, 'test', '**', '*.test.js');
16
+
17
+ // Use node:fs glob (Node 22+)
18
+ const { glob: fsGlob } = await import('node:fs/promises');
19
+ for await (const file of fsGlob(pattern)) {
20
+ testFiles.push(file);
21
+ }
22
+
23
+ if (testFiles.length === 0) {
24
+ console.log('No test files found in test/**/*.test.js');
25
+ return;
26
+ }
27
+
28
+ testArgs.push(...testFiles);
29
+
30
+ const child = spawn('node', testArgs, {
31
+ cwd,
32
+ stdio: 'inherit'
33
+ });
34
+
35
+ child.on('exit', (code) => {
36
+ process.exit(code || 0);
37
+ });
38
+ }
package/cli/index.js ADDED
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { parseArgs } from 'node:util';
4
+
5
+ const { positionals } = parseArgs({ allowPositionals: true, strict: false });
6
+ const command = positionals[0];
7
+
8
+ switch (command) {
9
+ case 'init':
10
+ await import('./commands/init.js').then(m => m.run());
11
+ break;
12
+ case 'dev':
13
+ await import('./commands/dev.js').then(m => m.run());
14
+ break;
15
+ case 'build':
16
+ await import('./commands/build.js').then(m => m.run());
17
+ break;
18
+ case 'test':
19
+ await import('./commands/test.js').then(m => m.run());
20
+ break;
21
+ default:
22
+ console.log(`
23
+ decantr v0.1.0 — AI-first web framework
24
+
25
+ Commands:
26
+ init Create a new decantr project
27
+ dev Start development server
28
+ build Build for production
29
+ test Run tests
30
+
31
+ Usage:
32
+ npx decantr init
33
+ npx decantr dev
34
+ npx decantr build
35
+ npx decantr test [--watch]
36
+ `);
37
+ break;
38
+ }
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "decantr",
3
+ "version": "0.1.0",
4
+ "description": "AI-first web framework. Zero dependencies. Native JS/CSS/HTML.",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=22.0.0"
8
+ },
9
+ "bin": {
10
+ "decantr": "./cli/index.js"
11
+ },
12
+ "exports": {
13
+ ".": {
14
+ "import": "./src/core/index.js"
15
+ },
16
+ "./core": {
17
+ "import": "./src/core/index.js"
18
+ },
19
+ "./state": {
20
+ "import": "./src/state/index.js"
21
+ },
22
+ "./router": {
23
+ "import": "./src/router/index.js"
24
+ },
25
+ "./css": {
26
+ "import": "./src/css/index.js"
27
+ },
28
+ "./test": {
29
+ "import": "./src/test/index.js"
30
+ }
31
+ },
32
+ "files": [
33
+ "src/",
34
+ "cli/",
35
+ "tools/"
36
+ ],
37
+ "scripts": {
38
+ "test": "node --test test/**/*.test.js"
39
+ },
40
+ "keywords": [
41
+ "framework",
42
+ "ai",
43
+ "llm",
44
+ "web",
45
+ "signals",
46
+ "zero-dependency"
47
+ ],
48
+ "license": "MIT",
49
+ "author": "David Aimi",
50
+ "repository": {
51
+ "type": "git",
52
+ "url": "https://github.com/david-aimi/decantr"
53
+ },
54
+ "homepage": "https://decantr.ai"
55
+ }
@@ -0,0 +1,176 @@
1
+ import { createEffect } from '../state/index.js';
2
+ export { onMount, onDestroy } from './lifecycle.js';
3
+ import { drainMountQueue, drainDestroyQueue } from './lifecycle.js';
4
+
5
+ /**
6
+ * @param {string} tag
7
+ * @param {Object|null} props
8
+ * @param {...(string|number|Node|Function)} children
9
+ * @returns {HTMLElement}
10
+ */
11
+ export function h(tag, props, ...children) {
12
+ const el = document.createElement(tag);
13
+
14
+ if (props) {
15
+ for (const key in props) {
16
+ const val = props[key];
17
+ if (key.startsWith('on') && typeof val === 'function') {
18
+ el.addEventListener(key.slice(2).toLowerCase(), val);
19
+ } else if (typeof val === 'function' && key !== 'ref') {
20
+ createEffect(() => {
21
+ const v = val();
22
+ if (key === 'class' || key === 'className') {
23
+ el.className = v;
24
+ } else if (key === 'style' && typeof v === 'object') {
25
+ Object.assign(el.style, v);
26
+ } else {
27
+ el.setAttribute(key, v);
28
+ }
29
+ });
30
+ } else if (key === 'ref' && typeof val === 'function') {
31
+ val(el);
32
+ } else if (key === 'class' || key === 'className') {
33
+ el.className = val;
34
+ } else if (key === 'style' && typeof val === 'object') {
35
+ Object.assign(el.style, val);
36
+ } else if (val !== false && val != null) {
37
+ el.setAttribute(key, val === true ? '' : String(val));
38
+ }
39
+ }
40
+ }
41
+
42
+ appendChildren(el, children);
43
+ return el;
44
+ }
45
+
46
+ /**
47
+ * @param {Function} getter
48
+ * @returns {Text}
49
+ */
50
+ export function text(getter) {
51
+ const node = document.createTextNode('');
52
+ createEffect(() => {
53
+ node.nodeValue = String(getter());
54
+ });
55
+ return node;
56
+ }
57
+
58
+ /**
59
+ * @param {Function} condition
60
+ * @param {Function} thenFn
61
+ * @param {Function} [elseFn]
62
+ * @returns {HTMLElement}
63
+ */
64
+ export function cond(condition, thenFn, elseFn) {
65
+ const container = document.createElement('d-cond');
66
+ let currentNode = null;
67
+
68
+ createEffect(() => {
69
+ const result = condition();
70
+ if (currentNode) {
71
+ container.removeChild(currentNode);
72
+ currentNode = null;
73
+ }
74
+ const fn = result ? thenFn : elseFn;
75
+ if (fn) {
76
+ currentNode = fn();
77
+ if (currentNode) container.appendChild(currentNode);
78
+ }
79
+ });
80
+
81
+ return container;
82
+ }
83
+
84
+ /**
85
+ * @param {Function} itemsGetter
86
+ * @param {Function} keyFn
87
+ * @param {Function} renderFn
88
+ * @returns {HTMLElement}
89
+ */
90
+ export function list(itemsGetter, keyFn, renderFn) {
91
+ const container = document.createElement('d-list');
92
+ /** @type {Map<*, {node: Node}>} */
93
+ let currentMap = new Map();
94
+
95
+ createEffect(() => {
96
+ const items = itemsGetter();
97
+ const newMap = new Map();
98
+ const newNodes = [];
99
+
100
+ for (let i = 0; i < items.length; i++) {
101
+ const item = items[i];
102
+ const key = keyFn(item, i);
103
+ const existing = currentMap.get(key);
104
+
105
+ if (existing) {
106
+ newMap.set(key, existing);
107
+ newNodes.push(existing.node);
108
+ } else {
109
+ const node = renderFn(item, i);
110
+ newMap.set(key, { node });
111
+ newNodes.push(node);
112
+ }
113
+ }
114
+
115
+ // Remove nodes no longer in list
116
+ for (const [key, entry] of currentMap) {
117
+ if (!newMap.has(key) && entry.node.parentNode === container) {
118
+ container.removeChild(entry.node);
119
+ }
120
+ }
121
+
122
+ // Append/reorder
123
+ for (let i = 0; i < newNodes.length; i++) {
124
+ const node = newNodes[i];
125
+ const current = container.childNodes[i];
126
+ if (node !== current) {
127
+ container.insertBefore(node, current || null);
128
+ }
129
+ }
130
+
131
+ currentMap = newMap;
132
+ });
133
+
134
+ return container;
135
+ }
136
+
137
+ /**
138
+ * @param {HTMLElement} root
139
+ * @param {Function} component
140
+ */
141
+ export function mount(root, component) {
142
+ const result = component();
143
+ if (result) root.appendChild(result);
144
+ // Flush mount queue
145
+ const fns = drainMountQueue();
146
+ for (const fn of fns) {
147
+ const cleanup = fn();
148
+ if (typeof cleanup === 'function') {
149
+ // store cleanup for later
150
+ }
151
+ }
152
+ }
153
+
154
+ /**
155
+ * @param {HTMLElement} el
156
+ * @param {Array} children
157
+ */
158
+ function appendChildren(el, children) {
159
+ for (let i = 0; i < children.length; i++) {
160
+ const child = children[i];
161
+ if (child == null || child === false) continue;
162
+ if (Array.isArray(child)) {
163
+ appendChildren(el, child);
164
+ } else if (child && typeof child === 'object' && child.nodeType) {
165
+ el.appendChild(child);
166
+ } else if (typeof child === 'function') {
167
+ const textNode = document.createTextNode('');
168
+ createEffect(() => {
169
+ textNode.nodeValue = String(child());
170
+ });
171
+ el.appendChild(textNode);
172
+ } else {
173
+ el.appendChild(document.createTextNode(String(child)));
174
+ }
175
+ }
176
+ }