cumstack 1.0.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) 2025 Lavion
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,3 @@
1
+ # cumstack
2
+
3
+ A lightweight reactive framework with signals, routing, and i18n support. Just a package for modern web applications with server-side rendering and client-side hydration. Made this since why not.
package/cli/build.js ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * cumstack Build Command
3
+ * builds application for production
4
+ */
5
+ import { buildApp } from './builder.js';
6
+
7
+ const appRoot = process.cwd();
8
+
9
+ export default async function build() {
10
+ console.log('[cumstack] Building cumstack application for production...\n');
11
+
12
+ try {
13
+ await buildApp(appRoot, false);
14
+ console.log('[cumstack] Build complete!');
15
+ } catch (error) {
16
+ console.error('[cumstack] Build failed:', error.message);
17
+ throw error;
18
+ }
19
+ }
package/cli/builder.js ADDED
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * cumstack Build Engine
5
+ * Core build logic for cumstack applications
6
+ */
7
+
8
+ import path from 'path';
9
+ import { fileURLToPath } from 'url';
10
+ import { existsSync } from 'fs';
11
+
12
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
+
14
+ // Extension resolution hierarchy: jsx > tsx > js > ts
15
+ const EXTENSION_HIERARCHY = ['.jsx', '.tsx', '.js', '.ts'];
16
+
17
+ /**
18
+ * Resolve file path with optional extension
19
+ * Tries exact path first, then extensions in hierarchy order
20
+ */
21
+ function resolveWithExtension(basePath) {
22
+ const ext = path.extname(basePath);
23
+ if (['.js', '.jsx', '.ts', '.tsx'].includes(ext) && existsSync(basePath)) return basePath;
24
+ if (existsSync(basePath)) return basePath;
25
+ for (const extension of EXTENSION_HIERARCHY) {
26
+ const pathWithExt = basePath + extension;
27
+ if (existsSync(pathWithExt)) return pathWithExt;
28
+ }
29
+ return basePath;
30
+ }
31
+
32
+ export async function buildApp(appRoot, isDev = false, buildTimestamp = Date.now()) {
33
+ const outDir = isDev ? 'dist/dev' : 'dist/live';
34
+ console.log(`[cumstack] Building cumstack app for ${isDev ? 'development' : 'production'}...`);
35
+ console.log(`[cumstack] Output: ${outDir}`);
36
+ globalThis.__BUILD_TIMESTAMP__ = buildTimestamp;
37
+ let serverBuild;
38
+ try {
39
+ serverBuild = await Bun.build({
40
+ entrypoints: [path.join(appRoot, 'src/entry.server.jsx')],
41
+ outdir: path.join(appRoot, outDir, 'server'),
42
+ target: 'bun',
43
+ format: 'esm',
44
+ minify: !isDev,
45
+ sourcemap: isDev ? 'inline' : 'external',
46
+ external: ['hono', 'cloudflare:*'],
47
+ naming: '[dir]/main.server.js',
48
+ jsx: {
49
+ runtime: 'automatic',
50
+ importSource: 'hono/jsx',
51
+ },
52
+ // resolve @cumstack imports to framework package
53
+ plugins: [
54
+ {
55
+ name: 'resolve-cumstack',
56
+ setup(build) {
57
+ build.onResolve({ filter: /^@cumstack\/app/ }, (args) => {
58
+ let subpath = args.path.replace('@cumstack/app', '');
59
+ if (subpath === '/server') subpath = '/app/server/index.js';
60
+ else if (subpath === '/client') subpath = '/app/client/index.js';
61
+ else if (subpath === '/client/Twink') subpath = '/app/client/Twink.js';
62
+ else if (subpath === '/shared/i18n') subpath = '/app/shared/i18n.js';
63
+ else if (subpath === '/shared/reactivity') subpath = '/app/shared/reactivity.js';
64
+ else if (subpath === '/shared/router') subpath = '/app/shared/router.js';
65
+ else if (subpath === '/shared/utils') subpath = '/app/shared/utils.js';
66
+ else if (subpath === '/shared/language-codes') subpath = '/app/shared/language-codes.js';
67
+ else if (!subpath.endsWith('.js')) subpath += '.js';
68
+ return {
69
+ path: path.join(__dirname, '..', 'src', subpath),
70
+ external: false,
71
+ };
72
+ });
73
+ // resolve ~ alias to src directory
74
+ build.onResolve({ filter: /^~\// }, (args) => {
75
+ const subpath = args.path.replace('~', 'src');
76
+ const basePath = path.join(appRoot, subpath);
77
+ const resolvedPath = resolveWithExtension(basePath);
78
+ return {
79
+ path: resolvedPath,
80
+ external: false,
81
+ };
82
+ });
83
+ },
84
+ },
85
+ ],
86
+ });
87
+ } catch (error) {
88
+ console.error('[cumstack] Server build threw an exception:');
89
+ console.error(error);
90
+ throw new Error(`Server build failed: ${error.message}`);
91
+ }
92
+ if (!serverBuild.success) {
93
+ console.error('[cumstack] Server build failed:');
94
+ console.error('[cumstack] Build logs count:', serverBuild.logs.length);
95
+ for (const log of serverBuild.logs) console.error(` [${log.level}] ${log.message}`);
96
+ throw new Error('Server build failed');
97
+ }
98
+
99
+ // build client entry
100
+ const clientBuild = await Bun.build({
101
+ entrypoints: [path.join(appRoot, 'src/entry.client.jsx')],
102
+ outdir: path.join(appRoot, outDir, 'client'),
103
+ target: 'browser',
104
+ format: 'esm',
105
+ minify: !isDev,
106
+ sourcemap: isDev ? 'inline' : 'external',
107
+ naming: '[dir]/main.client.js',
108
+ jsx: {
109
+ runtime: 'automatic',
110
+ importSource: 'hono/jsx',
111
+ },
112
+ // resolve @cumstack imports to framework package
113
+ plugins: [
114
+ {
115
+ name: 'resolve-cumstack',
116
+ setup(build) {
117
+ build.onResolve({ filter: /^@cumstack\/app/ }, (args) => {
118
+ let subpath = args.path.replace('@cumstack/app', '');
119
+ if (subpath === '/server') subpath = '/app/server/index.js';
120
+ else if (subpath === '/client') subpath = '/app/client/index.js';
121
+ else if (subpath === '/shared/i18n') subpath = '/app/shared/i18n.js';
122
+ else if (subpath === '/shared/reactivity') subpath = '/app/shared/reactivity.js';
123
+ else if (subpath === '/shared/router') subpath = '/app/shared/router.js';
124
+ else if (subpath === '/shared/utils') subpath = '/app/shared/utils.js';
125
+ else if (subpath === '/shared/language-codes') subpath = '/app/shared/language-codes.js';
126
+ else if (subpath === '/client/Twink') subpath = '/app/client/Twink.js';
127
+ else if (!subpath.endsWith('.js')) subpath += '.js';
128
+ return {
129
+ path: path.join(__dirname, '..', 'src', subpath),
130
+ external: false,
131
+ };
132
+ });
133
+ // resolve ~ alias to src directory
134
+ build.onResolve({ filter: /^~\// }, (args) => {
135
+ const subpath = args.path.replace('~', 'src');
136
+ const basePath = path.join(appRoot, subpath);
137
+ const resolvedPath = resolveWithExtension(basePath);
138
+ return {
139
+ path: resolvedPath,
140
+ external: false,
141
+ };
142
+ });
143
+ },
144
+ },
145
+ ],
146
+ });
147
+
148
+ if (!clientBuild.success) {
149
+ console.error('[cumstack] Client build failed:');
150
+ for (const log of clientBuild.logs) console.error(` ${log.level}: ${log.message}`);
151
+ throw new Error('Client build failed');
152
+ }
153
+ // process CSS through PostCSS (tailwindcss)
154
+ try {
155
+ const postcss = await import('postcss');
156
+ const tailwindcss = await import('@tailwindcss/postcss');
157
+ const cssPath = path.join(appRoot, 'src/main.css');
158
+ const css = await Bun.file(cssPath).text();
159
+ const result = await postcss.default([tailwindcss.default({ base: appRoot })]).process(css, {
160
+ from: cssPath,
161
+ to: path.join(appRoot, outDir, 'client/main.css'),
162
+ map: isDev ? { inline: true } : false,
163
+ });
164
+ await Bun.write(path.join(appRoot, outDir, 'client/main.css'), result.css);
165
+ console.log('[cumstack] CSS processed with Tailwind');
166
+ } catch (error) {
167
+ console.warn('[cumstack] PostCSS processing failed:', error.message);
168
+ const cssPath = path.join(appRoot, 'src/main.css');
169
+ await Bun.write(path.join(appRoot, outDir, 'client/main.css'), await Bun.file(cssPath).text());
170
+ }
171
+ console.log('[cumstack] Build complete!');
172
+ }
package/cli/create.js ADDED
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * cumstack Create Command
5
+ * Creates a new cumstack project from template
6
+ */
7
+
8
+ import { existsSync } from 'fs';
9
+ import { mkdir, cp } from 'fs/promises';
10
+ import path from 'path';
11
+ import { fileURLToPath } from 'url';
12
+
13
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
+
15
+ export default async function create() {
16
+ const projectName = process.argv[3];
17
+ if (!projectName) {
18
+ console.error('Error: Please provide a project name');
19
+ console.log('Usage: cum create <project-name>');
20
+ process.exit(1);
21
+ }
22
+ const targetDir = path.resolve(projectName);
23
+ if (existsSync(targetDir)) {
24
+ console.error(`Error: Directory "${projectName}" already exists`);
25
+ process.exit(1);
26
+ }
27
+ console.log(`Creating new cumstack project: ${projectName}`);
28
+ await mkdir(targetDir, { recursive: true });
29
+ const templateDir = path.join(__dirname, '../templates/monorepo');
30
+ await cp(templateDir, targetDir, { recursive: true });
31
+ console.log(`Project created successfully!`);
32
+ console.log(`\nNext steps:`);
33
+ console.log(` cd ${projectName}`);
34
+ console.log(` bun install`);
35
+ console.log(` bun run dev`);
36
+ }
package/cli/dev.js ADDED
@@ -0,0 +1,109 @@
1
+ /**
2
+ * cumstack Dev Command
3
+ * Starts development environment with build, wrangler, and HMR
4
+ */
5
+
6
+ import { spawn } from 'child_process';
7
+ import { watch } from 'fs';
8
+ import path from 'path';
9
+ import { fileURLToPath } from 'url';
10
+ import { WebSocketServer } from 'ws';
11
+ import { buildApp } from './builder.js';
12
+
13
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
+ const appRoot = process.cwd();
15
+ const HMR_PORT = 8790;
16
+ const DEBOUNCE_MS = 100;
17
+
18
+ export default async function dev() {
19
+ console.log('[cumstack] Starting cumstack development environment');
20
+ console.log(`[cumstack] App root: ${appRoot}`);
21
+ console.log('[cumstack] Building application...');
22
+ await runBuild();
23
+ // start HMR WebSocket server
24
+ const wss = new WebSocketServer({ port: HMR_PORT });
25
+ const clients = new Set();
26
+ wss.on('connection', (ws) => {
27
+ clients.add(ws);
28
+ console.log('[cumstack] HMR Client connected');
29
+ ws.on('close', () => {
30
+ clients.delete(ws);
31
+ console.log('[cumstack] HMR Client disconnected');
32
+ });
33
+ });
34
+ console.log(`[cumstack] HMR WebSocket server running on port ${HMR_PORT}`);
35
+ // start file watcher
36
+ let rebuildTimeout;
37
+ let isRebuilding = false;
38
+ const triggerRebuild = async (filePath) => {
39
+ if (rebuildTimeout) clearTimeout(rebuildTimeout);
40
+ rebuildTimeout = setTimeout(async () => {
41
+ if (isRebuilding) return;
42
+ isRebuilding = true;
43
+ const relativePath = path.relative(appRoot, filePath);
44
+ console.log(`[cumstack] HMR File changed: ${relativePath}`);
45
+ console.log('[cumstack] Rebuilding...');
46
+ try {
47
+ await runBuild();
48
+ const ext = path.extname(relativePath);
49
+ let updateType = 'js-update';
50
+ if (ext === '.css') {
51
+ updateType = 'css-update';
52
+ console.log('[cumstack] Build successful, hot swapping styles...');
53
+ } else if (['.js', '.jsx'].includes(ext) && relativePath.includes('/client/')) {
54
+ updateType = 'js-update';
55
+ console.log('[cumstack] Build successful, hot swapping modules...');
56
+ } else {
57
+ updateType = 'server-update';
58
+ console.log('[cumstack] Build successful, patching DOM...');
59
+ }
60
+ clients.forEach((client) => {
61
+ if (client.readyState === 1) {
62
+ client.send(
63
+ JSON.stringify({
64
+ type: updateType,
65
+ path: relativePath,
66
+ timestamp: Date.now(),
67
+ })
68
+ );
69
+ }
70
+ });
71
+ } catch (error) {
72
+ console.error('[cumstack] Build failed:', error.message);
73
+ } finally {
74
+ isRebuilding = false;
75
+ }
76
+ }, DEBOUNCE_MS);
77
+ };
78
+ const srcDir = path.join(appRoot, 'src');
79
+ watch(srcDir, { recursive: true }, (eventType, filename) => {
80
+ if (!filename) return;
81
+ const ext = path.extname(filename);
82
+ if (['.js', '.jsx', '.ts', '.tsx', '.css', '.json'].includes(ext)) triggerRebuild(path.join(srcDir, filename));
83
+ });
84
+ console.log(`[cumstack] HMR Watching ${path.relative(appRoot, srcDir)}/`);
85
+ console.log('[cumstack] Starting Wrangler dev server...');
86
+ const wrangler = spawn('wrangler', ['dev', '--env', 'dev'], {
87
+ cwd: appRoot,
88
+ stdio: 'inherit',
89
+ shell: true,
90
+ });
91
+ // handle exit
92
+ wrangler.on('exit', (code) => {
93
+ console.log('[cumstack] Shutting down cumstack dev server...');
94
+ wss.close();
95
+ process.exit(code);
96
+ });
97
+ // cleanup
98
+ process.on('SIGINT', () => {
99
+ console.log('[cumstack] Shutting down cumstack dev server...');
100
+ wrangler.kill();
101
+ wss.close();
102
+ process.exit(0);
103
+ });
104
+ }
105
+ // run build process
106
+ async function runBuild() {
107
+ const timestamp = Date.now();
108
+ await buildApp(appRoot, true, timestamp);
109
+ }
package/cli/index.js ADDED
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * cumstack CLI
5
+ * command-line interface for cumstack framework
6
+ */
7
+
8
+ const commands = {
9
+ dev: './dev.js',
10
+ build: './build.js',
11
+ create: './create.js',
12
+ };
13
+
14
+ const colors = {
15
+ reset: '\x1b[0m',
16
+ bold: '\x1b[1m',
17
+ cyan: '\x1b[36m',
18
+ green: '\x1b[32m',
19
+ red: '\x1b[31m',
20
+ yellow: '\x1b[33m',
21
+ magenta: '\x1b[35m',
22
+ };
23
+
24
+ function frameHeader(text) {
25
+ const lines = text.split('\n').map((line) => line.trim());
26
+ const maxLength = Math.max(...lines.map((l) => l.length));
27
+ const top = '╔' + '═'.repeat(maxLength + 2) + '╗';
28
+ const bottom = '╚' + '═'.repeat(maxLength + 2) + '╝';
29
+ const framedLines = lines.map((line) => `╟ ${line.padEnd(maxLength, ' ')} ╢`);
30
+ return [top, ...framedLines, bottom].join('\n');
31
+ }
32
+
33
+ // usage/help text
34
+ function printUsage(error) {
35
+ const header = frameHeader('cumstack Framework CLI');
36
+ console.log(error ? `${colors.red}${header}${colors.reset}` : `${colors.cyan}${header}${colors.reset}`);
37
+ console.log(`${colors.yellow}Usage:${colors.reset}
38
+ cum <command> [options]
39
+
40
+ ${colors.cyan}Commands:${colors.reset}
41
+ ${colors.green} dev${colors.reset} → Start development server with HMR
42
+ ${colors.green} build${colors.reset} → Build for production
43
+ ${colors.green} create${colors.reset} → Create a new cumstack project
44
+
45
+ ${colors.cyan}Examples:${colors.reset}
46
+ ${colors.green} cum dev
47
+ ${colors.green} cum build
48
+ ${colors.green} cum create my-app
49
+ `);
50
+
51
+ process.exit(error ? 1 : 0);
52
+ }
53
+
54
+ const command = process.argv[2];
55
+ if (!command) printUsage();
56
+ if (!commands[command]) printUsage(`Unknown command "${command}"`);
57
+ try {
58
+ const commandModule = await import(commands[command]);
59
+ if (typeof commandModule.default !== 'function') throw new Error(`Command module "${command}" does not export a default function`);
60
+ await commandModule.default();
61
+ } catch (err) {
62
+ console.error(`${colors.red}\nFailed to execute "${command}": ${err.message}${colors.reset}`);
63
+ if (err.stack) console.error(`${colors.red}${err.stack}${colors.reset}`);
64
+ process.exit(1);
65
+ }
package/index.js ADDED
@@ -0,0 +1,22 @@
1
+ /* cumstack Framework main entry point */
2
+
3
+ // client exports
4
+ export { cowgirl, CowgirlCreampie, configureHydration } from './src/app/client/index.js';
5
+ export { Twink } from './src/app/client/Twink.js';
6
+
7
+ // server exports
8
+ export { foxgirl, FoxgirlCreampie, Router, Route, Head, Title, Meta, TwinkTag, Script, h, renderToString } from './src/app/server/index.js';
9
+
10
+ // shared exports
11
+ export {
12
+ registerTranslations,
13
+ t,
14
+ setLanguage,
15
+ getLanguage,
16
+ getTranslations,
17
+ localizeRoute,
18
+ extractLanguageFromRoute,
19
+ detectBrowserLanguage,
20
+ } from './src/app/shared/i18n.js';
21
+
22
+ export { createMoan, onClimax, knotMemo, loadShot, batch, untrack, useLocation } from './src/app/shared/reactivity.js';
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "cumstack",
3
+ "version": "1.0.0",
4
+ "description": "A lightweight reactive framework with signals, routing, and i18n",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "cum": "./cli/index.js"
9
+ },
10
+ "scripts": {
11
+ "fmt": "prettier --write ."
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/iLavion/cumstack.git"
16
+ },
17
+ "bugs": {
18
+ "url": "https://github.com/iLavion/cumstack/issues"
19
+ },
20
+ "homepage": "https://github.com/iLavion/cumstack#readme",
21
+ "engines": {
22
+ "node": ">=22"
23
+ },
24
+ "files": [
25
+ "index.js",
26
+ "src/",
27
+ "cli/",
28
+ "templates/"
29
+ ],
30
+ "dependencies": {
31
+ "@tailwindcss/postcss": "^4.1.18",
32
+ "postcss": "^8.5.6",
33
+ "ws": "^8.18.3"
34
+ },
35
+ "exports": {
36
+ ".": "./index.js",
37
+ "./app/client": "./src/app/client/index.js",
38
+ "./app/server": "./src/app/server/index.js",
39
+ "./app/shared/i18n": "./src/app/shared/i18n.js",
40
+ "./app/shared/reactivity": "./src/app/shared/reactivity.js",
41
+ "./app/shared/router": "./src/app/shared/router.js",
42
+ "./app/shared/utils": "./src/app/shared/utils.js",
43
+ "./app/shared/language-codes": "./src/app/shared/language-codes.js",
44
+ "./client/Twink": "./src/app/client/Twink.js",
45
+ "./client/components": "./src/app/client/components.js",
46
+ "./client/hmr": "./src/app/client/hmr.js",
47
+ "./server/components": "./src/app/server/components.js",
48
+ "./server/jsx": "./src/app/server/jsx.js",
49
+ "./server/hono-utils": "./src/app/server/hono-utils.js",
50
+ "./cli": "./cli/index.js"
51
+ },
52
+ "keywords": [
53
+ "framework",
54
+ "reactivity",
55
+ "signals",
56
+ "router",
57
+ "i18n"
58
+ ],
59
+ "author": {
60
+ "name": "Lavion <lavion@invalsia.com>",
61
+ "url": "https://github.com/iLavion"
62
+ },
63
+ "license": "MIT",
64
+ "devDependencies": {
65
+ "prettier": "^3.7.4"
66
+ }
67
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * link component - client side
3
+ */
4
+
5
+ import { getLanguage } from '../shared/i18n.js';
6
+
7
+ /**
8
+ * check if currently in a language-prefixed route
9
+ * @returns {boolean} True if in a language-prefixed route
10
+ */
11
+ function isInLanguageRoute() {
12
+ if (typeof window === 'undefined') return false;
13
+ return window.location.pathname.match(/^\/[a-z]{2}(?:\/|$)/);
14
+ }
15
+
16
+ /**
17
+ * smart link component with automatic language prefix handling
18
+ * @param {Object} props - Twink properties
19
+ * @param {string} props.href - Twink URL
20
+ * @param {string|boolean} [props.locale] - Language code or false to disable prefix
21
+ * @param {string} [props.prefetch] - Prefetch mode ('hover' or 'visible')
22
+ * @param {string} [props.access] - Access level for link
23
+ * @param {boolean} [props.external] - Force external link behavior
24
+ * @param {*} props.children - Twink content
25
+ * @returns {Object} Virtual DOM element
26
+ */
27
+ export function Twink(props) {
28
+ const { href, locale, prefetch, access, external, children, ...rest } = props;
29
+ const currentLanguage = getLanguage();
30
+ const isExternal = external || href.startsWith('http') || href.startsWith('//');
31
+
32
+ // check if we're currently in a language-prefixed route
33
+ // this will be evaluated at render time, so it will update on navigation
34
+ const inLanguageRoute = isInLanguageRoute();
35
+
36
+ // apply locale to internal links only if we're in a language route or locale is explicitly set
37
+ let finalHref = href;
38
+ if (!isExternal && (locale || (locale !== false && inLanguageRoute))) {
39
+ const targetLocale = locale || currentLanguage;
40
+ if (!href.startsWith(`/${targetLocale}`)) finalHref = `/${targetLocale}${href === '/' ? '' : href}`;
41
+ }
42
+
43
+ const linkProps = {
44
+ ...rest,
45
+ href: finalHref,
46
+ 'data-spa-link': !isExternal,
47
+ ...(prefetch && { 'data-prefetch': prefetch }),
48
+ ...(access && { 'data-link-access': access }),
49
+ ...(isExternal && { target: '_blank', rel: 'noopener noreferrer' }),
50
+ };
51
+
52
+ return {
53
+ type: 'a',
54
+ props: linkProps,
55
+ children: Array.isArray(children) ? children : [children],
56
+ };
57
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * cumstack Client Components
3
+ * utilities for component hydration
4
+ */
5
+
6
+ /**
7
+ * initialize all cumstack components on the page
8
+ * override this in your app to add custom component initialization
9
+ * @returns {void}
10
+ */
11
+ export function initComponents() {
12
+ document.querySelectorAll('button[onclick*="setCount"]').forEach((button) => {
13
+ let count = 0;
14
+ const match = button.textContent.match(/Clicks: (\d+)/);
15
+ if (match) count = parseInt(match[1]);
16
+ button.removeAttribute('onclick');
17
+ button.addEventListener('click', () => {
18
+ count++;
19
+ button.textContent = `Clicks: ${count}`;
20
+ });
21
+ });
22
+ }
23
+
24
+ // auto-initialize components on DOMContentLoaded
25
+ if (typeof document !== 'undefined') {
26
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', initComponents);
27
+ else initComponents();
28
+ }