cumstack 1.0.1 → 1.0.2
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/cli/builder.js +2 -2
- package/index.js +2 -2
- package/package.json +2 -2
- package/src/app/client/{Twink.js → Lust.js} +4 -4
- package/src/app/client/index.js +3 -3
- package/src/app/client.js +13 -13
- package/src/app/server/index.js +85 -9
- package/src/app/shared/router.js +3 -3
- package/templates/monorepo/app/jsconfig.json +8 -0
- package/templates/monorepo/app/package.json +1 -1
package/cli/builder.js
CHANGED
|
@@ -61,7 +61,7 @@ export async function buildApp(appRoot, isDev = false, buildTimestamp = Date.now
|
|
|
61
61
|
let subpath = args.path.replace('@cumstack/app', '');
|
|
62
62
|
if (subpath === '/server') subpath = '/app/server/index.js';
|
|
63
63
|
else if (subpath === '/client') subpath = '/app/client/index.js';
|
|
64
|
-
else if (subpath === '/client/
|
|
64
|
+
else if (subpath === '/client/Lust') subpath = '/app/client/Lust.js';
|
|
65
65
|
else if (subpath === '/shared/i18n') subpath = '/app/shared/i18n.js';
|
|
66
66
|
else if (subpath === '/shared/reactivity') subpath = '/app/shared/reactivity.js';
|
|
67
67
|
else if (subpath === '/shared/router') subpath = '/app/shared/router.js';
|
|
@@ -128,7 +128,7 @@ export async function buildApp(appRoot, isDev = false, buildTimestamp = Date.now
|
|
|
128
128
|
else if (subpath === '/shared/router') subpath = '/app/shared/router.js';
|
|
129
129
|
else if (subpath === '/shared/utils') subpath = '/app/shared/utils.js';
|
|
130
130
|
else if (subpath === '/shared/language-codes') subpath = '/app/shared/language-codes.js';
|
|
131
|
-
else if (subpath === '/client/
|
|
131
|
+
else if (subpath === '/client/Lust') subpath = '/app/client/Lust.js';
|
|
132
132
|
else if (!subpath.endsWith('.js')) subpath += '.js';
|
|
133
133
|
return {
|
|
134
134
|
path: path.join(__dirname, '..', 'src', subpath),
|
package/index.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
// client exports
|
|
4
4
|
export { cowgirl, CowgirlCreampie, configureHydration } from './src/app/client/index.js';
|
|
5
|
-
export {
|
|
5
|
+
export { Lust } from './src/app/client/Lust.js';
|
|
6
6
|
|
|
7
7
|
// server exports
|
|
8
|
-
export { foxgirl, FoxgirlCreampie, Router, Route, Head, Title, Meta,
|
|
8
|
+
export { foxgirl, FoxgirlCreampie, Router, Route, Head, Title, Meta, LustTag, Script, h, renderToString } from './src/app/server/index.js';
|
|
9
9
|
|
|
10
10
|
// shared exports
|
|
11
11
|
export {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cumstack",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "A lightweight reactive framework with signals, routing, and i18n",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"./app/shared/router": "./src/app/shared/router.js",
|
|
43
43
|
"./app/shared/utils": "./src/app/shared/utils.js",
|
|
44
44
|
"./app/shared/language-codes": "./src/app/shared/language-codes.js",
|
|
45
|
-
"./client/
|
|
45
|
+
"./client/Lust": "./src/app/client/Lust.js",
|
|
46
46
|
"./client/components": "./src/app/client/components.js",
|
|
47
47
|
"./client/hmr": "./src/app/client/hmr.js",
|
|
48
48
|
"./server/components": "./src/app/server/components.js",
|
|
@@ -15,16 +15,16 @@ function isInLanguageRoute() {
|
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* smart link component with automatic language prefix handling
|
|
18
|
-
* @param {Object} props -
|
|
19
|
-
* @param {string} props.href -
|
|
18
|
+
* @param {Object} props - Lust properties
|
|
19
|
+
* @param {string} props.href - Lust URL
|
|
20
20
|
* @param {string|boolean} [props.locale] - Language code or false to disable prefix
|
|
21
21
|
* @param {string} [props.prefetch] - Prefetch mode ('hover' or 'visible')
|
|
22
22
|
* @param {string} [props.access] - Access level for link
|
|
23
23
|
* @param {boolean} [props.external] - Force external link behavior
|
|
24
|
-
* @param {*} props.children -
|
|
24
|
+
* @param {*} props.children - Lust content
|
|
25
25
|
* @returns {Object} Virtual DOM element
|
|
26
26
|
*/
|
|
27
|
-
export function
|
|
27
|
+
export function Lust(props) {
|
|
28
28
|
const { href, locale, prefetch, access, external, children, ...rest } = props;
|
|
29
29
|
const currentLanguage = getLanguage();
|
|
30
30
|
const isExternal = external || href.startsWith('http') || href.startsWith('//');
|
package/src/app/client/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import { setLanguage, detectBrowserLanguage, clearPreferredLanguage, getUserLang
|
|
|
7
7
|
import { initComponents } from './components.js';
|
|
8
8
|
import { initHMR } from './hmr.js';
|
|
9
9
|
|
|
10
|
-
export {
|
|
10
|
+
export { Lust } from './Lust.js';
|
|
11
11
|
export { configureHydration } from '../client.js';
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -117,7 +117,7 @@ function renderElement(vnode, container) {
|
|
|
117
117
|
*/
|
|
118
118
|
function setupNavigation() {
|
|
119
119
|
// update all spa links to include language prefix if in a language route
|
|
120
|
-
function
|
|
120
|
+
function updateLustsForLanguage() {
|
|
121
121
|
const inLanguageRoute = window.location.pathname.match(/^\/([a-z]{2})(?:\/|$)/);
|
|
122
122
|
if (!inLanguageRoute) return;
|
|
123
123
|
const lang = inLanguageRoute[1];
|
|
@@ -130,7 +130,7 @@ function setupNavigation() {
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
// update on initial load
|
|
133
|
-
|
|
133
|
+
updateLustsForLanguage();
|
|
134
134
|
|
|
135
135
|
document.addEventListener('click', (e) => {
|
|
136
136
|
const link = e.target.closest('a[data-spa-link]');
|
package/src/app/client.js
CHANGED
|
@@ -470,11 +470,11 @@ export function cowgirl(app, container) {
|
|
|
470
470
|
if (!link) return;
|
|
471
471
|
const href = link.getAttribute('href');
|
|
472
472
|
if (!href) return;
|
|
473
|
-
const
|
|
473
|
+
const isSpaLust = link.hasAttribute('data-spa-link');
|
|
474
474
|
const isNoSpa = link.hasAttribute('data-no-spa');
|
|
475
475
|
if (isNoSpa) return;
|
|
476
476
|
const isInternal = href.startsWith('/') && !href.startsWith('//');
|
|
477
|
-
if (isInternal ||
|
|
477
|
+
if (isInternal || isSpaLust) {
|
|
478
478
|
e.preventDefault();
|
|
479
479
|
if (i18nConfig?.explicitRouting) {
|
|
480
480
|
const { language } = extractLanguageFromRoute(href);
|
|
@@ -498,7 +498,7 @@ export function cowgirl(app, container) {
|
|
|
498
498
|
*/
|
|
499
499
|
const prefetchedUrls = new Set();
|
|
500
500
|
function setupPrefetching() {
|
|
501
|
-
const
|
|
501
|
+
const observedLusts = new Set();
|
|
502
502
|
const observer = new IntersectionObserver((entries) => {
|
|
503
503
|
entries.forEach((entry) => {
|
|
504
504
|
if (entry.isIntersecting) {
|
|
@@ -509,31 +509,31 @@ function setupPrefetching() {
|
|
|
509
509
|
} else {
|
|
510
510
|
// unobserve links that leave viewport
|
|
511
511
|
const link = entry.target;
|
|
512
|
-
if (!entry.isIntersecting &&
|
|
512
|
+
if (!entry.isIntersecting && observedLusts.has(link)) {
|
|
513
513
|
observer.unobserve(link);
|
|
514
|
-
|
|
514
|
+
observedLusts.delete(link);
|
|
515
515
|
}
|
|
516
516
|
}
|
|
517
517
|
});
|
|
518
518
|
});
|
|
519
|
-
const
|
|
519
|
+
const observeLusts = () => {
|
|
520
520
|
// observe new links
|
|
521
521
|
document.querySelectorAll('a[data-prefetch="visible"]').forEach((link) => {
|
|
522
|
-
if (!
|
|
522
|
+
if (!observedLusts.has(link)) {
|
|
523
523
|
observer.observe(link);
|
|
524
|
-
|
|
524
|
+
observedLusts.add(link);
|
|
525
525
|
}
|
|
526
526
|
});
|
|
527
527
|
// clean up removed links
|
|
528
|
-
|
|
528
|
+
observedLusts.forEach((link) => {
|
|
529
529
|
if (!document.contains(link)) {
|
|
530
530
|
observer.unobserve(link);
|
|
531
|
-
|
|
531
|
+
observedLusts.delete(link);
|
|
532
532
|
}
|
|
533
533
|
});
|
|
534
534
|
};
|
|
535
|
-
|
|
536
|
-
const mutationObserver = new MutationObserver(
|
|
535
|
+
observeLusts();
|
|
536
|
+
const mutationObserver = new MutationObserver(observeLusts);
|
|
537
537
|
mutationObserver.observe(document.body, { childList: true, subtree: true });
|
|
538
538
|
const mouseoverHandler = (e) => {
|
|
539
539
|
const link = e.target.closest('a[data-prefetch="hover"]');
|
|
@@ -549,7 +549,7 @@ function setupPrefetching() {
|
|
|
549
549
|
return () => {
|
|
550
550
|
observer.disconnect();
|
|
551
551
|
mutationObserver.disconnect();
|
|
552
|
-
|
|
552
|
+
observedLusts.clear();
|
|
553
553
|
document.removeEventListener('mouseover', mouseoverHandler);
|
|
554
554
|
};
|
|
555
555
|
}
|
package/src/app/server/index.js
CHANGED
|
@@ -36,7 +36,7 @@ export async function renderToString(vnode) {
|
|
|
36
36
|
return results.join('');
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
// For plain objects from cumstack components (like
|
|
39
|
+
// For plain objects from cumstack components (like Lust)
|
|
40
40
|
if (vnode && typeof vnode === 'object' && vnode.type && vnode.props !== undefined && !vnode.toStringToBuffer) {
|
|
41
41
|
const { type, props, children } = vnode;
|
|
42
42
|
|
|
@@ -173,6 +173,7 @@ const headContext = {
|
|
|
173
173
|
meta: [],
|
|
174
174
|
links: [],
|
|
175
175
|
scripts: [],
|
|
176
|
+
cache: null,
|
|
176
177
|
};
|
|
177
178
|
|
|
178
179
|
export function Head({ children }) {
|
|
@@ -192,7 +193,7 @@ export function Meta(props) {
|
|
|
192
193
|
return null;
|
|
193
194
|
}
|
|
194
195
|
|
|
195
|
-
export function
|
|
196
|
+
export function LustTag(props) {
|
|
196
197
|
headContext.links.push(props);
|
|
197
198
|
return null;
|
|
198
199
|
}
|
|
@@ -210,7 +211,7 @@ function processHeadChild(child) {
|
|
|
210
211
|
else if (child.type === 'script') headContext.scripts.push(child.props);
|
|
211
212
|
else if (child.type === Title) headContext.title = child.children?.[0] || 'App';
|
|
212
213
|
else if (child.type === Meta) headContext.meta.push(child.props);
|
|
213
|
-
else if (child.type ===
|
|
214
|
+
else if (child.type === LustTag) headContext.links.push(child.props);
|
|
214
215
|
else if (child.type === Script) headContext.scripts.push(child.props);
|
|
215
216
|
}
|
|
216
217
|
|
|
@@ -219,8 +220,59 @@ function resetHeadContext() {
|
|
|
219
220
|
headContext.meta = [];
|
|
220
221
|
headContext.links = [];
|
|
221
222
|
headContext.scripts = [];
|
|
223
|
+
headContext.cache = null;
|
|
222
224
|
}
|
|
223
225
|
|
|
226
|
+
export function getHeadContext() {
|
|
227
|
+
return headContext;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function setCache(cacheConfig) {
|
|
231
|
+
headContext.cache = cacheConfig;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* MilkStorage - Cache presets for common use cases
|
|
236
|
+
* Use with setCache() or in metadata.cache
|
|
237
|
+
*
|
|
238
|
+
* Simple syntax:
|
|
239
|
+
* cache: { app: 3600, cdn: 3600 }
|
|
240
|
+
*
|
|
241
|
+
* @example
|
|
242
|
+
* const metadata = {
|
|
243
|
+
* title: 'Home',
|
|
244
|
+
* cache: MilkStorage.medium
|
|
245
|
+
* };
|
|
246
|
+
*
|
|
247
|
+
* Or custom:
|
|
248
|
+
* cache: { app: 3600, cdn: 86400 }
|
|
249
|
+
*/
|
|
250
|
+
export const MilkStorage = {
|
|
251
|
+
// No caching - always fetch fresh
|
|
252
|
+
none: { app: 0 },
|
|
253
|
+
// Short cache - 5 minutes
|
|
254
|
+
short: { app: 300, cdn: 300 },
|
|
255
|
+
// Medium cache - 1 hour app, 6 hours CDN
|
|
256
|
+
medium: { app: 3600, cdn: 21600 },
|
|
257
|
+
// Long cache - 1 hour app, 1 day CDN
|
|
258
|
+
long: { app: 3600, cdn: 86400 },
|
|
259
|
+
// Static content - 1 day app, 1 week CDN
|
|
260
|
+
static: { app: 86400, cdn: 604800 },
|
|
261
|
+
// Dynamic with revalidation - serve stale while updating
|
|
262
|
+
revalidate: {
|
|
263
|
+
app: 60,
|
|
264
|
+
cdn: 3600,
|
|
265
|
+
staleWhileRevalidate: 86400,
|
|
266
|
+
},
|
|
267
|
+
// Resilient - serve stale on errors
|
|
268
|
+
resilient: {
|
|
269
|
+
app: 3600,
|
|
270
|
+
cdn: 86400,
|
|
271
|
+
staleWhileRevalidate: 86400,
|
|
272
|
+
staleIfError: 604800,
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
|
|
224
276
|
/**
|
|
225
277
|
* html document template
|
|
226
278
|
* user provides head and body, cumstack handles structure and script injection
|
|
@@ -243,7 +295,7 @@ async function Document({ content, language }) {
|
|
|
243
295
|
const headElements = customHead({ context: headContext });
|
|
244
296
|
headHtml = await renderToString(headElements);
|
|
245
297
|
} else {
|
|
246
|
-
//
|
|
298
|
+
// render meta tags as strings
|
|
247
299
|
const metaTags = headContext.meta
|
|
248
300
|
.map((m) => {
|
|
249
301
|
const attrs = Object.entries(m)
|
|
@@ -401,13 +453,37 @@ export async function foxgirl(app, options = {}) {
|
|
|
401
453
|
resetHeadContext();
|
|
402
454
|
const language = c.get('language');
|
|
403
455
|
const pageContent = component();
|
|
404
|
-
//
|
|
456
|
+
// convert pageContent to string first before passing to Document
|
|
405
457
|
const contentHtml = await renderToString(pageContent);
|
|
406
458
|
const html = await Document({ content: contentHtml, language });
|
|
407
|
-
//
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
459
|
+
// Set cache headers if configured
|
|
460
|
+
const headers = { 'Content-Type': 'text/html; charset=utf-8' };
|
|
461
|
+
const isDev = globalThis.__ENVIRONMENT__ === 'development';
|
|
462
|
+
|
|
463
|
+
// Always set cache control headers
|
|
464
|
+
if (isDev) headers['Cache-Control'] = 'no-cache, no-store, must-revalidate';
|
|
465
|
+
else if (headContext.cache) {
|
|
466
|
+
const { app = 0, cdn = null, staleWhileRevalidate = null, staleIfError = null, tags = null, mustRevalidate = false } = headContext.cache;
|
|
467
|
+
|
|
468
|
+
const directives = [];
|
|
469
|
+
if (app === 0 && cdn === null) directives.push('no-cache', 'no-store', 'must-revalidate');
|
|
470
|
+
else {
|
|
471
|
+
// If CDN cache is set, make it public; otherwise private
|
|
472
|
+
const isPublic = cdn !== null;
|
|
473
|
+
directives.push(isPublic ? 'public' : 'private');
|
|
474
|
+
if (app > 0) directives.push(`max-age=${app}`);
|
|
475
|
+
if (cdn !== null) directives.push(`s-maxage=${cdn}`);
|
|
476
|
+
if (staleWhileRevalidate !== null) directives.push(`stale-while-revalidate=${staleWhileRevalidate}`);
|
|
477
|
+
if (staleIfError !== null) directives.push(`stale-if-error=${staleIfError}`);
|
|
478
|
+
if (mustRevalidate) directives.push('must-revalidate');
|
|
479
|
+
}
|
|
480
|
+
headers['Cache-Control'] = directives.join(', ');
|
|
481
|
+
// cloudflare-specific: Cache tags for purging
|
|
482
|
+
if (tags && Array.isArray(tags)) headers['Cache-Tag'] = tags.join(',');
|
|
483
|
+
// add vary header for i18n if using explicit routing
|
|
484
|
+
if (globalI18nConfig?.explicitRouting) headers['Vary'] = 'Accept-Language';
|
|
485
|
+
} else headers['Cache-Control'] = 'private, no-cache';
|
|
486
|
+
return new Response(html, { headers });
|
|
411
487
|
} catch (error) {
|
|
412
488
|
console.error('Route error:', error);
|
|
413
489
|
return c.text('Internal Server Error', 500);
|
package/src/app/shared/router.js
CHANGED
|
@@ -113,10 +113,10 @@ export function createRouter() {
|
|
|
113
113
|
|
|
114
114
|
/**
|
|
115
115
|
* create a link component helper
|
|
116
|
-
* @param {string} href -
|
|
117
|
-
* @param {Object} options -
|
|
116
|
+
* @param {string} href - Lust href
|
|
117
|
+
* @param {Object} options - Lust options
|
|
118
118
|
*/
|
|
119
|
-
export function
|
|
119
|
+
export function createLust(href, options = {}) {
|
|
120
120
|
return {
|
|
121
121
|
href,
|
|
122
122
|
onClick: (e) => {
|