@pyreon/zero 0.12.15 → 0.13.1

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/lib/client.js CHANGED
@@ -79,7 +79,7 @@ function startClient(options) {
79
79
  const hasSSRLoaderData = ssrLoaderData !== void 0 && typeof ssrLoaderData === "object" && ssrLoaderData !== null;
80
80
  if (hasSSRLoaderData) hydrateLoaderData(router, ssrLoaderData);
81
81
  const vnode = h(App, null);
82
- const cleanup = container.childNodes.length > 0 ? hydrateRoot(container, vnode) : mount(vnode, container);
82
+ const cleanup = Array.from(container.childNodes).some((n) => n.nodeType === 1 || n.nodeType === 3 && n.textContent.trim().length > 0) ? hydrateRoot(container, vnode) : mount(vnode, container);
83
83
  if (!hasSSRLoaderData) {
84
84
  const currentPath = router.currentRoute().path;
85
85
  router.replace(currentPath).catch((err) => {
package/lib/client.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":[],"sources":["../src/app.ts","../src/client.ts"],"sourcesContent":["import type { ComponentFn, Props } from '@pyreon/core'\nimport { Fragment, h } from '@pyreon/core'\nimport { HeadProvider } from '@pyreon/head'\nimport type { RouteRecord } from '@pyreon/router'\nimport { createRouter, RouterProvider, RouterView } from '@pyreon/router'\n\n// ─── App assembly ────────────────────────────────────────────────────────────\n\nexport interface CreateAppOptions {\n /** Route definitions (from file-based routing or manual). */\n routes: RouteRecord[]\n\n /** Router mode. Default: \"history\" for SSR, \"hash\" for SPA. */\n routerMode?: 'hash' | 'history'\n\n /** Initial URL for SSR. */\n url?: string\n\n /** Root layout component wrapping all routes. */\n layout?: ComponentFn\n\n /** Global error component. */\n errorComponent?: ComponentFn\n}\n\n/**\n * Create a full Zero app — assembles router, head provider, and root layout.\n *\n * Used internally by entry-server and entry-client.\n */\nexport function createApp(options: CreateAppOptions) {\n const router = createRouter({\n routes: options.routes,\n mode: options.routerMode ?? 'history',\n ...(options.url ? { url: options.url } : {}),\n scrollBehavior: 'top',\n })\n\n const Layout = options.layout ?? DefaultLayout\n\n function App() {\n return h(\n HeadProvider,\n null,\n h(\n RouterProvider as ComponentFn<Props>,\n { router },\n h(Layout, null, h(RouterView as ComponentFn<Props>, null)),\n ),\n )\n }\n\n return { App, router }\n}\n\nfunction DefaultLayout(props: Props) {\n return h(Fragment, null, ...(Array.isArray(props.children) ? props.children : [props.children]))\n}\n","import type { ComponentFn } from '@pyreon/core'\nimport { h } from '@pyreon/core'\nimport type { RouteRecord } from '@pyreon/router'\nimport { hydrateLoaderData } from '@pyreon/router'\nimport { hydrateRoot, mount } from '@pyreon/runtime-dom'\nimport { createApp } from './app'\n\n// ─── Client entry factory ───────────────────────────────────────────────────\n\nexport interface StartClientOptions {\n /** Route definitions. */\n routes: RouteRecord[]\n /** Root layout component. */\n layout?: ComponentFn\n}\n\n/**\n * Start the client-side app — hydrates SSR content or mounts fresh for SPA.\n *\n * ## Loader data flow\n *\n * Direct navigation to a route with a `loader` function needs data to be\n * available on the VERY FIRST render. This is handled in two modes:\n *\n * - **SSR mode (zero's default)**: the server pre-runs loaders, renders the\n * HTML with loader data already applied, and embeds a JSON blob in the\n * HTML as `window.__PYREON_LOADER_DATA__`. On the client we read that\n * blob and call `hydrateLoaderData(router, data)` BEFORE hydrating — so\n * the hydration pass sees the same data the SSR render produced\n * (avoids hydration mismatches and the flash of \"not found\" fallback).\n *\n * - **SPA cold start (no SSR content)**: no `__PYREON_LOADER_DATA__` was\n * embedded, so we call `router.replace(currentPath)` after mount to\n * trigger the loader pipeline for the initial route. The first render\n * shows whatever the component displays for `useLoaderData() === undefined`\n * (typically a loading state or fallback); once loaders resolve, the\n * reactive `useLoaderData` re-renders with the data. This matches\n * standard SPA loading behavior.\n *\n * Without this wiring, direct URL navigation to a loader-backed route\n * (e.g. `/posts/3`) showed the \"Post not found\" fallback indefinitely\n * because `useLoaderData()` returned `undefined` forever. The router\n * only ran loaders on in-app navigation (push/replace), not on initial\n * mount.\n *\n * @example\n * import { routes } from \"virtual:zero/routes\"\n * import { startClient } from \"@pyreon/zero/client\"\n *\n * startClient({ routes })\n */\nexport function startClient(options: StartClientOptions) {\n // `startClient` is the browser entry point — only ever called from a\n // user's `client.ts` mounted in the browser. Explicit guard documents\n // that contract and gives a clearer error than `document is not defined`.\n if (typeof document === 'undefined') {\n throw new Error('[Pyreon] startClient() can only be called in the browser.')\n }\n const container = document.getElementById('app')\n if (!container) throw new Error('[Pyreon] Missing #app container element')\n\n const { App, router } = createApp({\n routes: options.routes,\n routerMode: 'history',\n ...(options.layout ? { layout: options.layout } : {}),\n })\n\n // ── Loader data hydration (SSR path) ───────────────────────────────────────\n // If the server embedded loader data, hydrate it BEFORE mounting so the\n // initial render sees the same data the SSR pass produced. This avoids\n // hydration mismatches and eliminates the flash-of-fallback.\n const ssrLoaderData = (window as unknown as Record<string, unknown>)\n .__PYREON_LOADER_DATA__\n const hasSSRLoaderData =\n ssrLoaderData !== undefined &&\n typeof ssrLoaderData === 'object' &&\n ssrLoaderData !== null\n if (hasSSRLoaderData) {\n // `router` is the public Router<> type; hydrateLoaderData uses the\n // internal RouterInstance shape. The cast is safe because they're\n // the same object at runtime — just narrower/wider type views.\n hydrateLoaderData(router as never, ssrLoaderData as Record<string, unknown>)\n }\n\n const vnode = h(App, null)\n\n // ── Mount vs hydrate ───────────────────────────────────────────────────────\n const hasSSRContent = container.childNodes.length > 0\n const cleanup = hasSSRContent ? hydrateRoot(container, vnode) : mount(vnode, container)\n\n // ── Loader run (SPA cold-start path) ───────────────────────────────────────\n // If we had no SSR loader data AND no SSR content, this is a true SPA\n // cold start. Trigger the router's loader pipeline for the current route\n // via `replace()` with the same path — doesn't change the URL, just kicks\n // off the loader batch. Guards, middleware, and redirects run too, which\n // matches what any other route navigation would do.\n //\n // If we DID have SSR content but NO loader data — that's an unusual case\n // (SSR disabled for this route but loader defined). Run loaders anyway so\n // the client catches up.\n if (!hasSSRLoaderData) {\n const currentPath = router.currentRoute().path\n router.replace(currentPath).catch((err: unknown) => {\n // Loader failures are already reported via the route's error handling\n // pipeline. We swallow the promise rejection here to prevent unhandled\n // rejection warnings — the route's `errorComponent` (if any) already\n // handled the display.\n // @ts-ignore — `import.meta.env.DEV` is provided by Vite/Rolldown at build time\n if (import.meta.env?.DEV === true) {\n // oxlint-disable-next-line no-console\n console.warn(\n '[Pyreon] Initial loader run failed for route:',\n currentPath,\n err,\n )\n }\n })\n }\n\n return cleanup\n}\n"],"mappings":";;;;;;;;;;;AA8BA,SAAgB,UAAU,SAA2B;CACnD,MAAM,SAAS,aAAa;EAC1B,QAAQ,QAAQ;EAChB,MAAM,QAAQ,cAAc;EAC5B,GAAI,QAAQ,MAAM,EAAE,KAAK,QAAQ,KAAK,GAAG,EAAE;EAC3C,gBAAgB;EACjB,CAAC;CAEF,MAAM,SAAS,QAAQ,UAAU;CAEjC,SAAS,MAAM;AACb,SAAO,EACL,cACA,MACA,EACE,gBACA,EAAE,QAAQ,EACV,EAAE,QAAQ,MAAM,EAAE,YAAkC,KAAK,CAAC,CAC3D,CACF;;AAGH,QAAO;EAAE;EAAK;EAAQ;;AAGxB,SAAS,cAAc,OAAc;AACnC,QAAO,EAAE,UAAU,MAAM,GAAI,MAAM,QAAQ,MAAM,SAAS,GAAG,MAAM,WAAW,CAAC,MAAM,SAAS,CAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACLlG,SAAgB,YAAY,SAA6B;AAIvD,KAAI,OAAO,aAAa,YACtB,OAAM,IAAI,MAAM,4DAA4D;CAE9E,MAAM,YAAY,SAAS,eAAe,MAAM;AAChD,KAAI,CAAC,UAAW,OAAM,IAAI,MAAM,0CAA0C;CAE1E,MAAM,EAAE,KAAK,WAAW,UAAU;EAChC,QAAQ,QAAQ;EAChB,YAAY;EACZ,GAAI,QAAQ,SAAS,EAAE,QAAQ,QAAQ,QAAQ,GAAG,EAAE;EACrD,CAAC;CAMF,MAAM,gBAAiB,OACpB;CACH,MAAM,mBACJ,kBAAkB,UAClB,OAAO,kBAAkB,YACzB,kBAAkB;AACpB,KAAI,iBAIF,mBAAkB,QAAiB,cAAyC;CAG9E,MAAM,QAAQ,EAAE,KAAK,KAAK;CAI1B,MAAM,UADgB,UAAU,WAAW,SAAS,IACpB,YAAY,WAAW,MAAM,GAAG,MAAM,OAAO,UAAU;AAYvF,KAAI,CAAC,kBAAkB;EACrB,MAAM,cAAc,OAAO,cAAc,CAAC;AAC1C,SAAO,QAAQ,YAAY,CAAC,OAAO,QAAiB;AAMlD,OAAI,OAAO,KAAK,KAAK,QAAQ,KAE3B,SAAQ,KACN,iDACA,aACA,IACD;IAEH;;AAGJ,QAAO"}
1
+ {"version":3,"file":"client.js","names":[],"sources":["../src/app.ts","../src/client.ts"],"sourcesContent":["import type { ComponentFn, Props } from '@pyreon/core'\nimport { Fragment, h } from '@pyreon/core'\nimport { HeadProvider } from '@pyreon/head'\nimport type { RouteRecord } from '@pyreon/router'\nimport { createRouter, RouterProvider, RouterView } from '@pyreon/router'\n\n// ─── App assembly ────────────────────────────────────────────────────────────\n\nexport interface CreateAppOptions {\n /** Route definitions (from file-based routing or manual). */\n routes: RouteRecord[]\n\n /** Router mode. Default: \"history\" for SSR, \"hash\" for SPA. */\n routerMode?: 'hash' | 'history'\n\n /** Initial URL for SSR. */\n url?: string\n\n /** Root layout component wrapping all routes. */\n layout?: ComponentFn\n\n /** Global error component. */\n errorComponent?: ComponentFn\n}\n\n/**\n * Create a full Zero app — assembles router, head provider, and root layout.\n *\n * Used internally by entry-server and entry-client.\n */\nexport function createApp(options: CreateAppOptions) {\n const router = createRouter({\n routes: options.routes,\n mode: options.routerMode ?? 'history',\n ...(options.url ? { url: options.url } : {}),\n scrollBehavior: 'top',\n })\n\n const Layout = options.layout ?? DefaultLayout\n\n function App() {\n return h(\n HeadProvider,\n null,\n h(\n RouterProvider as ComponentFn<Props>,\n { router },\n h(Layout, null, h(RouterView as ComponentFn<Props>, null)),\n ),\n )\n }\n\n return { App, router }\n}\n\nfunction DefaultLayout(props: Props) {\n return h(Fragment, null, ...(Array.isArray(props.children) ? props.children : [props.children]))\n}\n","import type { ComponentFn } from '@pyreon/core'\nimport { h } from '@pyreon/core'\nimport type { RouteRecord } from '@pyreon/router'\nimport { hydrateLoaderData } from '@pyreon/router'\nimport { hydrateRoot, mount } from '@pyreon/runtime-dom'\nimport { createApp } from './app'\n\n// ─── Client entry factory ───────────────────────────────────────────────────\n\nexport interface StartClientOptions {\n /** Route definitions. */\n routes: RouteRecord[]\n /** Root layout component. */\n layout?: ComponentFn\n}\n\n/**\n * Start the client-side app — hydrates SSR content or mounts fresh for SPA.\n *\n * ## Loader data flow\n *\n * Direct navigation to a route with a `loader` function needs data to be\n * available on the VERY FIRST render. This is handled in two modes:\n *\n * - **SSR mode (zero's default)**: the server pre-runs loaders, renders the\n * HTML with loader data already applied, and embeds a JSON blob in the\n * HTML as `window.__PYREON_LOADER_DATA__`. On the client we read that\n * blob and call `hydrateLoaderData(router, data)` BEFORE hydrating — so\n * the hydration pass sees the same data the SSR render produced\n * (avoids hydration mismatches and the flash of \"not found\" fallback).\n *\n * - **SPA cold start (no SSR content)**: no `__PYREON_LOADER_DATA__` was\n * embedded, so we call `router.replace(currentPath)` after mount to\n * trigger the loader pipeline for the initial route. The first render\n * shows whatever the component displays for `useLoaderData() === undefined`\n * (typically a loading state or fallback); once loaders resolve, the\n * reactive `useLoaderData` re-renders with the data. This matches\n * standard SPA loading behavior.\n *\n * Without this wiring, direct URL navigation to a loader-backed route\n * (e.g. `/posts/3`) showed the \"Post not found\" fallback indefinitely\n * because `useLoaderData()` returned `undefined` forever. The router\n * only ran loaders on in-app navigation (push/replace), not on initial\n * mount.\n *\n * @example\n * import { routes } from \"virtual:zero/routes\"\n * import { startClient } from \"@pyreon/zero/client\"\n *\n * startClient({ routes })\n */\nexport function startClient(options: StartClientOptions) {\n // `startClient` is the browser entry point — only ever called from a\n // user's `client.ts` mounted in the browser. Explicit guard documents\n // that contract and gives a clearer error than `document is not defined`.\n if (typeof document === 'undefined') {\n throw new Error('[Pyreon] startClient() can only be called in the browser.')\n }\n const container = document.getElementById('app')\n if (!container) throw new Error('[Pyreon] Missing #app container element')\n\n const { App, router } = createApp({\n routes: options.routes,\n routerMode: 'history',\n ...(options.layout ? { layout: options.layout } : {}),\n })\n\n // ── Loader data hydration (SSR path) ───────────────────────────────────────\n // If the server embedded loader data, hydrate it BEFORE mounting so the\n // initial render sees the same data the SSR pass produced. This avoids\n // hydration mismatches and eliminates the flash-of-fallback.\n const ssrLoaderData = (window as unknown as Record<string, unknown>)\n .__PYREON_LOADER_DATA__\n const hasSSRLoaderData =\n ssrLoaderData !== undefined &&\n typeof ssrLoaderData === 'object' &&\n ssrLoaderData !== null\n if (hasSSRLoaderData) {\n // `router` is the public Router<> type; hydrateLoaderData uses the\n // internal RouterInstance shape. The cast is safe because they're\n // the same object at runtime — just narrower/wider type views.\n hydrateLoaderData(router as never, ssrLoaderData as Record<string, unknown>)\n }\n\n const vnode = h(App, null)\n\n // ── Mount vs hydrate ───────────────────────────────────────────────────────\n // Ignore comment nodes (Vite injects <!--app-html-->) — only real DOM\n // elements or text nodes count as SSR content worth hydrating.\n const hasSSRContent = Array.from(container.childNodes).some(\n (n) => n.nodeType === 1 || (n.nodeType === 3 && n.textContent!.trim().length > 0),\n )\n const cleanup = hasSSRContent ? hydrateRoot(container, vnode) : mount(vnode, container)\n\n // ── Loader run (SPA cold-start path) ───────────────────────────────────────\n // If we had no SSR loader data AND no SSR content, this is a true SPA\n // cold start. Trigger the router's loader pipeline for the current route\n // via `replace()` with the same path — doesn't change the URL, just kicks\n // off the loader batch. Guards, middleware, and redirects run too, which\n // matches what any other route navigation would do.\n //\n // If we DID have SSR content but NO loader data — that's an unusual case\n // (SSR disabled for this route but loader defined). Run loaders anyway so\n // the client catches up.\n if (!hasSSRLoaderData) {\n const currentPath = router.currentRoute().path\n router.replace(currentPath).catch((err: unknown) => {\n // Loader failures are already reported via the route's error handling\n // pipeline. We swallow the promise rejection here to prevent unhandled\n // rejection warnings — the route's `errorComponent` (if any) already\n // handled the display.\n // @ts-ignore — `import.meta.env.DEV` is provided by Vite/Rolldown at build time\n if (import.meta.env?.DEV === true) {\n // oxlint-disable-next-line no-console\n console.warn(\n '[Pyreon] Initial loader run failed for route:',\n currentPath,\n err,\n )\n }\n })\n }\n\n return cleanup\n}\n"],"mappings":";;;;;;;;;;;AA8BA,SAAgB,UAAU,SAA2B;CACnD,MAAM,SAAS,aAAa;EAC1B,QAAQ,QAAQ;EAChB,MAAM,QAAQ,cAAc;EAC5B,GAAI,QAAQ,MAAM,EAAE,KAAK,QAAQ,KAAK,GAAG,EAAE;EAC3C,gBAAgB;EACjB,CAAC;CAEF,MAAM,SAAS,QAAQ,UAAU;CAEjC,SAAS,MAAM;AACb,SAAO,EACL,cACA,MACA,EACE,gBACA,EAAE,QAAQ,EACV,EAAE,QAAQ,MAAM,EAAE,YAAkC,KAAK,CAAC,CAC3D,CACF;;AAGH,QAAO;EAAE;EAAK;EAAQ;;AAGxB,SAAS,cAAc,OAAc;AACnC,QAAO,EAAE,UAAU,MAAM,GAAI,MAAM,QAAQ,MAAM,SAAS,GAAG,MAAM,WAAW,CAAC,MAAM,SAAS,CAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACLlG,SAAgB,YAAY,SAA6B;AAIvD,KAAI,OAAO,aAAa,YACtB,OAAM,IAAI,MAAM,4DAA4D;CAE9E,MAAM,YAAY,SAAS,eAAe,MAAM;AAChD,KAAI,CAAC,UAAW,OAAM,IAAI,MAAM,0CAA0C;CAE1E,MAAM,EAAE,KAAK,WAAW,UAAU;EAChC,QAAQ,QAAQ;EAChB,YAAY;EACZ,GAAI,QAAQ,SAAS,EAAE,QAAQ,QAAQ,QAAQ,GAAG,EAAE;EACrD,CAAC;CAMF,MAAM,gBAAiB,OACpB;CACH,MAAM,mBACJ,kBAAkB,UAClB,OAAO,kBAAkB,YACzB,kBAAkB;AACpB,KAAI,iBAIF,mBAAkB,QAAiB,cAAyC;CAG9E,MAAM,QAAQ,EAAE,KAAK,KAAK;CAQ1B,MAAM,UAHgB,MAAM,KAAK,UAAU,WAAW,CAAC,MACpD,MAAM,EAAE,aAAa,KAAM,EAAE,aAAa,KAAK,EAAE,YAAa,MAAM,CAAC,SAAS,EAChF,GAC+B,YAAY,WAAW,MAAM,GAAG,MAAM,OAAO,UAAU;AAYvF,KAAI,CAAC,kBAAkB;EACrB,MAAM,cAAc,OAAO,cAAc,CAAC;AAC1C,SAAO,QAAQ,YAAY,CAAC,OAAO,QAAiB;AAMlD,OAAI,OAAO,KAAK,KAAK,QAAQ,KAE3B,SAAQ,KACN,iDACA,aACA,IACD;IAEH;;AAGJ,QAAO"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/zero",
3
- "version": "0.12.15",
3
+ "version": "0.13.1",
4
4
  "description": "Pyreon Zero — zero-config full-stack framework powered by Pyreon and Vite",
5
5
  "license": "MIT",
6
6
  "author": "Vit Bokisch",
@@ -166,18 +166,18 @@
166
166
  "lint": "oxlint ."
167
167
  },
168
168
  "dependencies": {
169
- "@pyreon/core": "^0.12.15",
170
- "@pyreon/head": "^0.12.15",
171
- "@pyreon/meta": "^0.12.15",
172
- "@pyreon/router": "^0.12.15",
173
- "@pyreon/runtime-dom": "^0.12.15",
174
- "@pyreon/runtime-server": "^0.12.15",
175
- "@pyreon/server": "^0.12.15",
176
- "@pyreon/vite-plugin": "^0.12.15",
169
+ "@pyreon/core": "^0.13.1",
170
+ "@pyreon/head": "^0.13.1",
171
+ "@pyreon/meta": "^0.13.1",
172
+ "@pyreon/router": "^0.13.1",
173
+ "@pyreon/runtime-dom": "^0.13.1",
174
+ "@pyreon/runtime-server": "^0.13.1",
175
+ "@pyreon/server": "^0.13.1",
176
+ "@pyreon/vite-plugin": "^0.13.1",
177
177
  "vite": "^8.0.0"
178
178
  },
179
179
  "peerDependencies": {
180
- "@pyreon/reactivity": "^0.12.15",
180
+ "@pyreon/reactivity": "^0.13.1",
181
181
  "sharp": "^0.33.0"
182
182
  },
183
183
  "peerDependenciesMeta": {
package/src/client.ts CHANGED
@@ -85,7 +85,11 @@ export function startClient(options: StartClientOptions) {
85
85
  const vnode = h(App, null)
86
86
 
87
87
  // ── Mount vs hydrate ───────────────────────────────────────────────────────
88
- const hasSSRContent = container.childNodes.length > 0
88
+ // Ignore comment nodes (Vite injects <!--app-html-->) — only real DOM
89
+ // elements or text nodes count as SSR content worth hydrating.
90
+ const hasSSRContent = Array.from(container.childNodes).some(
91
+ (n) => n.nodeType === 1 || (n.nodeType === 3 && n.textContent!.trim().length > 0),
92
+ )
89
93
  const cleanup = hasSSRContent ? hydrateRoot(container, vnode) : mount(vnode, container)
90
94
 
91
95
  // ── Loader run (SPA cold-start path) ───────────────────────────────────────