@cfdez11/vex 0.8.2 → 0.9.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.
Files changed (71) hide show
  1. package/dist/bin/vex.js +3 -0
  2. package/dist/client/services/cache.js +1 -0
  3. package/dist/client/services/hmr-client.js +1 -0
  4. package/dist/client/services/html.js +1 -0
  5. package/dist/client/services/hydrate-client-components.js +1 -0
  6. package/dist/client/services/hydrate.js +1 -0
  7. package/dist/client/services/index.js +1 -0
  8. package/dist/client/services/navigation/create-layouts.js +1 -0
  9. package/dist/client/services/navigation/create-navigation.js +1 -0
  10. package/dist/client/services/navigation/index.js +1 -0
  11. package/dist/client/services/navigation/link-interceptor.js +1 -0
  12. package/dist/client/services/navigation/metadata.js +1 -0
  13. package/dist/client/services/navigation/navigate.js +1 -0
  14. package/dist/client/services/navigation/prefetch.js +1 -0
  15. package/dist/client/services/navigation/render-page.js +1 -0
  16. package/dist/client/services/navigation/render-ssr.js +1 -0
  17. package/dist/client/services/navigation/router.js +1 -0
  18. package/dist/client/services/navigation/use-query-params.js +1 -0
  19. package/dist/client/services/navigation/use-route-params.js +1 -0
  20. package/dist/client/services/navigation.js +1 -0
  21. package/dist/client/services/reactive.js +1 -0
  22. package/dist/server/build-static.js +6 -0
  23. package/dist/server/index.js +4 -0
  24. package/dist/server/prebuild.js +1 -0
  25. package/dist/server/utils/cache.js +1 -0
  26. package/dist/server/utils/component-processor.js +68 -0
  27. package/dist/server/utils/data-cache.js +1 -0
  28. package/dist/server/utils/esbuild-plugin.js +1 -0
  29. package/dist/server/utils/files.js +28 -0
  30. package/dist/server/utils/hmr.js +1 -0
  31. package/dist/server/utils/router.js +11 -0
  32. package/dist/server/utils/streaming.js +1 -0
  33. package/dist/server/utils/template.js +1 -0
  34. package/package.json +8 -7
  35. package/bin/vex.js +0 -69
  36. package/client/favicon.ico +0 -0
  37. package/client/services/cache.js +0 -55
  38. package/client/services/hmr-client.js +0 -22
  39. package/client/services/html.js +0 -377
  40. package/client/services/hydrate-client-components.js +0 -97
  41. package/client/services/hydrate.js +0 -25
  42. package/client/services/index.js +0 -9
  43. package/client/services/navigation/create-layouts.js +0 -172
  44. package/client/services/navigation/create-navigation.js +0 -103
  45. package/client/services/navigation/index.js +0 -8
  46. package/client/services/navigation/link-interceptor.js +0 -39
  47. package/client/services/navigation/metadata.js +0 -23
  48. package/client/services/navigation/navigate.js +0 -64
  49. package/client/services/navigation/prefetch.js +0 -43
  50. package/client/services/navigation/render-page.js +0 -45
  51. package/client/services/navigation/render-ssr.js +0 -157
  52. package/client/services/navigation/router.js +0 -48
  53. package/client/services/navigation/use-query-params.js +0 -225
  54. package/client/services/navigation/use-route-params.js +0 -76
  55. package/client/services/navigation.js +0 -6
  56. package/client/services/reactive.js +0 -247
  57. package/server/build-static.js +0 -138
  58. package/server/index.js +0 -135
  59. package/server/prebuild.js +0 -13
  60. package/server/utils/cache.js +0 -89
  61. package/server/utils/component-processor.js +0 -1631
  62. package/server/utils/data-cache.js +0 -62
  63. package/server/utils/delay.js +0 -1
  64. package/server/utils/esbuild-plugin.js +0 -110
  65. package/server/utils/files.js +0 -845
  66. package/server/utils/hmr.js +0 -21
  67. package/server/utils/router.js +0 -375
  68. package/server/utils/streaming.js +0 -324
  69. package/server/utils/template.js +0 -274
  70. /package/{client → dist/client}/app.webmanifest +0 -0
  71. /package/{server → dist/server}/root.html +0 -0
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import{spawn}from"child_process";import{fileURLToPath}from"url";import path from"path";const __dirname=path.dirname(fileURLToPath(import.meta.url)),serverDir=path.resolve(__dirname,"..","server"),[command]=process.argv.slice(2),commands={dev:()=>spawn("node",["--watch",path.join(serverDir,"index.js")],{stdio:"inherit"}),build:()=>spawn("node",[path.join(serverDir,"prebuild.js")],{stdio:"inherit"}),start:()=>spawn("node",[path.join(serverDir,"index.js")],{stdio:"inherit",env:{...process.env,NODE_ENV:"production"}}),"build:static":()=>spawn("node",[path.join(serverDir,"build-static.js")],{stdio:"inherit"})};commands[command]||(console.error(`Unknown command: "${command}"
3
+ Available: dev, build, build:static, start`),process.exit(1));const child=commands[command]();child.on("exit",code=>process.exit(code??0));
@@ -0,0 +1 @@
1
+ const routeCache=new Map;async function loadRouteComponent(path,importer){if(routeCache.has(path))return routeCache.get(path);const module=await importer();return routeCache.set(path,module),module}async function prefetchRouteComponent(path,importer){try{await loadRouteComponent(path,importer)}catch(e){console.error(`Prefetch failed for route ${path}:`,e)}}function isRouteLoaded(path){return routeCache.has(path)}function clearRouteCache(){routeCache.clear()}var cache_default=routeCache;export{clearRouteCache,cache_default as default,isRouteLoaded,loadRouteComponent,prefetchRouteComponent};
@@ -0,0 +1 @@
1
+ (function(){const evtSource=new EventSource("/_vexjs/hmr");evtSource.addEventListener("reload",e=>{console.log(`[HMR] ${e.data||"file changed"} \u2014 reloading`),location.reload()}),evtSource.onerror=()=>{evtSource.close()}})();
@@ -0,0 +1 @@
1
+ function html(strings,...values){const markers=values.map((_,i)=>`__HTML_MARKER_${i}__`);let htmlString=strings[0];for(let i=0;i<values.length;i++)htmlString+=markers[i]+strings[i+1];const template=document.createElement("template");template.innerHTML=htmlString.trim();const fragment=template.content.cloneNode(!0);processDirectives(fragment,markers,values);const node=fragment.childElementCount===1?fragment.firstElementChild:fragment;return processNode(node,markers,values),node}function processDirectives(node,markers,values){if(node.nodeType!==Node.ELEMENT_NODE&&node.nodeType!==Node.DOCUMENT_FRAGMENT_NODE)return;const children=Array.from(node.childNodes);for(let i=0;i<children.length;i++){const child=children[i];if(child.nodeType===Node.ELEMENT_NODE){if(child.hasAttribute("x-if")){i=handleConditionalChain(node,children,i,markers,values);continue}if(child.hasAttribute("x-for")){handleVFor(child,markers,values);continue}processDirectives(child,markers,values)}}}function handleConditionalChain(parent,children,startIndex,markers,values){const chain=[];let currentIndex=startIndex;for(;currentIndex<children.length;){const element=children[currentIndex];if(element.nodeType!==Node.ELEMENT_NODE){currentIndex++;continue}if(element.hasAttribute("x-if")){if(chain.length>0)break;chain.push({element,type:"if",condition:element.getAttribute("x-if")}),currentIndex++}else if(element.hasAttribute("x-else-if")){if(!chain.length)break;chain.push({element,type:"else-if",condition:element.getAttribute("x-else-if")}),currentIndex++}else if(element.hasAttribute("x-else")){if(!chain.length)break;chain.push({element,type:"else",condition:null}),currentIndex++;break}else break}let kept=null;for(const item of chain){if(kept){item.element.remove();continue}if(item.type==="else")kept=item.element,item.element.removeAttribute("x-else");else{const markerIndex=markers.findIndex(m=>item.condition.includes(m));(markerIndex!==-1?values[markerIndex]:!1)?(kept=item.element,item.element.removeAttribute(item.type==="if"?"x-if":"x-else-if")):item.element.remove()}}return currentIndex-1}function handleVFor(element,markers,values){const vForValue=element.getAttribute("x-for"),markerIndex=markers.findIndex(m=>vForValue.includes(m));if(markerIndex===-1||!Array.isArray(values[markerIndex])){element.removeAttribute("x-for");return}const items=values[markerIndex],parent=element.parentNode,template=element.cloneNode(!0);template.removeAttribute("x-for");const fragment=document.createDocumentFragment();for(const item of items){const clone=template.cloneNode(!0);replaceItemReferences(clone,item),fragment.appendChild(clone)}parent.replaceChild(fragment,element)}function replaceItemReferences(node,item){if(node.nodeType===Node.ELEMENT_NODE){for(const attr of Array.from(node.attributes))if(attr.value.includes("item.")){const prop=attr.value.replace("item.","");node.setAttribute(attr.name,item[prop]??"")}}for(const child of Array.from(node.childNodes))replaceItemReferences(child,item)}function processNode(node,markers,values){if(node.nodeType===Node.TEXT_NODE)return processTextNode(node,markers,values);node.nodeType===Node.ELEMENT_NODE&&processAttributes(node,markers,values);for(const child of Array.from(node.childNodes))processNode(child,markers,values)}function processTextNode(node,markers,values){let text=node.textContent;for(let i=0;i<markers.length;i++){if(!text.includes(markers[i]))continue;const value=values[i],parent=node.parentNode,parts=text.split(markers[i]);if(parts[0]&&parent.insertBefore(document.createTextNode(parts[0]),node),Array.isArray(value))for(const item of value)item instanceof Node?(processNode(item,markers,values),parent.insertBefore(item,node)):parent.insertBefore(document.createTextNode(String(item??"")),node);else value instanceof Node?(processNode(value,markers,values),parent.insertBefore(value,node)):parent.insertBefore(document.createTextNode(String(value??"")),node);text=parts.slice(1).join(markers[i])}node.textContent=text}function getNodePropertyInfo(attrName){return{class:{property:"className",canBeJoined:!0}}[attrName]||{property:attrName,canBeJoined:!1}}function processAttributes(element,markers,values){for(const attr of Array.from(element.attributes)){if(attr.name.startsWith("@")){const event=attr.name.slice(1),idx=markers.findIndex(m=>attr.value.includes(m)),handler=values[idx];typeof handler=="function"&&element.addEventListener(event,handler),element.removeAttribute(attr.name);continue}if(attr.name.startsWith(":")){const{property,canBeJoined}=getNodePropertyInfo(attr.name.slice(1)),idx=markers.findIndex(m=>attr.value.includes(m));if(idx!==-1){const value=values[idx];typeof value=="boolean"?element.toggleAttribute(property,value):element[property]=canBeJoined&&element[property]?`${element[property]} ${value}`:value}element.removeAttribute(attr.name)}if(attr.name==="x-show"){const idx=markers.findIndex(m=>attr.value.includes(m)),value=idx!==-1?values[idx]:!1;element.style.display=value?"":"none",element.removeAttribute("x-show");continue}if(attr.name.startsWith("data-")){const dataAttr=attr.name.slice(5),idx=markers.findIndex(m=>attr.value.includes(m));if(idx!==-1){const value=values[idx];element.dataset[dataAttr]=typeof value=="object"&&value!==null?JSON.stringify(value):String(value??"")}}}}export{html};
@@ -0,0 +1 @@
1
+ (function(){async function hydrateMarker(marker,props={}){if(marker.dataset.hydrated==="true")return;marker.dataset.hydrated="true";const componentName=marker.getAttribute("data-client:component"),componentProps=marker.getAttribute("data-client:props");let parsedProps={};try{parsedProps=JSON.parse(componentProps||"{}")}catch(e){console.warn(`Failed to parse props for component ${componentName}`,e)}const finalProps={...parsedProps,...props};try{await(await import(`/_vexjs/_components/${componentName}.js`)).hydrateClientComponent(marker,finalProps)}catch(error){console.error(`Failed to load component: ${componentName}`,error)}}async function hydrateComponents(container=document,props={}){const markers=container.querySelectorAll("[data-client\\:component]:not([data-hydrated='true'])");for(const marker of markers)await hydrateMarker(marker,props)}new MutationObserver(mutations=>{for(const mutation of mutations)for(const node of mutation.addedNodes)node.nodeType===1&&(node.matches?.("[data-client\\:component]")&&hydrateMarker(node),hydrateComponents(node))}).observe(document,{childList:!0,subtree:!0}),document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>hydrateComponents()):hydrateComponents(),window.hydrateComponents=hydrateComponents})();
@@ -0,0 +1 @@
1
+ window.hydrateTarget=function(targetId,sourceId){const target=document.getElementById(targetId),template=document.getElementById(sourceId);target&&template&&(target.replaceWith(template.content.cloneNode(!0)),template.remove())};
@@ -0,0 +1 @@
1
+ import{initializeRouter,navigate}from"./navigation/index.js";window.app={navigate},document.addEventListener("DOMContentLoaded",()=>{initializeRouter()});
@@ -0,0 +1 @@
1
+ function createLayoutRenderer(){const renderedLayouts=new Map;function cleanNotNeeded(routeLayouts){for(const name of renderedLayouts.keys())routeLayouts.some(l=>l.name===name)||renderedLayouts.delete(name)}function getNearestRendered(routeLayouts){const reversed=routeLayouts.toReversed();for(const layout of reversed)if(renderedLayouts.has(layout.name))return renderedLayouts.get(layout.name);return null}function getLayoutsToRender(routeLayouts,nearestRendered){if(!nearestRendered)return routeLayouts;const reversed=routeLayouts.toReversed(),idx=reversed.findIndex(l=>l.name===nearestRendered.name);return idx===-1?routeLayouts:reversed.slice(0,idx)}async function loadLayoutModules(layouts){return Promise.all(layouts.map(layout=>import(layout.importPath)))}async function generate({routeLayouts=[],pageNode,metadata}){if(!pageNode||routeLayouts.length===0)return{layoutId:null,node:pageNode,metadata};cleanNotNeeded(routeLayouts);const nearestRendered=getNearestRendered(routeLayouts),layoutsToRender=getLayoutsToRender(routeLayouts,nearestRendered),modules=await loadLayoutModules(layoutsToRender);let htmlContainerNode=pageNode,deepestMetadata=metadata;for(let i=modules.length-1;i>=0;i--){const layout=layoutsToRender[i],mod=modules[i],children=htmlContainerNode,marker=document.createElement("template");htmlContainerNode=mod.hydrateClientComponent(marker,{children}),!deepestMetadata&&mod.metadata&&(deepestMetadata=mod.metadata),renderedLayouts.set(layout.name,{name:layout.name,children,node:htmlContainerNode})}return{layoutId:nearestRendered?.name??null,node:htmlContainerNode,metadata:deepestMetadata}}function patch(layoutId,node){const record=renderedLayouts.get(layoutId);record&&(record.children.replaceWith(node),record.children=node)}function reset(){renderedLayouts.clear()}return{generate,patch,reset}}export{createLayoutRenderer};
@@ -0,0 +1 @@
1
+ import{setupLinkInterceptor}from"./link-interceptor.js";import{setupPrefetchObserver}from"./prefetch.js";import{navigateInternal}from"./navigate.js";import{findRouteWithParams}from"./router.js";import{createLayoutRenderer}from"./create-layouts.js";function createNavigationRuntime(){let currentNavigationController=null;const layoutRenderer=createLayoutRenderer();function abortPrevious(){currentNavigationController&&currentNavigationController.abort()}async function navigate(path,addToHistory=!0){abortPrevious();const controller=new AbortController;currentNavigationController=controller;try{await navigateInternal({path,addToHistory,controller,layoutRenderer,onFinish:()=>{currentNavigationController===controller&&(currentNavigationController=null)}})}catch(e){e.name!=="AbortError"&&console.error("Navigation error:",e)}}function initialize(){window.addEventListener("popstate",()=>{navigate(location.pathname,!1)}),setupLinkInterceptor(navigate),setupPrefetchObserver(),layoutRenderer.reset();const{route}=findRouteWithParams(location.pathname);route?.meta?.ssr||navigate(location.pathname,!1)}return{navigate,initialize}}export{createNavigationRuntime};
@@ -0,0 +1 @@
1
+ import{createNavigationRuntime}from"./create-navigation.js";const navigation=createNavigationRuntime(),initializeRouter=navigation.initialize,navigate=navigation.navigate;import{useRouteParams}from"./use-route-params.js";import{useQueryParams}from"./use-query-params.js";export{initializeRouter,navigate,useQueryParams,useRouteParams};
@@ -0,0 +1 @@
1
+ function setupLinkInterceptor(navigate){document.addEventListener("click",event=>{const link=event.target.closest("a");if(!link)return;const href=link.getAttribute("href");if(!href||href.startsWith("#"))return;const url=new URL(href,window.location.origin);url.origin!==window.location.origin||link.dataset.reload!==void 0||link.target==="_blank"||link.rel==="external"||(event.preventDefault(),navigate(url.pathname))})}export{setupLinkInterceptor};
@@ -0,0 +1 @@
1
+ function addMetadata(metadata){if(metadata.title&&(document.title=metadata.title),metadata.description){let meta=document.querySelector('meta[name="description"]');meta||(meta=document.createElement("meta"),meta.name="description",document.head.appendChild(meta)),meta.content=metadata.description}}export{addMetadata};
@@ -0,0 +1 @@
1
+ import{findRouteWithParams}from"./router.js";import{updateRouteParams}from"./use-route-params.js";import{renderPage}from"./render-page.js";import{renderSSRPage}from"./render-ssr.js";async function navigateInternal({path,addToHistory,controller,layoutRenderer,onFinish}){updateRouteParams(path);const routePath=path.split("?")[0],{route}=findRouteWithParams(routePath);addToHistory&&history.pushState({},"",path);try{if(route?.meta?.ssr){layoutRenderer.reset(),await renderSSRPage(path,controller.signal);return}if(route?.meta?.requiresAuth&&!app.Store?.loggedIn){location.href="/account/login";return}if(route?.meta?.guestOnly&&app.Store?.loggedIn){location.href="/account";return}await renderPage({route,layoutRenderer})}finally{onFinish()}}export{navigateInternal};
@@ -0,0 +1 @@
1
+ import{routes}from"../_routes.js";import{prefetchRouteComponent}from"../cache.js";function setupPrefetchObserver(){const observer=new IntersectionObserver(entries=>{entries.forEach(entry=>{if(!entry.isIntersecting)return;const link=entry.target;if(!link.hasAttribute("data-prefetch"))return;const url=new URL(link.href,location.origin),route=routes.find(r=>r.path===url.pathname);route?.component&&(prefetchRouteComponent(route.path,route.component),observer.unobserve(link))})},{rootMargin:"200px"});document.querySelectorAll("a[data-prefetch]").forEach(link=>{link.__prefetchObserved||(link.__prefetchObserved=!0,observer.observe(link))})}export{setupPrefetchObserver};
@@ -0,0 +1 @@
1
+ import{addMetadata}from"./metadata.js";async function renderPage({route,layoutRenderer}){if(!route?.component)return;const mod=await route.component();if(!mod.hydrateClientComponent)return;const root=document.getElementById("app-root")||document.body,marker=document.createElement("template"),pageNode=mod.hydrateClientComponent(marker),{node,layoutId,metadata}=await layoutRenderer.generate({routeLayouts:route.layouts,pageNode,metadata:mod.metadata});layoutId?layoutRenderer.patch(layoutId,node):(root.innerHTML="",root.appendChild(node)),metadata&&addMetadata(metadata),hydrateComponents()}export{renderPage};
@@ -0,0 +1 @@
1
+ async function renderSSRPage(path,signal){const res=await fetch(path,{signal});if(!res.body)throw new Error("Invalid SSR response");const reader=res.body.getReader(),decoder=new TextDecoder,parser=new DOMParser;let buffer="";const main=document.querySelector("main");for(;;){const{done,value}=await reader.read();if(done)break;buffer+=decoder.decode(value,{stream:!0}),buffer=processSSRMain(buffer,parser,main),buffer=processSSRTemplates(buffer,parser),buffer=processSSRScripts(buffer,parser),updateSSRMetadata(buffer,parser),hydrateComponents()}}function processSSRMain(buffer,parser,mainEl){const match=buffer.match(/<main[\s\S]*?<\/main>/i);if(!match)return buffer;const newMain=parser.parseFromString(match[0],"text/html").querySelector("main");return newMain&&mainEl&&(mainEl.innerHTML=newMain.innerHTML),buffer.slice(match.index+match[0].length)}function processSSRTemplates(buffer,parser){const regex=/<template\b[^>]*>[\s\S]*?<\/template>/gi;let match,lastIndex=-1;for(;(match=regex.exec(buffer))!==null;){const template=parser.parseFromString(match[0],"text/html").querySelector("template");template?.id&&document.body.appendChild(template),lastIndex=match.index+match[0].length}return lastIndex!==-1?buffer.slice(lastIndex):buffer}function processSSRScripts(buffer,parser){const regex=/<script[\s\S]*?<\/script>/gi;let match;for(;match=regex.exec(buffer);){const script=parser.parseFromString(match[0],"text/html").querySelector("script");if(script)if(script.src){if(![...document.scripts].some(s=>s.src===script.src)){const s=document.createElement("script");s.src=script.src,s.async=!0,document.head.appendChild(s)}}else try{new Function(script.textContent)()}catch(e){console.error("Error executing inline SSR script:",e)}}const end=buffer.lastIndexOf("<\/script>");return end!==-1?buffer.slice(end+9):buffer}function updateSSRMetadata(buffer,parser){const doc=parser.parseFromString(buffer,"text/html"),title=doc.querySelector("title");title&&(document.title=title.textContent);const desc=doc.querySelector('meta[name="description"]');if(desc){let meta=document.querySelector('meta[name="description"]');meta||(meta=document.createElement("meta"),meta.name="description",document.head.appendChild(meta)),meta.content=desc.content}}export{renderSSRPage};
@@ -0,0 +1 @@
1
+ import{routes}from"../_routes.js";function pathToRegex(routePath){const keys=[];return{regex:new RegExp("^"+routePath.replace(/:([^/]+)/g,(_,key)=>(keys.push(key),"([^/]+)"))+"$"),keys}}function findRouteWithParams(path){for(const r of routes)if(typeof r.path=="string"){const{regex,keys}=pathToRegex(r.path),match=path.match(regex);if(match){const params={};return keys.forEach((k,i)=>params[k]=match[i+1]),{route:r,params}}}else if(r.path instanceof RegExp&&r.path.test(path))return{route:r,params:{}};return{route:null,params:{}}}export{findRouteWithParams};
@@ -0,0 +1 @@
1
+ function parseRawQuery(search){const out={},qs=new URLSearchParams(search);for(const[k,v]of qs.entries())out[k]=v;return out}function buildQueryString(raw){const qs=new URLSearchParams;for(const k in raw)raw[k]!=null&&qs.set(k,String(raw[k]));return qs.toString()}function useQueryParams(options={}){const{schema={},replace=!1,listen=!0}=options,defaults={};for(const key in schema)defaults[key]=schema[key](void 0);let raw=parseRawQuery(window.location.search);function parseWithSchema(raw2){const parsed={};for(const key in schema){const parser=schema[key];parsed[key]=parser(raw2[key])}for(const key in raw2)key in parsed||(parsed[key]=raw2[key]);return parsed}function serializeWithSchema(next){const out={};for(const key in next){const value=next[key];Array.isArray(value)?out[key]=value.join(","):value!=null&&(out[key]=String(value))}return out}function sync(nextRaw){raw=nextRaw;const qs=buildQueryString(raw),url=window.location.pathname+(qs?`?${qs}`:"")+window.location.hash;history[replace?"replaceState":"pushState"](null,"",url)}function set(next){const serialized=serializeWithSchema(next);sync({...raw,...serialized})}function remove(...keys){const next={...raw};keys.forEach(k=>delete next[k]),sync(next)}function reset(){sync({})}return listen&&window.addEventListener("popstate",()=>{raw=parseRawQuery(window.location.search)}),{get params(){return parseWithSchema(raw)},get raw(){return{...raw}},set,remove,reset}}export{useQueryParams};
@@ -0,0 +1 @@
1
+ import{reactive}from"../reactive.js";import{routes}from"../_routes.js";const routeParams=reactive({});function extractParams(pathname){const pathParts=pathname.split("/").filter(Boolean);for(const route of routes){const routeParts=route.path.split("/").filter(Boolean);if(routeParts.length!==pathParts.length)continue;const params={};let match=!0;for(let i=0;i<routeParts.length;i++){const routePart=routeParts[i],pathPart=pathParts[i];if(routePart.startsWith(":"))params[routePart.slice(1)]=pathPart;else if(routePart!==pathPart){match=!1;break}}if(match)return params}return{}}function updateRouteParams(path=window.location.pathname){const newParams=extractParams(path);Object.keys(routeParams).forEach(k=>delete routeParams[k]),Object.assign(routeParams,newParams)}function useRouteParams(){return routeParams}export{updateRouteParams,useRouteParams};
@@ -0,0 +1 @@
1
+ import{useRouteParams,useQueryParams,navigate}from"./navigation/index.js";export{navigate,useQueryParams,useRouteParams};
@@ -0,0 +1 @@
1
+ let activeEffect=null;function adaptPrimitiveValue(input){return input===null||typeof input!="object"?{value:input,__isPrimitive:!0}:input}function reactive(obj){obj=adaptPrimitiveValue(obj);const depsMap=new Map;return new Proxy(obj,{get(target,prop){if(target.__isPrimitive&&prop===Symbol.toPrimitive){if(activeEffect){depsMap.has("value")||depsMap.set("value",new Set);const depSet=depsMap.get("value");depSet.add(activeEffect),activeEffect.deps||(activeEffect.deps=[]),activeEffect.deps.push(depSet)}return()=>target.value}const key=target.__isPrimitive?"value":prop;if(activeEffect){depsMap.has(key)||depsMap.set(key,new Set);const depSet=depsMap.get(key);depSet.add(activeEffect),activeEffect.deps||(activeEffect.deps=[]),activeEffect.deps.push(depSet)}return target[key]},set(target,prop,value){const key=target.__isPrimitive?"value":prop;return target[key]=value,depsMap.has(key)&&depsMap.get(key).forEach(effect2=>effect2()),!0}})}function effect(fn){const wrapped=()=>{activeEffect=wrapped,fn(),activeEffect=null};return wrapped(),()=>{wrapped.deps&&wrapped.deps.forEach(depSet=>depSet.delete(wrapped))}}function computed(getter){let value;return effect(()=>{value=getter()}),new Proxy({},{get(_,prop){if(prop===Symbol.toPrimitive)return()=>value;if(prop==="value")return value;const v=value?.[prop];return typeof v=="function"?v.bind(value):v}})}function watch(source,callback,options={}){let oldValue,cleanupFn;const onCleanup=fn=>{cleanupFn=fn};effect(()=>{const newValue=source();if(oldValue===void 0&&!options.immediate){oldValue=newValue;return}Object.is(newValue,oldValue)||(cleanupFn&&(cleanupFn(),cleanupFn=null),callback(newValue,oldValue,onCleanup),oldValue=newValue)})}export{computed,effect,reactive,watch};
@@ -0,0 +1,6 @@
1
+ import"dotenv/config";import fs from"fs/promises";import path from"path";import{build}from"./utils/component-processor.js";import{initializeDirectories,CLIENT_DIR,PROJECT_ROOT,getRootTemplate,generateComponentId,USER_GENERATED_DIR}from"./utils/files.js";const GENERATED_DIR=path.join(PROJECT_ROOT,".vexjs"),DIST_DIR=path.join(PROJECT_ROOT,"dist");console.log("\u{1F528} Starting static build..."),console.log("\u{1F4C1} Initializing directories..."),await initializeDirectories(),console.log("\u2699\uFE0F Generating components and routes...");const{serverRoutes}=await build();console.log("\u{1F5C2}\uFE0F Creating dist/ structure..."),await fs.rm(DIST_DIR,{recursive:!0,force:!0}),await fs.mkdir(path.join(DIST_DIR,"_vexjs","_components"),{recursive:!0}),await fs.mkdir(path.join(DIST_DIR,"_vexjs","user"),{recursive:!0}),console.log("\u{1F4C4} Generating index.html shell...");const rootTemplate=await getRootTemplate();let shell=rootTemplate.replace(/\{\{metadata\.title\}\}/g,"App").replace(/\{\{metadata\.description\}\}/g,"").replace(/\{\{props\.children\}\}/g,"");const frameworkScripts=["<style>vex-root { display: contents; }</style>",'<script type="module" src="/_vexjs/services/index.js"></script>','<script src="/_vexjs/services/hydrate-client-components.js"></script>','<script src="/_vexjs/services/hydrate.js" id="hydrate-script"></script>'].join(`
2
+ `);shell=shell.replace("</head>",` ${frameworkScripts}
3
+ </head>`),await fs.writeFile(path.join(DIST_DIR,"index.html"),shell,"utf-8"),console.log("\u{1F4E6} Copying framework assets...");for(const asset of["favicon.ico","app.webmanifest"])try{await fs.copyFile(path.join(CLIENT_DIR,asset),path.join(DIST_DIR,"_vexjs",asset))}catch{}console.log("\u{1F4E6} Copying services..."),await fs.cp(path.join(GENERATED_DIR,"services"),path.join(DIST_DIR,"_vexjs","services"),{recursive:!0}),console.log("\u{1F4E6} Copying component bundles..."),await fs.cp(path.join(GENERATED_DIR,"_components"),path.join(DIST_DIR,"_vexjs","_components"),{recursive:!0}),console.log("\u{1F4E6} Copying pre-bundled user JS files...");try{await fs.cp(USER_GENERATED_DIR,path.join(DIST_DIR,"_vexjs","user"),{recursive:!0})}catch{}console.log("\u{1F4E6} Copying public assets...");const publicDir=path.join(PROJECT_ROOT,"public");try{await fs.cp(publicDir,DIST_DIR,{recursive:!0})}catch{}const CACHE_DIR=path.join(GENERATED_DIR,"_cache"),ssgRoutes=serverRoutes.filter(r=>r.meta.revalidate==="never"||r.meta.revalidate===!1);if(ssgRoutes.length>0){console.log("\u{1F4C4} Copying pre-rendered SSG pages...");for(const route of ssgRoutes){const cacheFile=path.join(CACHE_DIR,`${generateComponentId(route.serverPath)}.html`);try{const html=await fs.readFile(cacheFile,"utf-8"),routeSegment=route.serverPath==="/"?"":route.serverPath,destPath=path.join(DIST_DIR,routeSegment,"index.html");await fs.mkdir(path.dirname(destPath),{recursive:!0}),await fs.writeFile(destPath,html,"utf-8"),console.log(` \u2713 ${route.serverPath}`)}catch{console.warn(` \u2717 ${route.serverPath} (no cached HTML found)`)}}}const ssrOnlyRoutes=serverRoutes.filter(r=>r.meta.ssr);if(ssrOnlyRoutes.length>0){console.warn(`
4
+ \u26A0\uFE0F The following routes require a server and were NOT included in the static build:`);for(const r of ssrOnlyRoutes)console.warn(` ${r.path} (SSR)`);console.warn(` These routes will show a 404 in the static build.
5
+ `)}console.log("\u2705 Static build complete! Output: dist/"),console.log(`
6
+ To serve locally: npx serve dist`),console.log("Static host note: configure your host to serve dist/index.html for all 404s (SPA fallback).");
@@ -0,0 +1,4 @@
1
+ import"dotenv/config";import express from"express";import path from"path";import{pathToFileURL}from"url";import{handlePageRequest,revalidatePath}from"./utils/router.js";import{initializeDirectories,CLIENT_DIR,USER_GENERATED_DIR}from"./utils/files.js";await initializeDirectories();let serverRoutes;if(process.env.NODE_ENV==="production")try{const routesPath=path.join(process.cwd(),".vexjs","_routes.js"),{routes}=await import(pathToFileURL(routesPath).href);serverRoutes=routes,console.log("Routes loaded.")}catch{console.error("ERROR: No build found. Run 'vex build' before starting in production."),process.exit(1)}else{const{build}=await import("./utils/component-processor.js"),result=await build();console.log("Components and routes generated."),serverRoutes=result.serverRoutes}const app=express();if(app.use("/_vexjs/_components",express.static(path.join(process.cwd(),".vexjs","_components"),{setHeaders(res,filePath){filePath.endsWith(".js")&&res.setHeader("Content-Type","application/javascript")}})),app.use("/_vexjs/services",express.static(path.join(process.cwd(),".vexjs","services"),{setHeaders(res,filePath){filePath.endsWith(".js")&&res.setHeader("Content-Type","application/javascript")}})),app.use("/_vexjs/user",express.static(USER_GENERATED_DIR,{setHeaders(res,filePath){filePath.endsWith(".js")&&res.setHeader("Content-Type","application/javascript")}})),app.use("/_vexjs",express.static(CLIENT_DIR,{setHeaders(res,filePath){filePath.endsWith(".js")&&res.setHeader("Content-Type","application/javascript")}})),app.use("/",express.static(path.join(process.cwd(),"public"))),app.get("/revalidate",revalidatePath),process.env.NODE_ENV!=="production"){const{hmrEmitter}=await import("./utils/hmr.js");app.get("/_vexjs/hmr",(req,res)=>{res.setHeader("Content-Type","text/event-stream"),res.setHeader("Cache-Control","no-cache"),res.setHeader("Connection","keep-alive"),res.flushHeaders();const onReload=filename=>{res.write(`event: reload
2
+ data: ${filename}
3
+
4
+ `)};hmrEmitter.on("reload",onReload),req.on("close",()=>hmrEmitter.off("reload",onReload))})}const registerSSRRoutes=(app2,routes)=>{routes.forEach(route=>{app2.get(route.serverPath,async(req,res)=>await handlePageRequest(req,res,route))})};registerSSRRoutes(app,serverRoutes),app.use(async(req,res)=>{const notFoundRoute=serverRoutes.find(r=>r.isNotFound);if(notFoundRoute)return handlePageRequest(req,res,notFoundRoute);res.status(404).send("Page not found")});const PORT=process.env.VEX_PORT||process.env.PORT||3001;app.listen(PORT,()=>{console.log(`Server running on port ${PORT}`)});var server_default=app;export{server_default as default};
@@ -0,0 +1 @@
1
+ import"dotenv/config";import{build}from"./utils/component-processor.js";import{initializeDirectories}from"./utils/files.js";console.log("\u{1F528} Starting prebuild..."),console.log("\u{1F4C1} Creating directories..."),await initializeDirectories(),console.log("\u2699\uFE0F Generating components and routes..."),await build(),console.log("\u2705 Prebuild complete!");
@@ -0,0 +1 @@
1
+ import{getComponentHtmlDisk,markComponentHtmlStale,saveComponentHtmlDisk}from"./files.js";async function getCachedComponentHtml({componentPath,revalidateSeconds=0}){const{html,meta}=await getComponentHtmlDisk({componentPath,revalidateSeconds});if(!html)return{html:null};let staleByTime=!1;revalidateSeconds!==-1&&(staleByTime=Date.now()-meta.generatedAt>revalidateSeconds*1e3);const isStale=meta.isStale===!0||staleByTime;return{html,isStale}}async function saveCachedComponentHtml({componentPath,html}){await saveComponentHtmlDisk({componentPath,html})}async function revalidateCachedComponentHtml(componentPath){await markComponentHtmlStale({componentPath})}function getRevalidateSeconds(revalidate){return typeof revalidate=="number"?revalidate:typeof revalidate=="string"&&!Number.isNaN(Number(revalidate))?Number(revalidate):revalidate===!0?60:revalidate==="never"||revalidate===!1?-1:0}export{getCachedComponentHtml,getRevalidateSeconds,revalidateCachedComponentHtml,saveCachedComponentHtml};
@@ -0,0 +1,68 @@
1
+ import{watch}from"fs";import fs from"fs/promises";import path from"path";import esbuild from"esbuild";import{compileTemplateToHTML}from"./template.js";import{getOriginalRoutePath,getPageFiles,getRoutePath,saveClientRoutesFile,saveComponentHtmlDisk,saveServerRoutesFile,readFile,getImportData,generateComponentId,adjustClientModulePath,PAGES_DIR,ROOT_HTML_DIR,getLayoutPaths,SRC_DIR,WATCH_IGNORE,WATCH_IGNORE_FILES,CLIENT_COMPONENTS_DIR,USER_GENERATED_DIR}from"./files.js";import{renderComponents}from"./streaming.js";import{getRevalidateSeconds}from"./cache.js";import{withCache}from"./data-cache.js";import{createVexAliasPlugin}from"./esbuild-plugin.js";function redirect(redirectPath,statusCode=302){const err=new Error("REDIRECT");throw err.redirect={path:redirectPath,statusCode},err}const processHtmlFileCache=new Map;let rootTemplate=await readFile(ROOT_HTML_DIR);if(process.env.NODE_ENV!=="production"){const{hmrEmitter}=await import("./hmr.js");watch(SRC_DIR,{recursive:!0},async(_,filename)=>{if(!filename||filename.split(path.sep).some(part=>WATCH_IGNORE.has(part)))return;const normalizedFilename=filename.replace(/\\/g,"/");if(!WATCH_IGNORE_FILES.some(pattern=>path.matchesGlob(normalizedFilename,pattern))){if(filename.endsWith(".vex")){const fullPath=path.join(SRC_DIR,filename);processHtmlFileCache.delete(fullPath),processedComponentsInBuild.delete(fullPath);try{await generateComponentAndFillCache(fullPath)}catch(e){console.error(`[HMR] Re-generation failed for ${filename}:`,e.message)}hmrEmitter.emit("reload",filename)}else if(filename.endsWith(".js")){const fullPath=path.join(SRC_DIR,filename);try{await buildUserFile(fullPath)}catch(e){console.error(`[HMR] Failed to rebuild user file ${filename}:`,e.message)}hmrEmitter.emit("reload",filename)}}}),watch(ROOT_HTML_DIR,async()=>{rootTemplate=await readFile(ROOT_HTML_DIR),hmrEmitter.emit("reload","root.html")})}const DEFAULT_METADATA={title:"Vanilla JS App",description:"Default description"},getScriptImports=async(script,isClientSide=!1,filePath=null)=>{const componentRegistry=new Map,imports={},clientImports={},importRegex=/import\s+(?:([a-zA-Z_$][\w$]*)|\{([^}]*)\})\s+from\s+['"]([^'"]+)['"]/g;let match;for(;(match=importRegex.exec(script))!==null;){const[importStatement,defaultImport,namedImports,modulePath]=match,{path:path2,fileUrl}=await getImportData(modulePath,filePath);if(path2.endsWith(".vex"))defaultImport&&componentRegistry.set(defaultImport,{path:path2,originalPath:modulePath,importStatement});else if(isClientSide)if(defaultImport){const adjustedClientModule=adjustClientModulePath(modulePath,importStatement,filePath);clientImports[defaultImport||namedImports]={fileUrl,originalPath:adjustedClientModule.path,importStatement:adjustedClientModule.importStatement,originalImportStatement:importStatement}}else namedImports.split(",").forEach(name=>{const trimmedName=name.trim(),adjustedClientModule=adjustClientModulePath(modulePath,importStatement,filePath);clientImports[trimmedName]={fileUrl,originalPath:adjustedClientModule.path,importStatement:adjustedClientModule.importStatement,originalImportStatement:importStatement}});else{const module=await import(fileUrl);defaultImport&&(imports[defaultImport]=module.default||module[defaultImport]),namedImports&&namedImports.split(",").forEach(name=>{const trimmedName=name.trim();imports[trimmedName]=module[trimmedName]})}}return{imports,componentRegistry,clientImports}};async function _processHtmlFile(filePath){const content=await readFile(filePath),serverMatch=content.match(/<script server>([\s\S]*?)<\/script>/),clientMatch=content.match(/<script client>([\s\S]*?)<\/script>/),templateMatch=content.match(/<template>([\s\S]*?)<\/template>/),template=templateMatch?templateMatch[1].trim():"",clientCode=clientMatch?clientMatch[1].trim():"";let serverComponents=new Map,clientComponents=new Map,clientImports={},getData=null,getStaticPaths=null,getMetadata=null;if(serverMatch){const scriptContent=serverMatch[1],{componentRegistry,imports}=await getScriptImports(scriptContent);serverComponents=componentRegistry;const cleanedScript=scriptContent.replace(/import\s+(?:(?:[a-zA-Z_$][\w$]*)|\{[^}]*\})\s+from\s+['"][^'"]*['"];?\n?/g,"").replace(/export\s+/g,"").trim();if(cleanedScript){const AsyncFunction=Object.getPrototypeOf(async function(){}).constructor,fn=new AsyncFunction("redirect","withCache",...Object.keys(imports),`
2
+ ${cleanedScript}
3
+ ${cleanedScript.includes("getData")?"":"const getData = null;"}
4
+ ${cleanedScript.includes("const metadata = ")?"":"const metadata = null;"}
5
+ ${cleanedScript.includes("getMetadata")?"":"const getMetadata = null;"}
6
+ ${cleanedScript.includes("getStaticPaths")?"":"const getStaticPaths = null;"}
7
+ return { getData, metadata, getMetadata, getStaticPaths };
8
+ `);try{const result=await fn(redirect,withCache,...Object.values(imports));getData=result.getData,getStaticPaths=result.getStaticPaths,getMetadata=result.metadata?()=>result.metadata:result.getMetadata}catch(error){console.error(`Error executing script in ${filePath}:`,error.message)}}}if(clientMatch){const{componentRegistry,clientImports:newClientImports}=await getScriptImports(clientMatch[1],!0,filePath);clientComponents=componentRegistry,clientImports=newClientImports}return{getStaticPaths,getData,getMetadata,template,clientCode,serverComponents,clientComponents,clientImports}}async function processHtmlFile(filePath){if(processHtmlFileCache.has(filePath))return processHtmlFileCache.get(filePath);const result=await _processHtmlFile(filePath);return processHtmlFileCache.set(filePath,result),result}async function renderHtmlFile(filePath,context={},extraComponentData={}){const{getData,getMetadata,template,clientCode,serverComponents,clientComponents,clientImports}=await processHtmlFile(filePath),componentData=getData?await getData(context):{},metadata=getMetadata?await getMetadata({req:context.req,props:componentData}):null;return{html:compileTemplateToHTML(template,{...componentData,...extraComponentData}),metadata,clientCode,serverComponents,clientComponents,clientImports}}function generateClientScriptTags({clientCode,clientImports={},clientComponentsScripts=[],clientComponents=new Map}){if(!clientCode)return"";for(const{importStatement}of clientComponents.values())clientCode=clientCode.replace(`${importStatement};`,"").replace(importStatement,"");for(const importData of Object.values(clientImports))importData.originalImportStatement&&importData.importStatement!==importData.originalImportStatement&&(clientCode=clientCode.replace(importData.originalImportStatement,importData.importStatement));const clientCodeWithoutComponentImports=clientCode.split(`
9
+ `).filter(line=>!/^\s*import\s+.*['"].*\.vex['"]/.test(line)).join(`
10
+ `).trim();return`
11
+ ${clientCodeWithoutComponentImports.trim()?`<script type="module">
12
+ ${clientCodeWithoutComponentImports}
13
+ </script>`:""}
14
+ ${clientComponentsScripts?.length?clientComponentsScripts.join(`
15
+ `):""}
16
+ `.trim()}async function renderPage(pagePath,ctx,awaitSuspenseComponents=!1,extraComponentData={}){const{html,metadata,clientCode,clientImports,serverComponents,clientComponents}=await renderHtmlFile(pagePath,ctx,extraComponentData),{html:htmlWithComponents,suspenseComponents,clientComponentsScripts=[]}=await renderComponents({html,serverComponents,clientComponents,awaitSuspenseComponents});return{html:htmlWithComponents,metadata,clientCode,clientImports,serverComponents,clientComponents,suspenseComponents,clientComponentsScripts}}async function renderLayouts(pagePath,pageContent,pageHead={}){const layoutPaths=await getLayoutPaths(pagePath);let currentContent=pageContent,deepMetadata=pageHead.metadata||{};for(let i=layoutPaths.length-1;i>=0;i--){const layoutPath=layoutPaths[i];try{const{html,metadata}=await renderPage(layoutPath,{},!1,{props:{children:currentContent}});deepMetadata={...deepMetadata,...metadata},currentContent=html}catch{console.warn(`Error rendering ${layoutPath}, skipping`);continue}}const{clientScripts,...restPageHead}=pageHead;currentContent=compileTemplateToHTML(rootTemplate,{...restPageHead,metadata:deepMetadata,props:{children:currentContent}});const frameworkScripts=["<style>vex-root { display: contents; }</style>",'<script type="module" src="/_vexjs/services/index.js"></script>','<script src="/_vexjs/services/hydrate-client-components.js"></script>','<script src="/_vexjs/services/hydrate.js" id="hydrate-script"></script>',process.env.NODE_ENV!=="production"?'<script src="/_vexjs/services/hmr-client.js"></script>':"",clientScripts||""].filter(Boolean).join(`
17
+ `);return currentContent=currentContent.replace("</head>",` ${frameworkScripts}
18
+ </head>`),currentContent}async function renderPageWithLayout(pagePath,ctx={},awaitSuspenseComponents=!1){const{html:pageHtml,metadata,clientCode,clientImports,serverComponents,clientComponents,suspenseComponents,clientComponentsScripts}=await renderPage(pagePath,ctx,awaitSuspenseComponents),clientScripts=generateClientScriptTags({clientCode,clientImports,clientComponentsScripts,clientComponents});return{html:await renderLayouts(pagePath,pageHtml,{clientScripts,metadata:{...DEFAULT_METADATA,...metadata}}),pageHtml,metadata,suspenseComponents,serverComponents,clientComponents}}function convertVueToHtmlTagged(template,clientCode=""){const reactiveVars=new Set,reactiveRegex=/(?:const|let|var)\s+(\w+)\s*=\s*(?:reactive|computed)\(/g;let match;for(;(match=reactiveRegex.exec(clientCode))!==null;)reactiveVars.add(match[1]);const processExpression=expr=>expr.replace(/\b(\w+)(?!\s*[\.\(])/g,(_,varName)=>reactiveVars.has(varName)?`${varName}.value`:varName);let result=template.trim();return result=result.replace(/<([\w-]+)([^>]*)\s+x-for="(\w+)\s+in\s+([^"]+)(?:\.value)?"([^>]*)\/>/g,(_,tag,beforeAttrs,iterVar,arrayVar,afterAttrs)=>{const cleanExpr=arrayVar.trim();return`\${${/^\w+$/.test(cleanExpr)&&reactiveVars.has(cleanExpr)?`${cleanExpr}.value`:cleanExpr}.map(${iterVar} => html\`<${tag}${beforeAttrs}${afterAttrs} />\`)}`}),result=result.replace(/<([\w-]+)([^>]*)\s+x-for="(\w+)\s+in\s+([^"]+)(?:\.value)?"([^>]*)>([\s\S]*?)<\/\1>/g,(_,tag,beforeAttrs,iterVar,arrayVar,afterAttrs,content)=>{const cleanExpr=arrayVar.trim();return`\${${/^\w+$/.test(cleanExpr)&&reactiveVars.has(cleanExpr)?`${cleanExpr}.value`:cleanExpr}.map(${iterVar} => html\`<${tag}${beforeAttrs}${afterAttrs}>${content}</${tag}>\`)}`}),result=result.replace(/x-show="([^"]+)"/g,(_,condition)=>`x-show="\${${processExpression(condition)}}"`),result=result.replace(/\{\{([^}]+)\}\}/g,(_,expr)=>`\${${processExpression(expr.trim())}}`),result=result.replace(/@(\w+)="([^"]+)"/g,(_,event,handler)=>{const isArrowFunction=/^\s*\(?.*?\)?\s*=>/.test(handler),isFunctionCall=/[\w$]+\s*\(.*\)/.test(handler.trim());return isArrowFunction?`@${event}="\${${handler.trim()}}"`:isFunctionCall?`@${event}="\${() => ${handler.trim()}}"`:`@${event}="\${${handler.trim()}}"`}),result=result.replace(/:(\w+)="(?!\$\{)([^"]+)"/g,(_,attr,value)=>`:${attr}='\${${processExpression(value)}}'`),result=result.replace(/x-if="([^"]*)"/g,'x-if="${$1}"'),result=result.replace(/x-else-if="([^"]*)"/g,'x-else-if="${$1}"'),result}async function generateClientComponentModule({clientCode,template,metadata,clientImports,clientComponents,componentFilePath,componentName}){if(!clientCode&&!template)return null;const defaults=extractVPropsDefaults(clientCode),clientCodeWithProps=addComputedProps(clientCode,defaults),cleanClientCode=clientCodeWithProps.replace(/const\s+props\s*=\s*xprops\s*\([\s\S]*?\)\s*;?/g,"").replace(/^\s*import\s+.*$/gm,"").trim(),convertedTemplate=convertVueToHtmlTagged(template,clientCodeWithProps),{html:processedHtml}=await renderComponents({html:convertedTemplate,clientComponents}),importLines=new Set(Object.values(clientImports).map(ci=>ci.originalImportStatement).filter(Boolean)),hasEffect=[...importLines].some(l=>/\beffect\b/.test(l)),hasHtml=[...importLines].some(l=>/\bhtml\b/.test(l));hasEffect||importLines.add("import { effect } from 'vex/reactive';"),hasHtml||importLines.add("import { html } from 'vex/html';");const entrySource=`
19
+ ${[...importLines].join(`
20
+ `)}
21
+
22
+ export const metadata = ${JSON.stringify(metadata)};
23
+
24
+ export function hydrateClientComponent(marker, incomingProps = {}) {
25
+ ${cleanClientCode}
26
+
27
+ const wrapper = document.createElement("vex-root");
28
+ marker.replaceWith(wrapper);
29
+
30
+ function render() {
31
+ const node = html\`${processedHtml}\`;
32
+ wrapper.replaceChildren(node);
33
+ }
34
+
35
+ effect(() => render());
36
+ return wrapper;
37
+ }
38
+ `.trim(),outfile=path.join(CLIENT_COMPONENTS_DIR,`${componentName}.js`);return await esbuild.build({stdin:{contents:entrySource,resolveDir:componentFilePath?path.dirname(componentFilePath):CLIENT_COMPONENTS_DIR},bundle:!0,outfile,format:"esm",platform:"browser",plugins:[createVexAliasPlugin()],logLevel:"silent"}),null}function getIfPageCanCSR(revalidate,hasServerComponents,hasGetData){const neverRevalidate=getRevalidateSeconds(revalidate??0)===-1;return!hasServerComponents&&(neverRevalidate||!hasGetData)}async function generateServerComponentHTML(componentPath){const{getStaticPaths,getData,getMetadata,serverComponents,...restProcessHtmlFile}=await processHtmlFile(componentPath),metadata=getMetadata?await getMetadata({req:{params:{}},props:{}}):null,canCSR=getIfPageCanCSR(metadata?.revalidate,serverComponents.size>0,typeof getData=="function"),paths=getStaticPaths?await getStaticPaths():[],result={htmls:[],canCSR,metadata,getStaticPaths,getData,getMetadata,serverComponents,...restProcessHtmlFile};if(!componentPath.includes(PAGES_DIR))return result;if(paths.length===0&&getData){const{html,pageHtml,metadata:pageMetadata}=await renderPageWithLayout(componentPath,{},!0);return result.htmls.push({params:{},html,pageHtml,metadata:pageMetadata}),result}for(const path2 of paths){const{html,pageHtml,metadata:metadata2}=await renderPageWithLayout(componentPath,{req:path2},!0);result.htmls.push({params:path2.params,html,pageHtml,metadata:metadata2})}return result}async function processClientComponent(componentName,componentAbsPath,props={}){const targetId=`client-${componentName}-${Date.now()}`,componentImport=generateComponentId(componentAbsPath),propsJson=serializeClientComponentProps(props);return`<template id="${targetId}" data-client:component="${componentImport}" data-client:props='${propsJson}'></template>`}function isTemplateExpression(value){return typeof value=="string"&&/^\$\{[\s\S]+\}$/.test(value.trim())}function serializeRuntimePropValue(value){return isTemplateExpression(value)?value.trim().slice(2,-1).trim():JSON.stringify(value)}function serializeClientComponentProps(props={}){return Object.values(props).some(isTemplateExpression)?`\${JSON.stringify({ ${Object.entries(props).map(([key,value])=>`${JSON.stringify(key)}: ${serializeRuntimePropValue(value)}`).join(", ")} })}`:JSON.stringify(props)}function extractVPropsObject(clientCode){const match=clientCode.match(/xprops\s*\(\s*(\{[\s\S]*?\})\s*\)/);return match?match[1]:null}function extractVPropsDefaults(clientCode){const xpropsLiteral=extractVPropsObject(clientCode);if(!xpropsLiteral)return{};const xpropsDef=safeObjectEval(xpropsLiteral),defaults={};for(const key in xpropsDef){const def=xpropsDef[key];def&&typeof def=="object"&&"default"in def&&(defaults[key]=def.default)}return defaults}function safeObjectEval(objectLiteral){return Function(`"use strict"; return (${objectLiteral})`)()}function applyDefaultProps(xpropsDefined,componentProps){const finalProps={};for(const key in xpropsDefined){const def=xpropsDefined[key];key in componentProps?finalProps[key]=componentProps[key]:"default"in def?finalProps[key]=def.default:finalProps[key]=void 0}return finalProps}function computeProps(clientCode,componentProps){const xpropsLiteral=extractVPropsObject(clientCode);if(!xpropsLiteral)return componentProps;const xpropsDefined=safeObjectEval(xpropsLiteral);return applyDefaultProps(xpropsDefined,componentProps)}function addComputedProps(clientCode,componentProps){const xpropsRegex=/const\s+props\s*=\s*xprops\s*\([\s\S]*?\)\s*;?/;if(!xpropsRegex.test(clientCode))return clientCode;const computedProps=computeProps(clientCode,componentProps);return clientCode.replace(xpropsRegex,`const props = { ...${JSON.stringify(computedProps)}, ...incomingProps };`)}async function getMetadataAndStaticPaths(getMetadata,getStaticPaths){const promises=[];getMetadata&&promises.push(getMetadata({req:{params:{}},props:{}})),getStaticPaths&&promises.push(getStaticPaths());const[metadata,paths]=await Promise.all(promises);return{metadata:metadata||DEFAULT_METADATA,paths:paths||[]}}function fillRoute(route,params){return route.replace(/:([a-zA-Z0-9_]+)/g,(_,key)=>{if(params[key]===void 0)throw new Error(`Missing parameter "${key}"`);return params[key]})}async function saveClientComponent({metadata,clientCode,template,clientImports,clientComponents,componentName,componentFilePath}){await generateClientComponentModule({metadata,clientCode,template,clientImports,clientComponents,componentFilePath,componentName})}const processedComponentsInBuild=new Set;async function generateComponentAndFillCache(filePath){if(processedComponentsInBuild.has(filePath))return"Already processed";processedComponentsInBuild.add(filePath);const urlPath=getRoutePath(filePath),{template,htmls:serverHtmls,canCSR,clientImports,metadata,clientCode,clientComponents,serverComponents}=await generateServerComponentHTML(filePath),saveServerHtmlsPromises=[],saveClientHtmlPromises=[],saveComponentsPromises=[];if(serverHtmls.length)for(const{params,html,pageHtml,metadata:pageMetadata}of serverHtmls){const cacheKey=fillRoute(urlPath,params);saveServerHtmlsPromises.push(saveComponentHtmlDisk({componentPath:cacheKey,html})),canCSR&&saveServerHtmlsPromises.push(saveClientComponent({metadata:pageMetadata,clientCode,template:pageHtml,clientImports,clientComponents,componentName:generateComponentId(cacheKey),componentFilePath:filePath}))}if(canCSR&&serverHtmls.length===0&&saveClientHtmlPromises.push(saveClientComponent({metadata,clientCode,template,clientImports,clientComponents,componentName:generateComponentId(urlPath),componentFilePath:filePath})),serverComponents.size>0){const serverComponentPaths=Array.from(serverComponents.values()).map(({path:path2})=>path2);saveComponentsPromises.push(...serverComponentPaths.map(generateComponentAndFillCache))}if(clientComponents.size>0){const clientComponentPaths=Array.from(clientComponents.values()).map(({path:path2})=>path2);saveComponentsPromises.push(...clientComponentPaths.map(generateComponentAndFillCache))}return await Promise.all([...saveServerHtmlsPromises,...saveClientHtmlPromises,...saveComponentsPromises]),"Component generated"}async function generateComponentsAndFillCache(){processedComponentsInBuild.clear();const generateComponentsPromises=(await getPageFiles({layouts:!0})).map(file=>generateComponentAndFillCache(file.fullpath));return await Promise.all(generateComponentsPromises),"Components generation completed"}async function getRouteFileData(file){const data={serverRoutes:[],clientRoutes:[]},[processedFileData,layoutPaths]=await Promise.all([processHtmlFile(file.fullpath),getLayoutPaths(file.fullpath)]),{getData,getMetadata,getStaticPaths,serverComponents}=processedFileData,filePath=getOriginalRoutePath(file.fullpath),urlPath=getRoutePath(file.fullpath),{metadata,paths}=await getMetadataAndStaticPaths(getMetadata,getStaticPaths),canCSR=getIfPageCanCSR(metadata?.revalidate,serverComponents.size>0,typeof getData=="function");if(data.serverRoutes.push({path:filePath,serverPath:urlPath,isNotFound:file.path.includes("/not-found/"),meta:{ssr:!canCSR,requiresAuth:!1,revalidate:metadata?.revalidate??0}}),!canCSR)return data.clientRoutes.push(`{
39
+ path: "${urlPath}",
40
+ meta: {
41
+ ssr: true,
42
+ requiresAuth: false,
43
+ },
44
+ }`),data;const componentsBasePath="/_vexjs/_components",layoutsImportData=layoutPaths.map(layoutPath=>{const urlPath2=getRoutePath(layoutPath),layoutComponentName=generateComponentId(urlPath2);return{name:layoutComponentName,importPath:`${componentsBasePath}/${layoutComponentName}.js`}});if(paths.length>0)for(const pathObj of paths){const filledPath=fillRoute(urlPath,pathObj.params),componentName=generateComponentId(filledPath),importPath=`${componentsBasePath}/${componentName}.js`;data.clientRoutes.push(`{
45
+ path: "${filledPath}",
46
+ component: async () => {
47
+ const mod = await loadRouteComponent("${filledPath}", () => import("${importPath}"));
48
+
49
+ return { hydrateClientComponent: mod.hydrateClientComponent, metadata: mod.metadata };
50
+ },
51
+ layouts: ${JSON.stringify(layoutsImportData)},
52
+ meta: {
53
+ ssr: false,
54
+ requiresAuth: false,
55
+ },
56
+ }`)}else{const componentName=generateComponentId(urlPath),importPath=`${componentsBasePath}/${componentName}.js`;data.clientRoutes.push(`{
57
+ path: "${urlPath}",
58
+ component: async () => {
59
+ const mod = await loadRouteComponent("${urlPath}", () => import("${importPath}"));
60
+
61
+ return { hydrateClientComponent: mod.hydrateClientComponent, metadata: mod.metadata };
62
+ },
63
+ layouts: ${JSON.stringify(layoutsImportData)},
64
+ meta: {
65
+ ssr: false,
66
+ requiresAuth: false,
67
+ },
68
+ }`)}return data}async function generateRoutes(){const pageFiles=await getPageFiles(),serverRoutes=[],clientRoutes=[],routeFilesPromises=pageFiles.map(pageFile=>getRouteFileData(pageFile)),routeFiles=await Promise.all(routeFilesPromises);for(const routeFile of routeFiles){const{serverRoutes:serverRoutesFile,clientRoutes:clientRoutesFile}=routeFile;serverRoutesFile?.length&&serverRoutes.push(...serverRoutesFile),clientRoutesFile?.length&&clientRoutes.push(...clientRoutesFile)}return await Promise.all([saveClientRoutesFile(clientRoutes),saveServerRoutesFile(serverRoutes)]),{serverRoutes}}async function buildUserFile(filePath){const rel=path.relative(SRC_DIR,filePath).replace(/\\/g,"/"),outfile=path.join(USER_GENERATED_DIR,rel);await esbuild.build({entryPoints:[filePath],bundle:!0,format:"esm",outfile,plugins:[createVexAliasPlugin()]})}async function buildUserFiles(){const collect=async dir=>{let entries;try{entries=await fs.readdir(dir,{withFileTypes:!0})}catch{return}await Promise.all(entries.map(async entry=>{if(WATCH_IGNORE.has(entry.name))return;const full=path.join(dir,entry.name);if(entry.isDirectory())await collect(full);else if(entry.name.endsWith(".js"))try{await buildUserFile(full)}catch(e){console.error(`[build] Failed to bundle user file ${full}:`,e.message)}}))};await collect(SRC_DIR)}async function build(){return await generateComponentsAndFillCache(),await buildUserFiles(),generateRoutes()}export{build,generateClientComponentModule,generateComponentsAndFillCache,generateRoutes,processClientComponent,processHtmlFile,renderHtmlFile,renderPageWithLayout};
@@ -0,0 +1 @@
1
+ const cache=new Map;function withCache(key,ttlSeconds,fn){const entry=cache.get(key);if(entry&&Date.now()<entry.expiresAt)return entry.value;const result=fn();return result instanceof Promise?result.then(value=>(cache.set(key,{value,expiresAt:Date.now()+ttlSeconds*1e3}),value)):(cache.set(key,{value:result,expiresAt:Date.now()+ttlSeconds*1e3}),result)}export{withCache};
@@ -0,0 +1 @@
1
+ import path from"path";import{SRC_DIR,PROJECT_ROOT}from"./files.js";function createVexAliasPlugin(){return{name:"vex-aliases",setup(build){build.onResolve({filter:/^vex\//},args=>{let mod=args.path.replace(/^vex\//,"");return path.extname(mod)||(mod+=".js"),{path:`/_vexjs/services/${mod}`,external:!0}}),build.onResolve({filter:/^@\//},args=>{let mod=args.path.slice(2);return path.extname(mod)||(mod+=".js"),{path:`/_vexjs/user/${mod}`,external:!0}}),build.onResolve({filter:/^\.\.?\//},args=>{let resolved=path.resolve(args.resolveDir,args.path);return path.extname(resolved)||(resolved+=".js"),!resolved.endsWith(".js")||!resolved.startsWith(SRC_DIR)&&!resolved.startsWith(PROJECT_ROOT)?void 0:{path:`/_vexjs/user/${path.relative(SRC_DIR,resolved).replace(/\\/g,"/")}`,external:!0}})}}}export{createVexAliasPlugin};
@@ -0,0 +1,28 @@
1
+ import fs from"fs/promises";import{watch,existsSync,statSync,readFileSync}from"fs";import path from"path";import crypto from"crypto";import{fileURLToPath,pathToFileURL}from"url";const __filename=fileURLToPath(import.meta.url),__dirname=path.dirname(__filename),FRAMEWORK_DIR=path.resolve(__dirname,"..",".."),PROJECT_ROOT=process.cwd(),ROOT_DIR=PROJECT_ROOT;let _vexConfig={};try{_vexConfig=JSON.parse(readFileSync(path.join(PROJECT_ROOT,"vex.config.json"),"utf-8"))}catch{}const SRC_DIR=path.resolve(PROJECT_ROOT,_vexConfig.srcDir||"."),WATCH_IGNORE=new Set(["dist","build","out",".output",".vexjs","public","node_modules",".git",".svn","coverage",".nyc_output",".next",".nuxt",".svelte-kit",".astro","tmp","temp",".cache",".claude",...(_vexConfig.watchIgnore||[]).filter(p=>!/[\/\*\.]/.test(p))]),WATCH_IGNORE_FILES=(_vexConfig.watchIgnore||[]).filter(p=>/[\/\*\.]/.test(p)),PAGES_DIR=path.resolve(SRC_DIR,"pages"),SERVER_APP_DIR=path.join(FRAMEWORK_DIR,"server"),CLIENT_DIR=path.join(FRAMEWORK_DIR,"client"),CLIENT_SERVICES_DIR=path.join(CLIENT_DIR,"services"),GENERATED_DIR=path.join(PROJECT_ROOT,".vexjs"),CACHE_DIR=path.join(GENERATED_DIR,"_cache"),CLIENT_COMPONENTS_DIR=path.join(GENERATED_DIR,"_components"),USER_GENERATED_DIR=path.join(GENERATED_DIR,"user"),ROOT_HTML_USER=path.join(PROJECT_ROOT,"root.html"),ROOT_HTML_DEFAULT=path.join(FRAMEWORK_DIR,"server","root.html"),ROOT_HTML_DIR=ROOT_HTML_USER;async function initializeDirectories(){try{const servicesDir=path.join(GENERATED_DIR,"services");return await Promise.all([fs.mkdir(GENERATED_DIR,{recursive:!0}),fs.mkdir(CACHE_DIR,{recursive:!0}),fs.mkdir(CLIENT_COMPONENTS_DIR,{recursive:!0}),fs.mkdir(USER_GENERATED_DIR,{recursive:!0}),fs.mkdir(servicesDir,{recursive:!0})]),await fs.cp(CLIENT_SERVICES_DIR,servicesDir,{recursive:!0}),!0}catch(err){console.error("Failed to create cache directory:",err)}}function adjustClientModulePath(modulePath,importStatement,componentFilePath=null){if(modulePath.startsWith("/_vexjs/"))return{path:modulePath,importStatement};const isRelative=(modulePath.startsWith("./")||modulePath.startsWith("../"))&&componentFilePath,isAtAlias=modulePath.startsWith("@/")||modulePath==="@";if(isRelative||isAtAlias){let resolvedPath;if(isAtAlias)resolvedPath=path.resolve(SRC_DIR,modulePath.replace(/^@\//,"").replace(/^@$/,""));else{const componentDir=path.dirname(componentFilePath);resolvedPath=path.resolve(componentDir,modulePath)}path.extname(resolvedPath)||(existsSync(resolvedPath+".js")?resolvedPath+=".js":existsSync(path.join(resolvedPath,"index.js"))?resolvedPath=path.join(resolvedPath,"index.js"):resolvedPath+=".js");const adjustedPath2=`/_vexjs/user/${path.relative(SRC_DIR,resolvedPath).replace(/\\/g,"/")}`,adjustedImportStatement2=importStatement.replace(modulePath,adjustedPath2);return{path:adjustedPath2,importStatement:adjustedImportStatement2}}let relative=modulePath.replace(/^vex\//,""),adjustedPath=`/_vexjs/services/${relative}`;const fsPath=path.join(CLIENT_SERVICES_DIR,relative);existsSync(fsPath)&&statSync(fsPath).isDirectory()?adjustedPath+="/index.js":path.extname(adjustedPath)||(adjustedPath+=".js");const adjustedImportStatement=importStatement.replace(modulePath,adjustedPath);return{path:adjustedPath,importStatement:adjustedImportStatement}}function getRelativePath(from,to){return path.relative(from,to)}function getDirectoryName(filePath){return path.dirname(filePath)}const layoutPathsCache=new Map;process.env.NODE_ENV!=="production"&&watch(PAGES_DIR,{recursive:!0},(_,filename)=>{(filename==="layout.vex"||filename?.endsWith(`${path.sep}layout.vex`))&&layoutPathsCache.clear()});async function _getLayoutPaths(pagePath){const layouts=[],relativePath=getRelativePath(PAGES_DIR,pagePath),pathSegments=getDirectoryName(relativePath).split(path.sep),baseLayout=path.join(PAGES_DIR,"layout.vex");await fileExists(baseLayout)&&layouts.push(baseLayout);let currentPath=PAGES_DIR;for(const segment of pathSegments){if(segment==="."||segment==="..")continue;currentPath=path.join(currentPath,segment);const layoutPath=path.join(currentPath,"layout.vex");await fileExists(layoutPath)&&layouts.push(layoutPath)}return layouts}async function getLayoutPaths(pagePath){if(layoutPathsCache.has(pagePath))return layoutPathsCache.get(pagePath);const result=await _getLayoutPaths(pagePath);return layoutPathsCache.set(pagePath,result),result}function formatFileContent(content){return content.trim()}async function writeFile(filePath,content){const formattedContent=formatFileContent(content);return fs.writeFile(filePath,formattedContent,"utf-8")}function readFile(filePath){return fs.readFile(filePath,"utf-8")}async function fileExists(filePath){try{return await fs.access(filePath),!0}catch{return!1}}function getAutogeneratedComponentName(componentPath){return`_${componentPath.replace(ROOT_DIR+path.sep,"").split(path.sep).filter(Boolean).join("_").replaceAll(".vex","").replaceAll(path.sep,"_").replaceAll("-","_").replaceAll(":","")}`}function generateComponentId(componentPath,options={}){const{length=8,prefix=!0}=options,relativePath=componentPath.replace(ROOT_DIR+path.sep,""),hash=crypto.createHash("sha256").update(relativePath).digest("hex").slice(0,length),baseName=getAutogeneratedComponentName(componentPath).replace(/^_/,"");return prefix?`_${baseName}_${hash}`:hash}const getPagePath=pageName=>path.resolve(PAGES_DIR,pageName,"page.vex"),getRootTemplate=async()=>{try{return await fs.access(ROOT_HTML_USER),await fs.readFile(ROOT_HTML_USER,"utf-8")}catch{return await fs.readFile(ROOT_HTML_DEFAULT,"utf-8")}};async function readDirectoryRecursive(dir){const entries=await fs.readdir(dir,{withFileTypes:!0}),files=[];for(const entry of entries){const fullpath=path.join(dir,entry.name);entry.isDirectory()?files.push(...await readDirectoryRecursive(fullpath)):files.push({path:fullpath.replace(ROOT_DIR,""),fullpath,name:entry.name})}return files}const getComponentNameFromPath=(fullFilepath,fileName)=>{const filePath=fullFilepath.replace(ROOT_DIR+path.sep,"");if(filePath.startsWith(path.join("pages",path.sep))){const segments=filePath.split(path.sep);return segments.length===2?segments[0].replace(".vex",""):segments[segments.length-2].replace(".vex","")}return fileName.replace(".vex","")};async function getComponentHtmlDisk({componentPath}){const filePath=path.join(CACHE_DIR,generateComponentId(componentPath)+".html"),metaPath=filePath+".meta.json",[existsHtml,existsMeta]=await Promise.all([fileExists(filePath),fileExists(metaPath)]);if(!existsMeta||!existsHtml)return{html:null,meta:null};const[html,meta]=await Promise.all([fs.readFile(filePath,"utf-8"),fs.readFile(metaPath,"utf-8")]).then(([htmlContent,metaContent])=>[htmlContent,JSON.parse(metaContent)]);return{html,meta}}async function saveComponentHtmlDisk({componentPath,html}){const filePath=path.join(CACHE_DIR,generateComponentId(componentPath)+".html"),metaPath=filePath+".meta.json",meta={generatedAt:Date.now(),isStale:!1,path:componentPath};await Promise.all([writeFile(filePath,html,"utf-8"),writeFile(metaPath,JSON.stringify(meta),"utf-8")])}async function markComponentHtmlStale({componentPath}){const metaPath=path.join(CACHE_DIR,generateComponentId(componentPath)+".html")+".meta.json";if(!await fileExists(metaPath))return;const meta=JSON.parse(await fs.readFile(metaPath,"utf-8"));meta.isStale=!0,await writeFile(metaPath,JSON.stringify(meta),"utf-8")}async function saveServerRoutesFile(serverRoutes){await writeFile(path.join(GENERATED_DIR,"_routes.js"),`// Auto-generated by prebuild \u2014 do not edit manually.
2
+ export const routes = ${JSON.stringify(serverRoutes,null,2)};
3
+ `)}async function saveClientRoutesFile(clientRoutes){const clientFileCode=`
4
+ import { loadRouteComponent } from './cache.js';
5
+
6
+
7
+ /**
8
+ * @typedef {Object} RouteMeta
9
+ * @property {boolean} ssr
10
+ * @property {boolean} requiresAuth
11
+ * @property {number} revalidateSeconds
12
+ */
13
+
14
+ /**
15
+ * @typedef {Object} Route
16
+ * @property {string} path
17
+ * @property {string} serverPath
18
+ * @property {boolean} isNotFound
19
+ * @property {(marker: HTMLElement) => Promise<{ render: (marker: string) => void, metadata: any}>} [component]
20
+ * @property {RouteMeta} meta
21
+ * @property {Array<{ name: string, importPath: string }>} [layouts]
22
+ */
23
+
24
+ export const routes = [
25
+ ${clientRoutes.join(`,
26
+ `)}
27
+ ];
28
+ `;await writeFile(path.join(GENERATED_DIR,"services","_routes.js"),clientFileCode)}function getOriginalRoutePath(filePath){let route=filePath.replace(PAGES_DIR,"").replace("/page.vex","");return route.startsWith("/")||(route="/"+route),route}async function getPageFiles({layouts=!1}={}){return(await readDirectoryRecursive(PAGES_DIR)).filter(file=>file.fullpath.endsWith("page.vex")||layouts&&file.name==="layout.vex")}function getRoutePath(filePath){let route=filePath.replace(PAGES_DIR,"").replace("/page.vex","");return route=route.replace(/\[([^\]]+)\]/g,":$1"),route.startsWith("/")||(route="/"+route),route}async function saveClientComponentModule(componentName,jsModuleCode){const outputPath=path.join(CLIENT_COMPONENTS_DIR,`${componentName}.js`);await writeFile(outputPath,jsModuleCode,"utf-8")}async function getImportData(importPath,callerFilePath=null){let resolvedPath;importPath.startsWith("vex/server/")?resolvedPath=path.resolve(FRAMEWORK_DIR,importPath.replace("vex/server/","server/")):importPath.startsWith("vex/")?resolvedPath=path.resolve(FRAMEWORK_DIR,"client/services",importPath.replace("vex/","")):importPath.startsWith("@/")||importPath==="@"?resolvedPath=path.resolve(SRC_DIR,importPath.replace(/^@\//,"").replace(/^@$/,"")):(importPath.startsWith("./")||importPath.startsWith("../"))&&callerFilePath?resolvedPath=path.resolve(path.dirname(callerFilePath),importPath):resolvedPath=path.resolve(ROOT_DIR,importPath),existsSync(resolvedPath)&&statSync(resolvedPath).isDirectory()&&(resolvedPath=path.join(resolvedPath,"index.js"));const fileUrl=pathToFileURL(resolvedPath).href;return{path:resolvedPath,fileUrl,importPath}}export{CLIENT_COMPONENTS_DIR,CLIENT_DIR,CLIENT_SERVICES_DIR,PAGES_DIR,PROJECT_ROOT,ROOT_HTML_DIR,SERVER_APP_DIR,SRC_DIR,USER_GENERATED_DIR,WATCH_IGNORE,WATCH_IGNORE_FILES,adjustClientModulePath,fileExists,generateComponentId,getComponentHtmlDisk,getComponentNameFromPath,getImportData,getLayoutPaths,getOriginalRoutePath,getPageFiles,getPagePath,getRelativePath,getRootTemplate,getRoutePath,initializeDirectories,markComponentHtmlStale,readDirectoryRecursive,readFile,saveClientComponentModule,saveClientRoutesFile,saveComponentHtmlDisk,saveServerRoutesFile,writeFile};
@@ -0,0 +1 @@
1
+ import{EventEmitter}from"events";const hmrEmitter=new EventEmitter;export{hmrEmitter};
@@ -0,0 +1,11 @@
1
+ import{renderSuspenseComponent,generateReplacementContent}from"./streaming.js";import{getCachedComponentHtml,getRevalidateSeconds,revalidateCachedComponentHtml,saveCachedComponentHtml}from"./cache.js";import{getPagePath}from"./files.js";import{renderPageWithLayout}from"./component-processor.js";const revalidatingRoutes=new Set,FALLBACK_ERROR_HTML=`
2
+ <!DOCTYPE html>
3
+ <html>
4
+ <head><title>Error 500</title></head>
5
+ <body>
6
+ <h1>Error 500 - Internal Server Error</h1>
7
+ <p>An unexpected error has occurred.</p>
8
+ <p><a href="/">Back to home</a></p>
9
+ </body>
10
+ </html>
11
+ `,sendResponse=(res,statusCode,html)=>{res.writeHead(statusCode,{"Content-Type":"text/html"}),res.end(html)},sendStartStreamChunkResponse=(res,statusCode,html,htmlChunks)=>{res.writeHead(statusCode,{"Content-Type":"text/html; charset=utf-8","Transfer-Encoding":"chunked","X-Content-Type-Options":"nosniff"}),sendStreamChunkResponse(res,html,htmlChunks)},sendStreamChunkResponse=(res,html,htmlChunks)=>{res.write(html),htmlChunks.push(html)},endStreamResponse=(res,htmlChunks)=>{res.write("</body></html>"),res.end(),htmlChunks.push("</body></html>")};async function revalidateInBackground(pagePath,context,cacheKey){try{const{html}=await renderPageWithLayout(pagePath,context,!0);await saveCachedComponentHtml({componentPath:cacheKey,html})}catch(error){console.error(`[ISR] Background revalidation failed for ${cacheKey}:`,error.message)}}async function renderAndSendPage({pageName,statusCode=200,context={},route}){const pagePath=getPagePath(pageName),revalidateSeconds=getRevalidateSeconds(route.meta?.revalidate??0),isISR=revalidateSeconds!==0,isrCacheKey=new URL(context.req.url,"http://x").pathname;if(isISR){const{html:cachedHtml,isStale}=await getCachedComponentHtml({componentPath:isrCacheKey,revalidateSeconds});if(cachedHtml&&!isStale){sendResponse(context.res,statusCode,cachedHtml);return}if(cachedHtml&&isStale){sendResponse(context.res,statusCode,cachedHtml),revalidatingRoutes.has(isrCacheKey)||(revalidatingRoutes.add(isrCacheKey),revalidateInBackground(pagePath,context,isrCacheKey).finally(()=>revalidatingRoutes.delete(isrCacheKey)));return}}const{html,suspenseComponents,serverComponents}=await renderPageWithLayout(pagePath,context);if(suspenseComponents.length===0){sendResponse(context.res,statusCode,html),isISR&&saveCachedComponentHtml({componentPath:isrCacheKey,html});return}const htmlChunks=[];let abortedStream=!1,errorStream=!1;context.res.on("close",()=>abortedStream=!0);const[beforeClosing]=html.split("</body>");sendStartStreamChunkResponse(context.res,200,beforeClosing,htmlChunks);const renderPromises=suspenseComponents.map(async suspenseComponent=>{try{const renderedContent=await renderSuspenseComponent(suspenseComponent,serverComponents),replacementContent=generateReplacementContent(suspenseComponent.id,renderedContent);sendStreamChunkResponse(context.res,replacementContent,htmlChunks)}catch(error){console.error(`Error rendering suspense ${suspenseComponent.id}:`,error);const errorContent=generateReplacementContent(suspenseComponent.id,'<div class="text-red-500">Error loading content</div>');context.res.write(errorContent),errorStream=!0}});await Promise.all(renderPromises),endStreamResponse(context.res,htmlChunks),isISR&&!abortedStream&&!errorStream&&saveCachedComponentHtml({componentPath:isrCacheKey,html:htmlChunks.join("")})}async function handlePageRequest(req,res,route){const pageName=route.path.slice(1),context={req,res};try{await renderAndSendPage({pageName,context,route})}catch(e){if(console.error(`[500] Error rendering page "${route.path}":`,e),e.redirect){res.redirect(e.redirect.statusCode,e.redirect.path);return}const errorData={message:e.message||"Internal server error",code:500,details:"Could not load the requested page",path:route.path,stack:e.stack};try{await renderAndSendPage({pageName:"error",statusCode:500,context:{...context,...errorData},route})}catch(err){console.warn("error}}}}}}}}}}",err),console.error(`Failed to render error page: ${err.message}`),sendResponse(res,500,FALLBACK_ERROR_HTML)}}}async function revalidatePath(req,res){try{const componentPath=req.query.path;return componentPath?(await revalidateCachedComponentHtml(componentPath),res.status(200).json({message:`Cache for '${componentPath}' marked as stale. It will regenerate on next request.`})):res.status(400).json({error:"Missing 'path' query parameter"})}catch(err){return console.error("Error revalidating cache:",err),res.status(500).json({error:"Failed to revalidate cache"})}}export{handlePageRequest,revalidatePath};
@@ -0,0 +1 @@
1
+ import{processClientComponent,renderHtmlFile}from"./component-processor.js";function decodeAttrValue(raw){const decoded=raw.replace(/&quot;/g,'"');if(decoded.startsWith("[")||decoded.startsWith("{"))try{return JSON.parse(decoded)}catch{}return decoded}function parseAttributes(rawAttrs){const attrs={},regex=/:([\w-]+)=(?:"([^"]*)"|'([^']*)')|@([\w-]+)=(?:"([^"]*)"|'([^']*)')|([\w:-]+)=(?:"([^"]*)"|'([^']*)')/g;let match;for(;(match=regex.exec(rawAttrs))!==null;)match[1]?attrs[match[1]]=match[2]??match[3]??"":match[4]?attrs[match[4]]=match[5]??match[6]??"":match[7]&&(attrs[match[7]]=decodeAttrValue(match[8]??match[9]??""));return attrs}async function processServerComponents(html,serverComponents){let processedHtml=html;for(const[componentName,componentData]of serverComponents.entries()){const escapedName=componentName.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),componentRegex=new RegExp(`<${escapedName}(?![a-zA-Z0-9_-])\\s*([^>]*?)\\s*(?:\\/>|>\\s*<\\/${escapedName}(?![a-zA-Z0-9_-])>)`,"gi"),replacements=[];let match;for(;(match=componentRegex.exec(processedHtml))!==null;)replacements.push({name:componentName,attrs:parseAttributes(match[1]),fullMatch:match[0],start:match.index,end:match.index+match[0].length});if(replacements.length===0)continue;const rendered=await Promise.all(replacements.map(({attrs})=>renderHtmlFile(componentData.path,attrs)));for(let i=replacements.length-1;i>=0;i--){const{start,end}=replacements[i];processedHtml=processedHtml.slice(0,start)+rendered[i].html+processedHtml.slice(end)}}return processedHtml}async function renderClientComponents(html,clientComponents){let processedHtml=html;const allScripts=[];for(const[componentName,{path:componentAbsPath}]of clientComponents.entries()){const escapedName=componentName.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),componentRegex=new RegExp(`<${escapedName}\\b((?:\\s+(?:[^\\s>"'=]+(?:=(?:"[^"]*"|'[^']*'|[^\\s"'=<>]+))?))*?)\\s*\\/?>`,"gi"),replacements=[];let match;const htmlToProcess=processedHtml;for(;(match=componentRegex.exec(htmlToProcess))!==null;){const matchData={name:componentName,attrs:parseAttributes(match[1]),fullMatch:match[0],start:match.index,end:match.index+match[0].length};replacements.push(matchData)}for(let i=replacements.length-1;i>=0;i--){const{start,end,attrs}=replacements[i],htmlComponent=await processClientComponent(componentName,componentAbsPath,attrs);processedHtml=processedHtml.slice(0,start)+htmlComponent+processedHtml.slice(end)}}return{html:processedHtml,allScripts}}async function renderServerComponents(pageHtml,serverComponents=new Map,awaitSuspenseComponents=!1){const suspenseComponents=[];let suspenseId=0,html=pageHtml;const suspenseRegex=/<Suspense\s+fallback="([^"]*)">([\s\S]*?)<\/Suspense>/g;let match;for(;(match=suspenseRegex.exec(html))!==null;){const id=`suspense-${suspenseId++}`,[fullMatch,fallback,content]=match,fallbackHtml=await processServerComponents(awaitSuspenseComponents?content:fallback,serverComponents);suspenseComponents.push({id,content});const replacement=`<div id="${id}">${fallbackHtml}</div>`;html=html.replace(fullMatch,replacement),suspenseRegex.lastIndex=0}return html=await processServerComponents(html,serverComponents),{html,suspenseComponents}}async function renderComponents({html,serverComponents=new Map,clientComponents=new Map,awaitSuspenseComponents=!1}){const hasServerComponents=serverComponents.size>0,hasClientComponents=clientComponents.size>0,{html:htmlServerComponents,suspenseComponents}=hasServerComponents?await renderServerComponents(html,serverComponents,awaitSuspenseComponents):{html,suspenseComponents:[]},{html:htmlClientComponents,allScripts:clientComponentsScripts}=hasClientComponents?await renderClientComponents(htmlServerComponents,clientComponents):{html:htmlServerComponents,allScripts:[]};return{html:htmlClientComponents,suspenseComponents,clientComponentsScripts}}function generateReplacementContent(suspenseId,renderedContent){const contentId=`${suspenseId}-content`;return`<template id="${contentId}">${renderedContent}</template><script>window.hydrateTarget("${suspenseId}","${contentId}")</script>`}async function renderSuspenseComponent(suspenseComponent,serverComponents){return await processServerComponents(suspenseComponent.content,serverComponents)}export{generateReplacementContent,renderComponents,renderSuspenseComponent};
@@ -0,0 +1 @@
1
+ import{parseDocument,DomUtils}from"htmlparser2";import{render}from"dom-serializer";const fnCache=new Map;function getDataValue(expression,scope){const keys=Object.keys(scope),cacheKey=`${expression}::${keys.join(",")}`;fnCache.has(cacheKey)||fnCache.set(cacheKey,Function(...keys,`return (${expression})`));try{return fnCache.get(cacheKey)(...Object.values(scope))}catch{return""}}function isEmptyTextNode(node){return node.type==="text"&&/^\s*$/.test(node.data)}function parseHTMLToNodes(html){try{const cleanHtml=html.replace(/[\r\n\t]+/g," ").replace(/ +/g," ").trim(),dom=parseDocument(cleanHtml,{xmlMode:!0});return DomUtils.getChildren(dom)}catch(error){return console.error("Error parsing HTML:",error),[]}}function processNode(node,scope,previousRendered=!1){if(node.type==="text")return node.data=node.data.replace(/(?<!\\)\{\{(.+?)\}\}/g,(_,expr)=>getDataValue(expr.trim(),scope)).replace(/\\\{\{/g,"{{"),node;if(node.type==="tag"){const attrs=node.attribs||{};for(const[attrName,attrValue]of Object.entries(attrs))typeof attrValue=="string"&&(attrs[attrName]=attrValue.replace(/(?<!\\)\{\{(.+?)\}\}/g,(_,expr)=>getDataValue(expr.trim(),scope)).replace(/\\\{\{/g,"{{"));if("x-if"in attrs){const show=getDataValue(attrs["x-if"],scope);if(delete attrs["x-if"],!show)return null}if("x-else-if"in attrs){const show=getDataValue(attrs["x-else-if"],scope);if(delete attrs["x-else-if"],previousRendered||!show)return null}if("x-else"in attrs&&(delete attrs["x-else"],previousRendered))return null;if("x-show"in attrs){const show=getDataValue(attrs["x-show"],scope);delete attrs["x-show"],show||(attrs.style=(attrs.style||"")+"display:none;")}if("x-for"in attrs){const exp=attrs["x-for"];delete attrs["x-for"];const match=exp.match(/(.+?)\s+in\s+(.+)/);if(!match)throw new Error("Invalid x-for format: "+exp);const itemName=match[1].trim(),listExpr=match[2].trim(),list=getDataValue(listExpr,scope);if(!Array.isArray(list))return null;const clones=[];for(const item of list){const cloned=structuredClone(node),newScope={...scope,[itemName]:item};clones.push(processNode(cloned,newScope))}return clones}for(const[name,value]of Object.entries({...attrs})){if(name.startsWith(":")){const isSuspenseFallback=name===":fallback"&&node.name==="Suspense",realName=name.slice(1);if(isSuspenseFallback)attrs[realName]=value;else{const val=getDataValue(value,scope);attrs[realName]=val!=null&&typeof val=="object"?JSON.stringify(val):String(val??"")}delete attrs[name]}if(name.startsWith("x-bind:")){const realName=name.slice(7),val=getDataValue(value,scope);attrs[realName]=val!=null&&typeof val=="object"?JSON.stringify(val):String(val??""),delete attrs[name]}}for(const[name]of Object.entries({...attrs}))(name.startsWith("@")||name.startsWith("x-on:"))&&delete attrs[name];if(node.children){const result=[];let isPreviousRendered=!1;for(const child of node.children){if(isEmptyTextNode(child))continue;const processed=processNode(child,scope,isPreviousRendered);Array.isArray(processed)?(result.push(...processed),isPreviousRendered=processed.length>0):processed?(result.push(processed),isPreviousRendered=!0):isPreviousRendered=!1}node.children=result}return node}return node}const parsedTemplateCache=new Map;function compileTemplateToHTML(template,data={}){try{parsedTemplateCache.has(template)||parsedTemplateCache.set(template,parseHTMLToNodes(template));const processed=structuredClone(parsedTemplateCache.get(template)).map(n=>processNode(n,data)).flat().filter(Boolean);return render(processed,{encodeEntities:!1})}catch(error){throw console.error("Error compiling template:",error),error}}export{compileTemplateToHTML};
package/package.json CHANGED
@@ -1,20 +1,21 @@
1
1
  {
2
2
  "name": "@cfdez11/vex",
3
- "version": "0.8.2",
3
+ "version": "0.9.0",
4
4
  "description": "A vanilla JavaScript meta-framework with file-based routing, SSR/CSR/SSG/ISR and Vue-like reactivity",
5
5
  "type": "module",
6
- "main": "./server/index.js",
6
+ "main": "./dist/server/index.js",
7
7
  "exports": {
8
- ".": "./server/index.js"
8
+ ".": "./dist/server/index.js"
9
9
  },
10
10
  "bin": {
11
- "vex": "./bin/vex.js"
11
+ "vex": "./dist/bin/vex.js"
12
12
  },
13
13
  "files": [
14
- "bin",
15
- "server",
16
- "client"
14
+ "dist"
17
15
  ],
16
+ "scripts": {
17
+ "prepublishOnly": "node build-dist.js"
18
+ },
18
19
  "engines": {
19
20
  "node": ">=18.0.0"
20
21
  },
package/bin/vex.js DELETED
@@ -1,69 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { spawn } from "child_process";
4
- import { fileURLToPath } from "url";
5
- import path from "path";
6
-
7
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
- const serverDir = path.resolve(__dirname, "..", "server");
9
-
10
- const [command] = process.argv.slice(2);
11
-
12
- /**
13
- * Available CLI commands.
14
- *
15
- * Each entry is a factory function that calls `spawn()` to launch a child
16
- * process and returns the ChildProcess handle.
17
- *
18
- * `spawn(command, args, options)` forks a new OS process running `command`
19
- * with the given `args`. It is non-blocking: the parent (this CLI) keeps
20
- * running while the child executes. The returned ChildProcess emits an
21
- * "exit" event when the child terminates, which we use to forward its exit
22
- * code so the shell sees the correct status (e.g. for CI).
23
- *
24
- * `stdio: "inherit"` wires the child's stdin/stdout/stderr directly to the
25
- * terminal that launched the CLI. Without it the child's output would be
26
- * captured internally and never displayed. "inherit" is equivalent to
27
- * passing [process.stdin, process.stdout, process.stderr].
28
- */
29
- const commands = {
30
- /** Start the dev server with Node's built-in file watcher (--watch restarts on .js changes). */
31
- dev: () =>
32
- spawn(
33
- "node",
34
- ["--watch", path.join(serverDir, "index.js")],
35
- { stdio: "inherit" }
36
- ),
37
-
38
- /** Run the prebuild: scan pages/, generate component bundles and route registries. */
39
- build: () =>
40
- spawn(
41
- "node",
42
- [path.join(serverDir, "prebuild.js")],
43
- { stdio: "inherit" }
44
- ),
45
-
46
- /** Start the production server. Sets NODE_ENV=production to disable HMR and file watchers. */
47
- start: () =>
48
- spawn(
49
- "node",
50
- [path.join(serverDir, "index.js")],
51
- { stdio: "inherit", env: { ...process.env, NODE_ENV: "production" } }
52
- ),
53
-
54
- /** Run the static build: prebuild + copy assets to dist/ for deployment without a server. */
55
- "build:static": () =>
56
- spawn(
57
- "node",
58
- [path.join(serverDir, "build-static.js")],
59
- { stdio: "inherit" }
60
- ),
61
- };
62
-
63
- if (!commands[command]) {
64
- console.error(`Unknown command: "${command}"\nAvailable: dev, build, build:static, start`);
65
- process.exit(1);
66
- }
67
-
68
- const child = commands[command]();
69
- child.on("exit", code => process.exit(code ?? 0));
Binary file
@@ -1,55 +0,0 @@
1
- /**
2
- * Central cache for dynamically imported route components.
3
- * Stores already loaded modules to avoid repeated imports.
4
- */
5
- const routeCache = new Map();
6
-
7
- /**
8
- * Load a route component dynamically and cache it.
9
- *
10
- * @param {string} path - Unique path or key for the route module
11
- * @param {() => Promise<any>} importer - Function that imports the module
12
- * @returns {Promise<any>} - Resolves with the imported module
13
- */
14
- export async function loadRouteComponent(path, importer) {
15
- if (routeCache.has(path)) {
16
- return routeCache.get(path);
17
- }
18
-
19
- const module = await importer();
20
- routeCache.set(path, module);
21
- return module;
22
- }
23
-
24
- /**
25
- * Prefetch a route component without rendering it.
26
- *
27
- * @param {string} path
28
- * @param {() => Promise<any>} importer
29
- */
30
- export async function prefetchRouteComponent(path, importer) {
31
- try {
32
- await loadRouteComponent(path, importer);
33
- } catch (e) {
34
- console.error(`Prefetch failed for route ${path}:`, e);
35
- }
36
- }
37
-
38
- /**
39
- * Check if a route component is already loaded.
40
- *
41
- * @param {string} path
42
- * @returns {boolean}
43
- */
44
- export function isRouteLoaded(path) {
45
- return routeCache.has(path);
46
- }
47
-
48
- /**
49
- * Clear cache (optional, e.g., for HMR or logout scenarios)
50
- */
51
- export function clearRouteCache() {
52
- routeCache.clear();
53
- }
54
-
55
- export default routeCache;
@@ -1,22 +0,0 @@
1
- /**
2
- * HMR client script — injected only in dev mode (see root.html).
3
- *
4
- * Opens a Server-Sent Events connection to `/_vexjs/hmr`. When the server emits
5
- * a `reload` event (triggered by a file change), the page reloads automatically
6
- * so the developer always sees the latest version without a manual refresh.
7
- *
8
- * On error (e.g. server restart) the connection is closed silently — the
9
- * browser will reconnect on the next page load.
10
- */
11
- (function () {
12
- const evtSource = new EventSource("/_vexjs/hmr");
13
-
14
- evtSource.addEventListener("reload", (e) => {
15
- console.log(`[HMR] ${e.data || "file changed"} — reloading`);
16
- location.reload();
17
- });
18
-
19
- evtSource.onerror = () => {
20
- evtSource.close();
21
- };
22
- })();