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 +21 -0
- package/README.md +3 -0
- package/cli/build.js +19 -0
- package/cli/builder.js +172 -0
- package/cli/create.js +36 -0
- package/cli/dev.js +109 -0
- package/cli/index.js +65 -0
- package/index.js +22 -0
- package/package.json +67 -0
- package/src/app/client/Twink.js +57 -0
- package/src/app/client/components.js +28 -0
- package/src/app/client/hmr.js +161 -0
- package/src/app/client/index.js +144 -0
- package/src/app/client.js +599 -0
- package/src/app/index.js +8 -0
- package/src/app/server/hono-utils.js +292 -0
- package/src/app/server/index.js +457 -0
- package/src/app/server/jsx.js +168 -0
- package/src/app/server.js +373 -0
- package/src/app/shared/i18n.js +271 -0
- package/src/app/shared/language-codes.js +199 -0
- package/src/app/shared/reactivity.js +259 -0
- package/src/app/shared/router.js +153 -0
- package/src/app/shared/utils.js +127 -0
- package/templates/monorepo/README.md +27 -0
- package/templates/monorepo/api/package.json +13 -0
- package/templates/monorepo/app/package.json +19 -0
- package/templates/monorepo/app/src/entry.client.jsx +4 -0
- package/templates/monorepo/app/src/entry.server.jsx +14 -0
- package/templates/monorepo/app/src/main.css +7 -0
- package/templates/monorepo/app/src/pages/404.jsx +9 -0
- package/templates/monorepo/app/src/pages/Home.jsx +8 -0
- package/templates/monorepo/app/wrangler.toml +35 -0
- package/templates/monorepo/package.json +18 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cumstack HMR (Hot Module Reload)
|
|
3
|
+
* built-in hmr for wrangler development
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
let hmrWebSocket = null;
|
|
7
|
+
let reconnectAttempts = 0;
|
|
8
|
+
let reconnectTimeout = null;
|
|
9
|
+
const maxReconnectAttempts = 10;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* initialize hmr connection for development
|
|
13
|
+
* @returns {void}
|
|
14
|
+
*/
|
|
15
|
+
export function initHMR() {
|
|
16
|
+
if (typeof window === 'undefined' || !globalThis.__ENVIRONMENT__?.includes('dev')) return;
|
|
17
|
+
const hmrPort = globalThis.__HMR_PORT__ || 8790;
|
|
18
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
19
|
+
const wsUrl = `${protocol}//${window.location.hostname}:${hmrPort}`;
|
|
20
|
+
connectHMR(wsUrl);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* establish websocket connection to HMR server
|
|
25
|
+
* @param {string} wsUrl - WebSocket URL
|
|
26
|
+
* @returns {void}
|
|
27
|
+
*/
|
|
28
|
+
function connectHMR(wsUrl) {
|
|
29
|
+
try {
|
|
30
|
+
hmrWebSocket = new WebSocket(wsUrl);
|
|
31
|
+
hmrWebSocket.addEventListener('open', () => {
|
|
32
|
+
console.log('[WS] Established connection to HMR server');
|
|
33
|
+
reconnectAttempts = 0;
|
|
34
|
+
if (reconnectTimeout) {
|
|
35
|
+
clearTimeout(reconnectTimeout);
|
|
36
|
+
reconnectTimeout = null;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
hmrWebSocket.addEventListener('message', (event) => {
|
|
41
|
+
const data = JSON.parse(event.data);
|
|
42
|
+
if (data.type === 'server-update') handleServerUpdate(data);
|
|
43
|
+
else if (data.type === 'full-reload') window.location.reload();
|
|
44
|
+
else if (data.type === 'css-update') handleCSSUpdate(data);
|
|
45
|
+
else if (data.type === 'js-update') handleJSUpdate(data);
|
|
46
|
+
else if (data.type === 'reload') window.location.reload();
|
|
47
|
+
else if (data.type === 'update') handleHotUpdate(data);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
hmrWebSocket.addEventListener('close', () => {
|
|
51
|
+
console.log('[WS] Connection closed');
|
|
52
|
+
if (reconnectTimeout) clearTimeout(reconnectTimeout);
|
|
53
|
+
// debounced reconnection
|
|
54
|
+
if (reconnectAttempts < maxReconnectAttempts) {
|
|
55
|
+
reconnectAttempts++;
|
|
56
|
+
const delay = Math.min(1000 * reconnectAttempts, 5000);
|
|
57
|
+
console.log(`[WS] Reconnecting...`);
|
|
58
|
+
reconnectTimeout = setTimeout(() => {
|
|
59
|
+
reconnectTimeout = null;
|
|
60
|
+
connectHMR(wsUrl);
|
|
61
|
+
}, delay);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
hmrWebSocket.addEventListener('error', (error) => console.error('[HMR] WebSocket error:', error));
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error('[HMR] Failed to connect:', error);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* handle server update from HMR
|
|
72
|
+
* @param {Object} data - Update data
|
|
73
|
+
* @returns {void}
|
|
74
|
+
*/
|
|
75
|
+
function handleServerUpdate(data) {
|
|
76
|
+
const currentTimestamp = window.__BUILD_TIMESTAMP__;
|
|
77
|
+
const pollForNewBuild = (attempt = 1, maxAttempts = 40) => {
|
|
78
|
+
fetch(window.location.href, { headers: { 'Cache-Control': 'no-cache', Pragma: 'no-cache' } })
|
|
79
|
+
.then((response) => response.text())
|
|
80
|
+
.then((html) => {
|
|
81
|
+
// extract build timestamp from new html
|
|
82
|
+
const timestampMatch = html.match(/window\.__BUILD_TIMESTAMP__\s*=\s*(\d+)/);
|
|
83
|
+
const newTimestamp = timestampMatch ? parseInt(timestampMatch[1]) : null;
|
|
84
|
+
if (!newTimestamp || newTimestamp === currentTimestamp) {
|
|
85
|
+
// build not ready yet, poll
|
|
86
|
+
if (attempt < maxAttempts) setTimeout(() => pollForNewBuild(attempt + 1, maxAttempts), 100);
|
|
87
|
+
else window.location.reload();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const parser = new DOMParser();
|
|
91
|
+
const newDoc = parser.parseFromString(html, 'text/html');
|
|
92
|
+
const newApp = newDoc.getElementById('app');
|
|
93
|
+
const currentApp = document.getElementById('app');
|
|
94
|
+
if (newApp && currentApp) {
|
|
95
|
+
currentApp.innerHTML = newApp.innerHTML;
|
|
96
|
+
// update build timestamp
|
|
97
|
+
window.__BUILD_TIMESTAMP__ = newTimestamp;
|
|
98
|
+
// re-initialize client-side components
|
|
99
|
+
if (window.initComponents) window.initComponents();
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
.catch((error) => {
|
|
103
|
+
if (attempt < maxAttempts) setTimeout(() => pollForNewBuild(attempt + 1, maxAttempts), 100);
|
|
104
|
+
else {
|
|
105
|
+
console.error('[HMR] Failed to fetch updated HTML:', error);
|
|
106
|
+
window.location.reload();
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
// small delay to let wrangler start reloading, then start polling
|
|
111
|
+
setTimeout(() => pollForNewBuild(), 150);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* handle hot module update
|
|
116
|
+
* @param {Object} data - Update data
|
|
117
|
+
* @returns {void}
|
|
118
|
+
*/
|
|
119
|
+
function handleHotUpdate(data) {
|
|
120
|
+
window.location.reload();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* handle CSS update from HMR
|
|
125
|
+
* @param {Object} data - Update data with timestamp
|
|
126
|
+
* @returns {void}
|
|
127
|
+
*/
|
|
128
|
+
function handleCSSUpdate(data) {
|
|
129
|
+
const links = document.querySelectorAll('link[rel="stylesheet"]');
|
|
130
|
+
links.forEach((link) => {
|
|
131
|
+
const href = link.href;
|
|
132
|
+
const url = new URL(href);
|
|
133
|
+
// add timestamp to bust cache
|
|
134
|
+
url.searchParams.set('t', data.timestamp);
|
|
135
|
+
link.href = url.toString();
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* handle JavaScript update from HMR
|
|
141
|
+
* @param {Object} data - Update data
|
|
142
|
+
* @returns {void}
|
|
143
|
+
*/
|
|
144
|
+
function handleJSUpdate(data) {
|
|
145
|
+
window.location.reload();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* dispose hmr connection and cleanup
|
|
150
|
+
* @returns {void}
|
|
151
|
+
*/
|
|
152
|
+
export function disposeHMR() {
|
|
153
|
+
if (reconnectTimeout) {
|
|
154
|
+
clearTimeout(reconnectTimeout);
|
|
155
|
+
reconnectTimeout = null;
|
|
156
|
+
}
|
|
157
|
+
if (hmrWebSocket) {
|
|
158
|
+
hmrWebSocket.close();
|
|
159
|
+
hmrWebSocket = null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cumstack Client
|
|
3
|
+
* client-side rendering and hydration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { setLanguage, detectBrowserLanguage, clearPreferredLanguage, getUserLanguage } from '../shared/i18n.js';
|
|
7
|
+
import { initComponents } from './components.js';
|
|
8
|
+
import { initHMR } from './hmr.js';
|
|
9
|
+
|
|
10
|
+
export { Twink } from './Twink.js';
|
|
11
|
+
export { configureHydration } from '../client.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* get environment variable (client-side)
|
|
15
|
+
* @param {string} key - environment variable name
|
|
16
|
+
* @param {string} [fallback] - fallback value if not found
|
|
17
|
+
* @returns {string|undefined} environment variable value
|
|
18
|
+
*/
|
|
19
|
+
export function env(key, fallback) {
|
|
20
|
+
if (typeof window === 'undefined') return fallback;
|
|
21
|
+
const envVars = window.__ENV__ || {};
|
|
22
|
+
return envVars[key] ?? fallback;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* client entry wrapper component
|
|
27
|
+
* @param {Object} props - Component props
|
|
28
|
+
* @param {*} props.children - Child elements
|
|
29
|
+
* @returns {*} Children
|
|
30
|
+
*/
|
|
31
|
+
export function CowgirlCreampie({ children }) {
|
|
32
|
+
return children || null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* mount and hydrate the app (client-side initialization)
|
|
37
|
+
* @param {Function} app - App component function
|
|
38
|
+
* @param {HTMLElement|string} container - Container element or selector
|
|
39
|
+
* @returns {void}
|
|
40
|
+
*/
|
|
41
|
+
export function cowgirl(app, container) {
|
|
42
|
+
initHMR();
|
|
43
|
+
if (typeof window !== 'undefined') window.initComponents = initComponents;
|
|
44
|
+
const pathSegments = window.location.pathname.split('/').filter(Boolean);
|
|
45
|
+
let language = 'en';
|
|
46
|
+
let isExplicitRoute = false;
|
|
47
|
+
|
|
48
|
+
if (pathSegments.length > 0 && pathSegments[0].length === 2) {
|
|
49
|
+
// LANG ROUTE - (e.g., /en/, /sv/about)
|
|
50
|
+
language = pathSegments[0];
|
|
51
|
+
isExplicitRoute = true;
|
|
52
|
+
} else {
|
|
53
|
+
// ROOT ROUTE - check for user language first, then use auto-detection
|
|
54
|
+
clearPreferredLanguage();
|
|
55
|
+
const userLang = getUserLanguage();
|
|
56
|
+
if (userLang) language = userLang;
|
|
57
|
+
else language = detectBrowserLanguage();
|
|
58
|
+
}
|
|
59
|
+
setLanguage(language, isExplicitRoute);
|
|
60
|
+
setupNavigation();
|
|
61
|
+
initComponents();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* render virtual node to DOM element
|
|
66
|
+
* @param {*} vnode - Virtual node to render
|
|
67
|
+
* @param {HTMLElement} container - Container element
|
|
68
|
+
* @returns {void}
|
|
69
|
+
*/
|
|
70
|
+
function renderElement(vnode, container) {
|
|
71
|
+
if (vnode == null || vnode === false || vnode === true) return;
|
|
72
|
+
if (typeof vnode === 'string' || typeof vnode === 'number') {
|
|
73
|
+
container.appendChild(document.createTextNode(vnode));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (Array.isArray(vnode)) {
|
|
77
|
+
vnode.forEach((child) => renderElement(child, container));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (typeof vnode === 'function') {
|
|
81
|
+
renderElement(vnode(), container);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (vnode.type === 'fragment') {
|
|
85
|
+
vnode.children?.forEach((child) => renderElement(child, container));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (typeof vnode.type === 'function') {
|
|
89
|
+
const result = vnode.type(vnode.props || {});
|
|
90
|
+
renderElement(result, container);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const element = document.createElement(vnode.type);
|
|
95
|
+
|
|
96
|
+
// set props
|
|
97
|
+
if (vnode.props) {
|
|
98
|
+
Object.entries(vnode.props).forEach(([key, value]) => {
|
|
99
|
+
if (key === 'children') return;
|
|
100
|
+
if (key.startsWith('on') && typeof value === 'function') {
|
|
101
|
+
const eventName = key.slice(2).toLowerCase();
|
|
102
|
+
element.addEventListener(eventName, value);
|
|
103
|
+
} else if (key === 'className') element.className = value;
|
|
104
|
+
else if (key === 'style' && typeof value === 'object') Object.assign(element.style, value);
|
|
105
|
+
else if (key === 'dangerouslySetInnerHTML') element.innerHTML = value.__html;
|
|
106
|
+
else element.setAttribute(key, value);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
// render children
|
|
110
|
+
if (vnode.children) vnode.children.forEach((child) => renderElement(child, element));
|
|
111
|
+
container.appendChild(element);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* setup SPA navigation with language route handling
|
|
116
|
+
* @returns {void}
|
|
117
|
+
*/
|
|
118
|
+
function setupNavigation() {
|
|
119
|
+
// update all spa links to include language prefix if in a language route
|
|
120
|
+
function updateTwinksForLanguage() {
|
|
121
|
+
const inLanguageRoute = window.location.pathname.match(/^\/([a-z]{2})(?:\/|$)/);
|
|
122
|
+
if (!inLanguageRoute) return;
|
|
123
|
+
const lang = inLanguageRoute[1];
|
|
124
|
+
document.querySelectorAll('a[data-spa-link]').forEach((link) => {
|
|
125
|
+
const href = link.getAttribute('href');
|
|
126
|
+
if (!href || href.startsWith('http') || href.startsWith('//')) return;
|
|
127
|
+
// if href doesn't already have language prefix, add it
|
|
128
|
+
if (!href.match(/^\/[a-z]{2}(?:\/|$)/)) link.setAttribute('href', `/${lang}${href === '/' ? '' : href}`);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// update on initial load
|
|
133
|
+
updateTwinksForLanguage();
|
|
134
|
+
|
|
135
|
+
document.addEventListener('click', (e) => {
|
|
136
|
+
const link = e.target.closest('a[data-spa-link]');
|
|
137
|
+
if (!link) return;
|
|
138
|
+
const href = link.getAttribute('href');
|
|
139
|
+
if (!href || href.startsWith('http') || href.startsWith('//')) return;
|
|
140
|
+
e.preventDefault();
|
|
141
|
+
window.history.pushState({}, '', href);
|
|
142
|
+
window.location.reload();
|
|
143
|
+
});
|
|
144
|
+
}
|