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 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/Twink') subpath = '/app/client/Twink.js';
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/Twink') subpath = '/app/client/Twink.js';
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 { Twink } from './src/app/client/Twink.js';
5
+ export { Lust } from './src/app/client/Lust.js';
6
6
 
7
7
  // server exports
8
- export { foxgirl, FoxgirlCreampie, Router, Route, Head, Title, Meta, TwinkTag, Script, h, renderToString } from './src/app/server/index.js';
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.1",
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/Twink": "./src/app/client/Twink.js",
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 - Twink properties
19
- * @param {string} props.href - Twink URL
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 - Twink content
24
+ * @param {*} props.children - Lust content
25
25
  * @returns {Object} Virtual DOM element
26
26
  */
27
- export function Twink(props) {
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('//');
@@ -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 { Twink } from './Twink.js';
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 updateTwinksForLanguage() {
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
- updateTwinksForLanguage();
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 isSpaTwink = link.hasAttribute('data-spa-link');
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 || isSpaTwink) {
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 observedTwinks = new Set();
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 && observedTwinks.has(link)) {
512
+ if (!entry.isIntersecting && observedLusts.has(link)) {
513
513
  observer.unobserve(link);
514
- observedTwinks.delete(link);
514
+ observedLusts.delete(link);
515
515
  }
516
516
  }
517
517
  });
518
518
  });
519
- const observeTwinks = () => {
519
+ const observeLusts = () => {
520
520
  // observe new links
521
521
  document.querySelectorAll('a[data-prefetch="visible"]').forEach((link) => {
522
- if (!observedTwinks.has(link)) {
522
+ if (!observedLusts.has(link)) {
523
523
  observer.observe(link);
524
- observedTwinks.add(link);
524
+ observedLusts.add(link);
525
525
  }
526
526
  });
527
527
  // clean up removed links
528
- observedTwinks.forEach((link) => {
528
+ observedLusts.forEach((link) => {
529
529
  if (!document.contains(link)) {
530
530
  observer.unobserve(link);
531
- observedTwinks.delete(link);
531
+ observedLusts.delete(link);
532
532
  }
533
533
  });
534
534
  };
535
- observeTwinks();
536
- const mutationObserver = new MutationObserver(observeTwinks);
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
- observedTwinks.clear();
552
+ observedLusts.clear();
553
553
  document.removeEventListener('mouseover', mouseoverHandler);
554
554
  };
555
555
  }
@@ -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 Twink)
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 TwinkTag(props) {
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 === TwinkTag) headContext.links.push(child.props);
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
- // Render meta tags as strings
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
- // Convert pageContent to string first before passing to Document
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
- // Return HTML response
408
- return new Response(html, {
409
- headers: { 'Content-Type': 'text/html; charset=utf-8' },
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);
@@ -113,10 +113,10 @@ export function createRouter() {
113
113
 
114
114
  /**
115
115
  * create a link component helper
116
- * @param {string} href - Twink href
117
- * @param {Object} options - Twink options
116
+ * @param {string} href - Lust href
117
+ * @param {Object} options - Lust options
118
118
  */
119
- export function createTwink(href, options = {}) {
119
+ export function createLust(href, options = {}) {
120
120
  return {
121
121
  href,
122
122
  onClick: (e) => {
@@ -0,0 +1,8 @@
1
+ {
2
+ "compilerOptions": {
3
+ "baseUrl": ".",
4
+ "paths": {
5
+ "~/*": ["src/*"]
6
+ }
7
+ }
8
+ }
@@ -8,7 +8,7 @@
8
8
  "start": "bun run dist/server.js"
9
9
  },
10
10
  "dependencies": {
11
- "cumstack": "^1.0.1"
11
+ "cumstack": "^1.0.2"
12
12
  },
13
13
  "devDependencies": {
14
14
  "bun": "^1.3.5",